Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Updated PBKDF2 hasher with more complex format handling #319

Merged
merged 2 commits into from
Mar 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 25 additions & 18 deletions hashing/hashing_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,22 +126,29 @@ func TestArgon2ID(t *testing.T) {

func TestPBKDF2(t *testing.T) {
password := "test-password"

// Test base64.
hasher := NewPBKDF2Hasher(defaultPBKDF2SaltSize, defaultPBKDF2Iterations, defaultPBKDF2Algorithm, Base64, defaultPBKDF2KeyLen)

passwordHash, err := hasher.Hash(password)

assert.Nil(t, err)
assert.True(t, hasher.Compare(password, passwordHash))
assert.False(t, hasher.Compare("other", passwordHash))

// Test UTF8.
hasher = NewPBKDF2Hasher(defaultPBKDF2SaltSize, defaultPBKDF2Iterations, defaultPBKDF2Algorithm, UTF8, defaultPBKDF2KeyLen)

passwordHash, err = hasher.Hash(password)

assert.Nil(t, err)
assert.True(t, hasher.Compare(password, passwordHash))
assert.False(t, hasher.Compare("other", passwordHash))
b64Hasher := NewPBKDF2Hasher(defaultPBKDF2SaltSize, defaultPBKDF2Iterations, defaultPBKDF2Algorithm, Base64, defaultPBKDF2KeyLen)
utf8Hasher := NewPBKDF2Hasher(defaultPBKDF2SaltSize, defaultPBKDF2Iterations, defaultPBKDF2Algorithm, UTF8, defaultPBKDF2KeyLen)

t.Run("OlderFormat", func(t *testing.T) {
t.Run("Base64", func(t *testing.T) {
passwordHash, err := b64Hasher.Hash(password)

assert.Nil(t, err)
assert.True(t, b64Hasher.Compare(password, passwordHash))
assert.False(t, b64Hasher.Compare("other", passwordHash))
})
t.Run("UTF8", func(t *testing.T) {
passwordHash, err := utf8Hasher.Hash(password)

assert.Nil(t, err)
assert.True(t, utf8Hasher.Compare(password, passwordHash))
assert.False(t, utf8Hasher.Compare("other", passwordHash))
})
})

t.Run("PHC-SF-Spec", func(t *testing.T) {
passwordHash := "$pbkdf2-sha512$i=10000,l=32$/DsNR8DBuoF/MxzLY+QVaw$YNfYNfT+6yT2blLrXKKR8Ll+aesgHYqSOtFTBsyscRM"
assert.True(t, b64Hasher.Compare(password, passwordHash))
assert.False(t, b64Hasher.Compare("other", passwordHash))
})
}
145 changes: 108 additions & 37 deletions hashing/pbkdf2.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,34 +23,32 @@
keyLen int
}

func NewPBKDF2Hasher(saltSize int, iterations int, algorithm string, saltEncoding string, keylen int) HashComparer {
func NewPBKDF2Hasher(saltSize int, iterations int, algorithm string, saltEncoding string, keyLen int) HashComparer {
return pbkdf2Hasher{
saltSize: saltSize,
iterations: iterations,
algorithm: algorithm,
saltEncoding: preferredEncoding(saltEncoding),
keyLen: keylen,
keyLen: keyLen,
}
}

/*
* PBKDF2 methods are adapted from github.com/brocaar/chirpstack-application-server, some comments included.
*/

// Hash function reference may be found at https://github.com/brocaar/chirpstack-application-server/blob/master/internal/storage/user.go#L421.

// Generate the hash of a password for storage in the database.
// NOTE: We store the details of the hashing algorithm with the hash itself,
// making it easy to recreate the hash for password checking, even if we change
// the default criteria here.
// Hash function generates a hash of the supplied password. The hash
// can then be stored directly in the database. The return hash will
// contain options according to the PHC String format found at
// https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md
func (h pbkdf2Hasher) Hash(password string) (string, error) {
// Generate a random salt value with the given salt size.
salt := make([]byte, h.saltSize)
_, err := rand.Read(salt)

// We need to ensure that salt doesn contain $, which is 36 in decimal.
// So we check if there'sbyte that represents $ and change it with a random number in the range 0-35
//// This is far from ideal, but should be good enough with a reasonable salt size.
// We need to ensure that salt doesn't contain $, which is 36 in decimal.
// So we check if there's byte that represents $ and change it with a random number in the range 0-35
// // This is far from ideal, but should be good enough with a reasonable salt size.
for i := 0; i < len(salt); i++ {
if salt[i] == 36 {
n, err := rand.Int(rand.Reader, big.NewInt(35))
Expand All @@ -69,52 +67,125 @@
return h.hashWithSalt(password, salt, h.iterations, h.algorithm, h.keyLen), nil
}

// HashCompare verifies that passed password hashes to the same value as the
// Compare verifies that passed password hashes to the same value as the
// passed passwordHash.
// Reference: https://github.com/brocaar/chirpstack-application-server/blob/master/internal/storage/user.go#L458.
// Parsing reference: https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md
func (h pbkdf2Hasher) Compare(password string, passwordHash string) bool {
hashSplit := strings.Split(passwordHash, "$")

if len(hashSplit) != 5 {
log.Errorf("invalid PBKDF2 hash supplied, expected length 5, got: %d", len(hashSplit))
return false
}

algorithm := hashSplit[1]
hashSplit := h.getFields(passwordHash)

var (
err error
algorithm string
paramString string
hashedPassword []byte
salt []byte
iterations int
keyLen int
)
if hashSplit[0] == "PBKDF2" {
algorithm = hashSplit[1]
iterations, err = strconv.Atoi(hashSplit[2])
if err != nil {
log.Errorf("iterations error: %s", err)
return false
}

iterations, err := strconv.Atoi(hashSplit[2])
if err != nil {
log.Errorf("iterations error: %s", err)
return false
}
switch h.saltEncoding {
case UTF8:
salt = []byte(hashSplit[3])
default:
var err error
salt, err = base64.StdEncoding.DecodeString(hashSplit[3])
if err != nil {
log.Errorf("base64 salt error: %s", err)
return false
}
}

var salt []byte
switch h.saltEncoding {
case UTF8:
salt = []byte(hashSplit[3])
default:
salt, err = base64.StdEncoding.DecodeString(hashSplit[3])
hashedPassword, err = base64.StdEncoding.DecodeString(hashSplit[4])
if err != nil {
log.Errorf("base64 salt error: %s", err)
log.Errorf("base64 hash decoding error: %s", err)
return false
}
keyLen = len(hashedPassword)

} else if hashSplit[0] == "pbkdf2-sha512" {
algorithm = "sha512"
paramString = hashSplit[1]

opts := strings.Split(paramString, ",")
for _, opt := range opts {
parts := strings.Split(opt, "=")
for i := 0; i < len(parts); i += 2 {
key := parts[i]
val := parts[i+1]
switch key {
case "i":
iterations, _ = strconv.Atoi(val)
case "l":
keyLen, _ = strconv.Atoi(val)
default:
log.Errorf("unknown options key (\"%s\")", key)
Dismissed Show dismissed Hide dismissed
return false
}
}
}

switch h.saltEncoding {
case UTF8:
salt = []byte(hashSplit[2])
default:
var err error
salt, err = base64.StdEncoding.WithPadding(base64.NoPadding).DecodeString(hashSplit[2])
if err != nil {
log.Errorf("base64 salt error: %s", err)
return false
}
}

hashedPassword, err = base64.StdEncoding.WithPadding(base64.NoPadding).DecodeString(hashSplit[3])
} else {
log.Errorf("invalid PBKDF2 hash supplied, unrecognized format \"%s\"", hashSplit[0])
Dismissed Show dismissed Hide dismissed
return false
}

hashedPassword, err := base64.StdEncoding.DecodeString(hashSplit[4])
newHash := h.hashWithSalt(password, salt, iterations, algorithm, keyLen)
hashSplit = h.getFields(newHash)
newHashedPassword, err := base64.StdEncoding.DecodeString(hashSplit[4])
if err != nil {
log.Errorf("base64 hash decoding error: %s", err)
log.Errorf("base64 salt error: %s", err)
return false
}

keylen := len(hashedPassword)
return h.compareBytes(hashedPassword, newHashedPassword)
}

func (h pbkdf2Hasher) compareBytes(a, b []byte) bool {
for i, x := range a {
if b[i] != x {
return false
}
}
return true
}

return passwordHash == h.hashWithSalt(password, salt, iterations, algorithm, keylen)
func (h pbkdf2Hasher) getFields(passwordHash string) []string {
hashSplit := strings.FieldsFunc(passwordHash, func(r rune) bool {
switch r {
case '$':
return true
default:
return false
}
})
return hashSplit
}

// Reference: https://github.com/brocaar/chirpstack-application-server/blob/master/internal/storage/user.go#L432.
func (h pbkdf2Hasher) hashWithSalt(password string, salt []byte, iterations int, algorithm string, keylen int) string {
// Generate the hashed password. This should be a little painful, adjust ITERATIONS
// if it needs performance tweeking. Greatly depends on the hardware.
// if it needs performance tweaking. Greatly depends on the hardware.
// NOTE: We store these details with the returned hashed, so changes will not
// affect our ability to do password compares.
shaHash := sha512.New
Expand Down
Loading