Skip to content

Commit

Permalink
feat: allow importing scrypt hashing algorithm (ory#2689)
Browse files Browse the repository at this point in the history
It is now possible to import scrypt-hashed passwords.

See ory#2422
  • Loading branch information
LandonPattison authored Sep 8, 2022
1 parent b48481a commit 17dbb96
Show file tree
Hide file tree
Showing 6 changed files with 114 additions and 1 deletion.
61 changes: 61 additions & 0 deletions hash/hash_comparator.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"golang.org/x/crypto/argon2"
"golang.org/x/crypto/bcrypt"
"golang.org/x/crypto/pbkdf2"
"golang.org/x/crypto/scrypt"

"github.com/ory/kratos/driver/config"
)
Expand All @@ -28,6 +29,8 @@ func Compare(ctx context.Context, password []byte, hash []byte) error {
return CompareArgon2i(ctx, password, hash)
case IsPbkdf2Hash(hash):
return ComparePbkdf2(ctx, password, hash)
case IsScryptHash(hash):
return CompareScrypt(ctx, password, hash)
default:
return errors.WithStack(ErrUnknownHashAlgorithm)
}
Expand Down Expand Up @@ -106,11 +109,35 @@ func ComparePbkdf2(_ context.Context, password []byte, hash []byte) error {
return errors.WithStack(ErrMismatchedHashAndPassword)
}

func CompareScrypt(_ context.Context, password []byte, hash []byte) error {
// Extract the parameters, salt and derived key from the encoded password
// hash.
p, salt, hash, err := decodeScryptHash(string(hash))
if err != nil {
return err
}

// Derive the key from the other password using the same parameters.
otherHash, err := scrypt.Key(password, salt, int(p.Cost), int(p.Block), int(p.Parrellization), int(p.KeyLength))
if err != nil {
return errors.WithStack(err)
}

// Check that the contents of the hashed passwords are identical. Note
// that we are using the subtle.ConstantTimeCompare() function for this
// to help prevent timing attacks.
if subtle.ConstantTimeCompare(hash, otherHash) == 1 {
return nil
}
return errors.WithStack(ErrMismatchedHashAndPassword)
}

var (
isBcryptHash = regexp.MustCompile(`^\$2[abzy]?\$`)
isArgon2idHash = regexp.MustCompile(`^\$argon2id\$`)
isArgon2iHash = regexp.MustCompile(`^\$argon2i\$`)
isPbkdf2Hash = regexp.MustCompile(`^\$pbkdf2-sha[0-9]{1,3}\$`)
isScryptHash = regexp.MustCompile(`^\$scrypt\$`)
)

func IsBcryptHash(hash []byte) bool {
Expand All @@ -129,6 +156,10 @@ func IsPbkdf2Hash(hash []byte) bool {
return isPbkdf2Hash.Match(hash)
}

func IsScryptHash(hash []byte) bool {
return isScryptHash.Match(hash)
}

func decodeArgon2idHash(encodedHash string) (p *config.Argon2, salt, hash []byte, err error) {
parts := strings.Split(encodedHash, "$")
if len(parts) != 6 {
Expand Down Expand Up @@ -199,3 +230,33 @@ func decodePbkdf2Hash(encodedHash string) (p *Pbkdf2, salt, hash []byte, err err

return p, salt, hash, nil
}

// decodeScryptHash decodes Scrypt encoded password hash.
// format: $scrypt$ln=<cost>,r=<block>,p=<parrrelization>$<salt>$<hash>
func decodeScryptHash(encodedHash string) (p *Scrypt, salt, hash []byte, err error) {
parts := strings.Split(encodedHash, "$")
if len(parts) != 5 {
return nil, nil, nil, ErrInvalidHash
}

p = new(Scrypt)

_, err = fmt.Sscanf(parts[2], "ln=%d,r=%d,p=%d", &p.Cost, &p.Block, &p.Parrellization)
if err != nil {
return nil, nil, nil, err
}

salt, err = base64.StdEncoding.Strict().DecodeString(parts[3])
if err != nil {
return nil, nil, nil, err
}
p.SaltLength = uint32(len(salt))

hash, err = base64.StdEncoding.Strict().DecodeString(parts[4])
if err != nil {
return nil, nil, nil, err
}
p.KeyLength = uint32(len(hash))

return p, salt, hash, nil
}
9 changes: 9 additions & 0 deletions hash/hasher_scrypt.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package hash

type Scrypt struct {
Cost uint32
Block uint32
Parrellization uint32
SaltLength uint32
KeyLength uint32
}
11 changes: 11 additions & 0 deletions hash/hasher_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -231,4 +231,15 @@ func TestCompare(t *testing.T) {
assert.Error(t, hash.Compare(context.Background(), []byte("test"), []byte("$pbkdf2-sha256$i=100000,l=32$1jP+5Zxpxgtee/iPxGgOz0RfE9/KJuDElP1ley4VxXcc$QJxzfvdbHYBpydCbHoFg3GJEqMFULwskiuqiJctoYpI")))
assert.Error(t, hash.Compare(context.Background(), []byte("test"), []byte("$pbkdf2-sha256$i=100000,l=32$1jP+5Zxpxgtee/iPxGgOz0RfE9/KJuDElP1ley4VxXc$QJxzfvdbHYBpydCbHoFg3GJEqMFULwskiuqiJctoYpII")))
assert.Error(t, hash.Compare(context.Background(), []byte("test"), []byte("$pbkdf2-sha512$I=100000,l=32$bdHBpn7OWOivJMVJypy2UqR0UnaD5prQXRZevj/05YU$+wArTfv1a+bNGO1iZrmEdVjhA+lL11wF4/IxpgYfPwc")))

assert.Nil(t, hash.Compare(context.Background(), []byte("test"), []byte("$scrypt$ln=16384,r=8,p=1$2npRo7P03Mt8keSoMbyD/tKFWyUzjiQf2svUaNDSrhA=$MiCzNcIplSMqSBrm4HckjYqYhaVPPjTARTzwB1cVNYE=")))
assert.Nil(t, hash.CompareScrypt(context.Background(), []byte("test"), []byte("$scrypt$ln=16384,r=8,p=1$2npRo7P03Mt8keSoMbyD/tKFWyUzjiQf2svUaNDSrhA=$MiCzNcIplSMqSBrm4HckjYqYhaVPPjTARTzwB1cVNYE=")))
assert.Error(t, hash.Compare(context.Background(), []byte("test"), []byte("$scrypt$ln=16384,r=8,p=1$2npRo7P03Mt8keSoMbyD/tKFWyUzjiQf2svUaNDSrhA=$MiCzNcIplSMqSBrm4HckjYqYhaVPPjTARTzwB1cVNYF=")))

assert.Error(t, hash.Compare(context.Background(), []byte("test"), []byte("$scrypt$2npRo7P03Mt8keSoMbyD/tKFWyUzjiQf2svUaNDSrhA=$MiCzNcIplSMqSBrm4HckjYqYhaVPPjTARTzwB1cVNYE=")))
assert.Error(t, hash.Compare(context.Background(), []byte("test"), []byte("$scrypt$ln=16384,r=8,p=1$(2npRo7P03Mt8keSoMbyD/tKFWyUzjiQf2svUaNDSrhA=$MiCzNcIplSMqSBrm4HckjYqYhaVPPjTARTzwB1cVNYE=")))
assert.Error(t, hash.Compare(context.Background(), []byte("test"), []byte("$scrypt$ln=16384,r=8,p=1$(2npRo7P03Mt8keSoMbyD/tKFWyUzjiQf2svUaNDSrhA=$(MiCzNcIplSMqSBrm4HckjYqYhaVPPjTARTzwB1cVNYE=")))
assert.Error(t, hash.Compare(context.Background(), []byte("test"), []byte("$scrypt$ln=16385,r=8,p=1$2npRo7P03Mt8keSoMbyD/tKFWyUzjiQf2svUaNDSrhA=$MiCzNcIplSMqSBrm4HckjYqYhaVPPjTARTzwB1cVNYE=")))
assert.Error(t, hash.Compare(context.Background(), []byte("tesu"), []byte("$scrypt$ln=16384,r=8,p=1$2npRo7P03Mt8keSoMbyD/tKFWyUzjiQf2svUaNDSrhA=$MiCzNcIplSMqSBrm4HckjYqYhaVPPjTARTzwB1cVNYE=")))
assert.Error(t, hash.Compare(context.Background(), []byte("tesu"), []byte("$scrypt$ln=abc,r=8,p=1$2npRo7P03Mt8keSoMbyD/tKFWyUzjiQf2svUaNDSrhA=$MiCzNcIplSMqSBrm4HckjYqYhaVPPjTARTzwB1cVNYE=")))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"credentials": {
"password": {
"type": "password",
"identifiers": [
"[email protected]"
],
"config": {
},
"version": 0
}
},
"schema_id": "default",
"state": "active",
"traits": {
"email": "[email protected]"
},
"metadata_public": null,
"metadata_admin": null
}
2 changes: 1 addition & 1 deletion identity/handler_import.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ func (h *Handler) importPasswordCredentials(ctx context.Context, i *Identity, cr
creds.Config.HashedPassword = string(hashed)
}

if !(hash.IsArgon2idHash(hashed) || hash.IsArgon2iHash(hashed) || hash.IsBcryptHash(hashed) || hash.IsPbkdf2Hash(hashed)) {
if !(hash.IsArgon2idHash(hashed) || hash.IsArgon2iHash(hashed) || hash.IsBcryptHash(hashed) || hash.IsPbkdf2Hash(hashed) || hash.IsScryptHash(hashed)) {
return errors.WithStack(herodot.ErrBadRequest.WithReasonf("The imported password does not match any known hash format. For more information see https://www.ory.sh/dr/2"))
}

Expand Down
12 changes: 12 additions & 0 deletions identity/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,18 @@ func TestHandler(t *testing.T) {

require.NoError(t, hash.Compare(ctx, []byte("123456"), []byte(gjson.GetBytes(actual.Credentials[identity.CredentialsTypePassword].Config, "hashed_password").String())))
})

t.Run("with scrypt password", func(t *testing.T) {
res := send(t, adminTS, "POST", "/identities", http.StatusCreated, identity.AdminCreateIdentityBody{Traits: []byte(`{"email": "[email protected]"}`),
Credentials: &identity.AdminIdentityImportCredentials{Password: &identity.AdminIdentityImportCredentialsPassword{
Config: identity.AdminIdentityImportCredentialsPasswordConfig{HashedPassword: "$scrypt$ln=16384,r=8,p=1$ZtQva9xCHzlSELH/mA7Kj5KjH2tCrkbwYzdxknkL0QQ=$pnTcXKaWVT+FwFDdk3vO1K0J7ZgOxdSU1tCJNYmn8zI="}}}})
actual, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(ctx, uuid.FromStringOrNil(res.Get("id").String()))
require.NoError(t, err)

snapshotx.SnapshotTExceptMatchingKeys(t, identity.WithCredentialsAndAdminMetadataInJSON(*actual), append(ignoreDefault, "hashed_password"))

require.NoError(t, hash.Compare(ctx, []byte("123456"), []byte(gjson.GetBytes(actual.Credentials[identity.CredentialsTypePassword].Config, "hashed_password").String())))
})
})

t.Run("case=unable to set ID itself", func(t *testing.T) {
Expand Down

0 comments on commit 17dbb96

Please sign in to comment.