diff --git a/hash/hash_comparator.go b/hash/hash_comparator.go index 0200e08cad22..e68c1e663286 100644 --- a/hash/hash_comparator.go +++ b/hash/hash_comparator.go @@ -8,13 +8,16 @@ import ( "context" "crypto/aes" "crypto/cipher" + "crypto/hmac" "crypto/md5" //#nosec G501 -- compatibility for imported passwords "crypto/sha1" //#nosec G505 -- compatibility for imported passwords "crypto/sha256" "crypto/sha512" "crypto/subtle" "encoding/base64" + "encoding/hex" "fmt" + "hash" "regexp" "strings" @@ -23,6 +26,10 @@ import ( "go.opentelemetry.io/otel/attribute" "golang.org/x/crypto/argon2" "golang.org/x/crypto/bcrypt" + + //nolint:staticcheck + //lint:ignore SA1019 + "golang.org/x/crypto/md4" //#nosec G501 -- compatibility for imported passwords "golang.org/x/crypto/pbkdf2" "golang.org/x/crypto/scrypt" @@ -94,6 +101,9 @@ func Compare(ctx context.Context, password []byte, hash []byte) error { case IsMD5Hash(hash): span.SetAttributes(attribute.String("hash.type", "md5")) return CompareMD5(ctx, password, hash) + case IsHMACHash(hash): + span.SetAttributes(attribute.String("hash.type", "hmac")) + return CompareHMAC(ctx, password, hash) default: span.SetAttributes(attribute.String("hash.type", "unknown")) return errors.WithStack(ErrUnknownHashAlgorithm) @@ -270,6 +280,24 @@ func CompareMD5(_ context.Context, password []byte, hash []byte) error { return comparePasswordHashConstantTime(hash, otherHash[:]) } +func CompareHMAC(_ context.Context, password []byte, hash []byte) error { + // Extract the hash from the encoded password + hasher, hash, key, err := decodeHMACHash(string(hash)) + if err != nil { + return err + } + + mac := hmac.New(hasher, key) + _, err = mac.Write([]byte(password)) + if err != nil { + return err + } + + otherHash := []byte(hex.EncodeToString(mac.Sum(nil))) + + return comparePasswordHashConstantTime(hash, otherHash) +} + var ( isMD5CryptHash = regexp.MustCompile(`^\$md5-crypt\$`) isBcryptHash = regexp.MustCompile(`^\$2[abzy]?\$`) @@ -283,6 +311,7 @@ var ( isSHAHash = regexp.MustCompile(`^\$sha(1|256|512)\$`) isFirebaseScryptHash = regexp.MustCompile(`^\$firescrypt\$`) isMD5Hash = regexp.MustCompile(`^\$md5\$`) + isHMACHash = regexp.MustCompile(`^\$hmac-(md4|md5|sha1|sha224|sha256|sha384|sha512)\$`) ) func IsMD5CryptHash(hash []byte) bool { return isMD5CryptHash.Match(hash) } @@ -297,6 +326,7 @@ func IsSSHAHash(hash []byte) bool { return isSSHAHash.Match(hash) } func IsSHAHash(hash []byte) bool { return isSHAHash.Match(hash) } func IsFirebaseScryptHash(hash []byte) bool { return isFirebaseScryptHash.Match(hash) } func IsMD5Hash(hash []byte) bool { return isMD5Hash.Match(hash) } +func IsHMACHash(hash []byte) bool { return isHMACHash.Match(hash) } func IsValidHashFormat(hash []byte) bool { if IsMD5CryptHash(hash) || @@ -310,7 +340,8 @@ func IsValidHashFormat(hash []byte) bool { IsSSHAHash(hash) || IsSHAHash(hash) || IsFirebaseScryptHash(hash) || - IsMD5Hash(hash) { + IsMD5Hash(hash) || + IsHMACHash(hash) { return true } else { return false @@ -614,6 +645,53 @@ func decodeMD5Hash(encodedHash string) (pf, salt, hash []byte, err error) { } } +// decodeHMACHash decodes HMAC encoded password hash. +// format : $hmac-$$ +func decodeHMACHash(encodedHash string) (hasher func() hash.Hash, hash, key []byte, err error) { + parts := strings.Split(encodedHash, "$") + + if len(parts) != 4 { + return nil, nil, nil, ErrInvalidHash + } + + hashMatch := isHMACHash.FindStringSubmatch(encodedHash) + + if len(hashMatch) != 2 { + return nil, nil, nil, errors.WithStack(ErrUnknownHashAlgorithm) + } + + switch hashMatch[1] { + case "md4": + hasher = md4.New //#nosec G401 -- compatibility for imported passwords + case "md5": + hasher = md5.New //#nosec G401 -- compatibility for imported passwords + case "sha1": + hasher = sha1.New //#nosec G401 -- compatibility for imported passwords + case "sha224": + hasher = sha256.New224 + case "sha256": + hasher = sha256.New + case "sha384": + hasher = sha512.New384 + case "sha512": + hasher = sha512.New + default: + return nil, nil, nil, errors.WithStack(ErrUnknownHashAlgorithm) + } + + hash, err = base64.StdEncoding.Strict().DecodeString(parts[2]) + if err != nil { + return nil, nil, nil, err + } + + key, err = base64.StdEncoding.Strict().DecodeString(parts[3]) + if err != nil { + return nil, nil, nil, err + } + + return hasher, hash, key, nil +} + func comparePasswordHashConstantTime(hash, otherHash []byte) error { // use subtle.ConstantTimeCompare() to prevent timing attacks. if subtle.ConstantTimeCompare(hash, otherHash) == 1 { diff --git a/hash/hasher_test.go b/hash/hasher_test.go index f2fa9d4fdab7..dc715d3d141f 100644 --- a/hash/hasher_test.go +++ b/hash/hasher_test.go @@ -413,4 +413,125 @@ func TestCompare(t *testing.T) { assert.Error(t, hash.Compare(context.Background(), []byte("ory"), []byte("$sha512-crypt$$")), "shacrypt decode error: provided encoded hash has an invalid format") assert.Error(t, hash.Compare(context.Background(), []byte("ory"), []byte("$sha512-crypt$$$"))) }) + + t.Run("hmac errors", func(t *testing.T) { + t.Parallel() + + //Missing Key + assert.ErrorIs(t, hash.Compare(context.Background(), []byte("test"), []byte("$hmac-md5$ZmU4Njk3Zjc0MmQwODA0MDVkMTI3MGU2MTYzMzE2Zjk=")), hash.ErrInvalidHash) + assert.Error(t, hash.CompareHMAC(context.Background(), []byte("test"), []byte("$hmac-md5$ZmU4Njk3Zjc0MmQwODA0MDVkMTI3MGU2MTYzMzE2Zjk="))) + assert.ErrorIs(t, hash.Compare(context.Background(), []byte("test"), []byte("$hmac-md5$ZmU4Njk3Zjc0MmQwODA0MDVkMTI3MGU2MTYzMzE2Zjk=$")), hash.ErrMismatchedHashAndPassword) + assert.Error(t, hash.CompareHMAC(context.Background(), []byte("test"), []byte("$hmac-md5$ZmU4Njk3Zjc0MmQwODA0MDVkMTI3MGU2MTYzMzE2Zjk=$"))) + //Missing Password Hash + assert.ErrorIs(t, hash.Compare(context.Background(), []byte("test"), []byte("$hmac-md5$MTIzNDU=")), hash.ErrInvalidHash) + assert.Error(t, hash.CompareHMAC(context.Background(), []byte("test"), []byte("$hmac-md5$MTIzNDU="))) + assert.ErrorIs(t, hash.Compare(context.Background(), []byte("test"), []byte("$hmac-md5$$MTIzNDU=")), hash.ErrMismatchedHashAndPassword) + assert.Error(t, hash.CompareHMAC(context.Background(), []byte("test"), []byte("$hmac-md5$$MTIzNDU="))) + //Missing Password Hash and Key + assert.ErrorIs(t, hash.Compare(context.Background(), []byte("test"), []byte("$hmac-md5$")), hash.ErrInvalidHash) + assert.Error(t, hash.CompareHMAC(context.Background(), []byte("test"), []byte("$hmac-md5$"))) + //Missing Hash Algorithm + assert.ErrorIs(t, hash.Compare(context.Background(), []byte("test"), []byte("$hmac$ZmU4Njk3Zjc0MmQwODA0MDVkMTI3MGU2MTYzMzE2Zjk=$MTIzNDU=")), hash.ErrUnknownHashAlgorithm) + assert.Error(t, hash.CompareHMAC(context.Background(), []byte("test"), []byte("$hmac$ZmU4Njk3Zjc0MmQwODA0MDVkMTI3MGU2MTYzMzE2Zjk=$MTIzNDU="))) + //Missing Invalid Hash Algorithm + assert.ErrorIs(t, hash.Compare(context.Background(), []byte("test"), []byte("$hmac-invalid$ZmU4Njk3Zjc0MmQwODA0MDVkMTI3MGU2MTYzMzE2Zjk=$MTIzNDU=")), hash.ErrUnknownHashAlgorithm) + assert.Error(t, hash.CompareHMAC(context.Background(), []byte("test"), []byte("$hmac-invalid$ZmU4Njk3Zjc0MmQwODA0MDVkMTI3MGU2MTYzMzE2Zjk=$MTIzNDU="))) + + }) + + t.Run("hmac-md4", func(t *testing.T) { + t.Parallel() + + //Valid + assert.Nil(t, hash.Compare(context.Background(), []byte("test"), []byte("$hmac-md4$MWQ5ZTI4Nzc2Zjg4YmE2MTQ5YjQ0OTMyOGE4NWU4YjA=$MTIzNDU="))) + assert.Nil(t, hash.CompareHMAC(context.Background(), []byte("test"), []byte("$hmac-md4$MWQ5ZTI4Nzc2Zjg4YmE2MTQ5YjQ0OTMyOGE4NWU4YjA=$MTIzNDU="))) + //Wrong Key + assert.Error(t, hash.Compare(context.Background(), []byte("test"), []byte("$hmac-md4$MWQ5ZTI4Nzc2Zjg4YmE2MTQ5YjQ0OTMyOGE4NWU4YjA=$MTIzNA==")), hash.ErrMismatchedHashAndPassword) + //Different password + assert.Error(t, hash.Compare(context.Background(), []byte("ory"), []byte("$hmac-md4$MWQ5ZTI4Nzc2Zjg4YmE2MTQ5YjQ0OTMyOGE4NWU4YjA=$MTIzNDU="))) + }) + + t.Run("hmac-md5", func(t *testing.T) { + t.Parallel() + + //Valid + assert.Nil(t, hash.Compare(context.Background(), []byte("test"), []byte("$hmac-md5$ZmU4Njk3Zjc0MmQwODA0MDVkMTI3MGU2MTYzMzE2Zjk=$MTIzNDU="))) + assert.Nil(t, hash.CompareHMAC(context.Background(), []byte("test"), []byte("$hmac-md5$ZmU4Njk3Zjc0MmQwODA0MDVkMTI3MGU2MTYzMzE2Zjk=$MTIzNDU="))) + + //Wrong Key + assert.Error(t, hash.Compare(context.Background(), []byte("test"), []byte("$hmac-md5$ZmU4Njk3Zjc0MmQwODA0MDVkMTI3MGU2MTYzMzE2Zjk=$MTIzNA=="))) + //Different password + assert.Error(t, hash.Compare(context.Background(), []byte("ory"), []byte("$hmac-md5$ZmU4Njk3Zjc0MmQwODA0MDVkMTI3MGU2MTYzMzE2Zjk=$MTIzNDU="))) + + }) + + t.Run("hmac-sha1", func(t *testing.T) { + t.Parallel() + + //Valid + assert.Nil(t, hash.Compare(context.Background(), []byte("test"), []byte("$hmac-sha1$NDMyNjcxZTUyY2Y2YTBmYjZjZDE2NjQxYjAwNjFiZjAwOGEzNWM5MA==$MTIzNDU="))) + assert.Nil(t, hash.CompareHMAC(context.Background(), []byte("test"), []byte("$hmac-sha1$NDMyNjcxZTUyY2Y2YTBmYjZjZDE2NjQxYjAwNjFiZjAwOGEzNWM5MA==$MTIzNDU="))) + + //Wrong Key + assert.Error(t, hash.Compare(context.Background(), []byte("test"), []byte("$hmac-sha1$NDMyNjcxZTUyY2Y2YTBmYjZjZDE2NjQxYjAwNjFiZjAwOGEzNWM5MA==$MTIzNA=="))) + //Different password + assert.Error(t, hash.Compare(context.Background(), []byte("ory"), []byte("$hmac-sha1$NDMyNjcxZTUyY2Y2YTBmYjZjZDE2NjQxYjAwNjFiZjAwOGEzNWM5MA==$MTIzNDU="))) + + }) + + t.Run("hmac-sha224", func(t *testing.T) { + t.Parallel() + + //Valid + assert.Nil(t, hash.Compare(context.Background(), []byte("test"), []byte("$hmac-sha224$YmUwYmYzM2EwNGRlNDE0YjQzNjBhNmIyOThmNmIyYzI4OWQyMzk3MDUwZDFjMzliYjVmMDMyOTQ=$MTIzNDU="))) + assert.Nil(t, hash.CompareHMAC(context.Background(), []byte("test"), []byte("$hmac-sha224$YmUwYmYzM2EwNGRlNDE0YjQzNjBhNmIyOThmNmIyYzI4OWQyMzk3MDUwZDFjMzliYjVmMDMyOTQ=$MTIzNDU="))) + + //Wrong Key + assert.Error(t, hash.Compare(context.Background(), []byte("test"), []byte("$hmac-sha224$YmUwYmYzM2EwNGRlNDE0YjQzNjBhNmIyOThmNmIyYzI4OWQyMzk3MDUwZDFjMzliYjVmMDMyOTQ=$MTIzNA=="))) + //Different password + assert.Error(t, hash.Compare(context.Background(), []byte("ory"), []byte("$hmac-sha224$YmUwYmYzM2EwNGRlNDE0YjQzNjBhNmIyOThmNmIyYzI4OWQyMzk3MDUwZDFjMzliYjVmMDMyOTQ=$MTIzNDU="))) + + }) + + t.Run("hmac-sha256", func(t *testing.T) { + t.Parallel() + + //Valid + assert.Nil(t, hash.Compare(context.Background(), []byte("test"), []byte("$hmac-sha256$ZTAzMWJhMWMyOTM4YjFkMjgzZjkxOWExZGY5YWM2NmMxOTJhN2RkNzQ0MzJkNWZkNGFkYTI5OTk0MWJhMTA5Zg==$MTIzNDU="))) + assert.Nil(t, hash.CompareHMAC(context.Background(), []byte("test"), []byte("$hmac-sha256$ZTAzMWJhMWMyOTM4YjFkMjgzZjkxOWExZGY5YWM2NmMxOTJhN2RkNzQ0MzJkNWZkNGFkYTI5OTk0MWJhMTA5Zg==$MTIzNDU="))) + + //Wrong Key + assert.Error(t, hash.Compare(context.Background(), []byte("test"), []byte("$hmac-sha256$ZTAzMWJhMWMyOTM4YjFkMjgzZjkxOWExZGY5YWM2NmMxOTJhN2RkNzQ0MzJkNWZkNGFkYTI5OTk0MWJhMTA5Zg==$MTIzNA=="))) + //Different password + assert.Error(t, hash.Compare(context.Background(), []byte("ory"), []byte("$hmac-sha256$ZTAzMWJhMWMyOTM4YjFkMjgzZjkxOWExZGY5YWM2NmMxOTJhN2RkNzQ0MzJkNWZkNGFkYTI5OTk0MWJhMTA5Zg==$MTIzNDU="))) + + }) + + t.Run("hmac-sha384", func(t *testing.T) { + t.Parallel() + + //Valid + assert.Nil(t, hash.Compare(context.Background(), []byte("test"), []byte("$hmac-sha384$ZWEyMGM3NGE4Y2UzMTljNTdjZTlhZGQyYTZjNDE0MGQ4YjMwYWIwOWM4OTRiNWQ4MmZjODlhMzBhMmQzNGE5NmQ0NDY1NWRhYjQ2ZjhiYjBkNTRmYjk5YWZkZTA1MGY1$MTIzNDU="))) + assert.Nil(t, hash.CompareHMAC(context.Background(), []byte("test"), []byte("$hmac-sha384$ZWEyMGM3NGE4Y2UzMTljNTdjZTlhZGQyYTZjNDE0MGQ4YjMwYWIwOWM4OTRiNWQ4MmZjODlhMzBhMmQzNGE5NmQ0NDY1NWRhYjQ2ZjhiYjBkNTRmYjk5YWZkZTA1MGY1$MTIzNDU="))) + + //Wrong Key + assert.Error(t, hash.Compare(context.Background(), []byte("test"), []byte("$hmac-sha384$ZWEyMGM3NGE4Y2UzMTljNTdjZTlhZGQyYTZjNDE0MGQ4YjMwYWIwOWM4OTRiNWQ4MmZjODlhMzBhMmQzNGE5NmQ0NDY1NWRhYjQ2ZjhiYjBkNTRmYjk5YWZkZTA1MGY1$MTIzNA=="))) + //Different password + assert.Error(t, hash.Compare(context.Background(), []byte("ory"), []byte("$hmac-sha384$ZWEyMGM3NGE4Y2UzMTljNTdjZTlhZGQyYTZjNDE0MGQ4YjMwYWIwOWM4OTRiNWQ4MmZjODlhMzBhMmQzNGE5NmQ0NDY1NWRhYjQ2ZjhiYjBkNTRmYjk5YWZkZTA1MGY1$MTIzNDU="))) + + }) + + t.Run("hmac-sha512", func(t *testing.T) { + t.Parallel() + + //Valid + assert.Nil(t, hash.Compare(context.Background(), []byte("test"), []byte("$hmac-sha512$OTFmODY0ZTI1NmU0ZjVhYjhiMDViZGFmNGVmNGZmMGVlNTY4ODYwNWJhYTk4MTk2OTgyMzc3NzI1YTc4MzcxMTMzNzZmY2YxYTk5MGMxM2RiZDk2MGFmMmQ1YzRmODdlMGMwYTNkYjcyNjY0NjM4NGE4YzQ2MjNhZDZkN2UxZTE=$MTIzNDU="))) + assert.Nil(t, hash.CompareHMAC(context.Background(), []byte("test"), []byte("$hmac-sha512$OTFmODY0ZTI1NmU0ZjVhYjhiMDViZGFmNGVmNGZmMGVlNTY4ODYwNWJhYTk4MTk2OTgyMzc3NzI1YTc4MzcxMTMzNzZmY2YxYTk5MGMxM2RiZDk2MGFmMmQ1YzRmODdlMGMwYTNkYjcyNjY0NjM4NGE4YzQ2MjNhZDZkN2UxZTE=$MTIzNDU="))) + + //Wrong Key + assert.Error(t, hash.Compare(context.Background(), []byte("test"), []byte("$hmac-sha512$OTFmODY0ZTI1NmU0ZjVhYjhiMDViZGFmNGVmNGZmMGVlNTY4ODYwNWJhYTk4MTk2OTgyMzc3NzI1YTc4MzcxMTMzNzZmY2YxYTk5MGMxM2RiZDk2MGFmMmQ1YzRmODdlMGMwYTNkYjcyNjY0NjM4NGE4YzQ2MjNhZDZkN2UxZTE=$MTIzNA=="))) + //Different password + assert.Error(t, hash.Compare(context.Background(), []byte("ory"), []byte("$hmac-sha512$OTFmODY0ZTI1NmU0ZjVhYjhiMDViZGFmNGVmNGZmMGVlNTY4ODYwNWJhYTk4MTk2OTgyMzc3NzI1YTc4MzcxMTMzNzZmY2YxYTk5MGMxM2RiZDk2MGFmMmQ1YzRmODdlMGMwYTNkYjcyNjY0NjM4NGE4YzQ2MjNhZDZkN2UxZTE=$MTIzNDU="))) + + }) } diff --git a/identity/.snapshots/TestHandler-case=should_be_able_to_import_users-with_hashed_passwords-hash=hmac.json b/identity/.snapshots/TestHandler-case=should_be_able_to_import_users-with_hashed_passwords-hash=hmac.json new file mode 100644 index 000000000000..c60f5c5c0075 --- /dev/null +++ b/identity/.snapshots/TestHandler-case=should_be_able_to_import_users-with_hashed_passwords-hash=hmac.json @@ -0,0 +1,21 @@ +{ + "credentials": { + "password": { + "type": "password", + "identifiers": [ + "import-hash-9@ory.sh" + ], + "config": { + }, + "version": 0 + } + }, + "schema_id": "default", + "state": "active", + "traits": { + "email": "import-hash-9@ory.sh" + }, + "metadata_public": null, + "metadata_admin": null, + "organization_id": null +} diff --git a/identity/handler_test.go b/identity/handler_test.go index 5bba60754761..5da5bf076b68 100644 --- a/identity/handler_test.go +++ b/identity/handler_test.go @@ -280,6 +280,10 @@ func TestHandler(t *testing.T) { name: "SSHA512", hash: "{SSHA512}xPUl/px+1cG55rUH4rzcwxdOIPSB2TingLpiJJumN2xyDWN4Ix1WQG3ihnvHaWUE8MYNkvMi5rf0C9NYixHsE6Yh59M=", pass: "test123", + }, { + name: "hmac", + hash: "$hmac-sha256$YjhhZDA4YTNhNTQ3ZTM1ODI5YjgyMWI3NTM3MDMwMWRkOGM0YjA2YmRkNzc3MWY5YjU0MWE3NTkxNDA2ODcxOA==$MTIzNDU2", + pass: "123456", }, } { t.Run("hash="+tt.name, func(t *testing.T) {