Skip to content

Commit

Permalink
feat: allow importing hmac hashed passwords (#3544)
Browse files Browse the repository at this point in the history
The basic format is `$hmac-<hashfunction>$<base64 encoded hash>$<base64 encoded key>`:

```
# password = test; key=key; hash function=sha
$hmac-sha1$NjcxZjU0Y2UwYzU0MGY3OGZmZTFlMjZkY2Y5YzJhMDQ3YWVhNGZkYQ==$a2V5
```

See #2422
  • Loading branch information
tristankenney authored Oct 12, 2023
1 parent 13de64d commit 0a0e1f7
Show file tree
Hide file tree
Showing 4 changed files with 225 additions and 1 deletion.
80 changes: 79 additions & 1 deletion hash/hash_comparator.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand All @@ -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"

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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]?\$`)
Expand All @@ -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) }
Expand All @@ -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) ||
Expand All @@ -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
Expand Down Expand Up @@ -614,6 +645,53 @@ func decodeMD5Hash(encodedHash string) (pf, salt, hash []byte, err error) {
}
}

// decodeHMACHash decodes HMAC encoded password hash.
// format : $hmac-<hash function>$<hash>$<key>
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 {
Expand Down
121 changes: 121 additions & 0 deletions hash/hasher_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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=")))

})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"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,
"organization_id": null
}
4 changes: 4 additions & 0 deletions identity/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down

0 comments on commit 0a0e1f7

Please sign in to comment.