snapshot of https://github.com/ory/kratos.git (master, 2026-09-15; blobs > 131072B omitted)
Some checks failed
CI / Run tests and lints (push) Failing after 29s
CI / Build CLI docs (push) Has been skipped
CI / Run Playwright end-to-end tests (mysql) (push) Failing after 3s
CI / Run Playwright end-to-end tests (postgres) (push) Failing after 2s
CI / Run Playwright end-to-end tests (sqlite) (push) Failing after 3s
CodeQL / Analyze (go) (push) Failing after 4s
CodeQL / Analyze (javascript) (push) Failing after 3s
Docker Image Scanners / scanners (push) Failing after 4s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Run end-to-end tests (postgres) (push) Failing after 19s
CI / Run end-to-end tests (sqlite) (push) Failing after 2s
CI / Run Playwright end-to-end tests (cockroach) (push) Failing after 2s
CI / Draft newsletter (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped

This commit is contained in:
upstream-mirror
2026-09-15 21:19:12 +05:00
commit c60cc7ca75
8092 changed files with 394160 additions and 0 deletions

814
hash/hash_comparator.go Normal file
View File

@@ -0,0 +1,814 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package hash
import (
"bytes"
"context"
"crypto/aes"
"crypto/cipher"
"crypto/hmac"
"crypto/md5" //nolint:all // System compatibility for imported passwords
"crypto/pbkdf2"
"crypto/sha1" //nolint:all // System compatibility for imported passwords
"crypto/sha256"
"crypto/sha512"
"crypto/subtle"
"encoding/base64"
"encoding/hex"
"fmt"
"hash"
"regexp"
"strings"
"github.com/go-crypt/crypt"
"github.com/go-crypt/crypt/algorithm/md5crypt"
"github.com/go-crypt/crypt/algorithm/shacrypt"
"github.com/pkg/errors"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"golang.org/x/crypto/argon2"
"golang.org/x/crypto/bcrypt"
//nolint:staticcheck
//lint:ignore SA1019 compatibility for imported passwords
"golang.org/x/crypto/md4" //nolint:gosec // disable G115 G501 -- compatibility for imported passwords
"golang.org/x/crypto/scrypt"
"github.com/ory/kratos/driver/config"
"github.com/ory/x/otelx"
)
var ErrUnknownHashAlgorithm = errors.New("unknown hash algorithm")
func NewCryptDecoder() *crypt.Decoder {
decoder := crypt.NewDecoder()
// The register function only returns an error if the decoder is nil or the algorithm is already registered.
// This is never the case here, if it is, we did something horribly wrong.
if err := md5crypt.RegisterDecoderCommon(decoder); err != nil {
panic(err)
}
if err := shacrypt.RegisterDecoder(decoder); err != nil {
panic(err)
}
return decoder
}
var CryptDecoder = NewCryptDecoder()
type SupportedHasher struct {
Comparator func(ctx context.Context, password, hash []byte) error
Name string
Is func(hash []byte) bool
}
func AddSupportedHasher(s SupportedHasher) {
supportedHashers = append(supportedHashers, s)
}
var supportedHashers = []SupportedHasher{
{
Comparator: CompareMD5Crypt,
Name: "md5crypt",
Is: IsMD5CryptHash,
},
{
Comparator: CompareBcrypt,
Name: "bcrypt",
Is: IsBcryptHash,
},
{
Comparator: CompareSHA256Crypt,
Name: "sha256crypt",
Is: IsSHA256CryptHash,
},
{
Comparator: CompareSHA512Crypt,
Name: "sha512crypt",
Is: IsSHA512CryptHash,
},
{
Comparator: CompareArgon2id,
Name: "argon2id",
Is: IsArgon2idHash,
},
{
Comparator: CompareArgon2i,
Name: "argon2i",
Is: IsArgon2iHash,
},
{
Comparator: ComparePbkdf2,
Name: "pbkdf2",
Is: IsPbkdf2Hash,
},
{
Comparator: CompareScrypt,
Name: "scrypt",
Is: IsScryptHash,
},
{
Comparator: CompareSSHA,
Name: "ssha",
Is: IsSSHAHash,
},
{
Comparator: CompareSHA,
Name: "sha",
Is: IsSHAHash,
},
{
Comparator: CompareFirebaseScrypt,
Name: "firebasescrypt",
Is: IsFirebaseScryptHash,
},
{
Comparator: CompareMD5,
Name: "md5",
Is: IsMD5Hash,
},
{
Comparator: CompareHMAC,
Name: "hmac",
Is: IsHMACHash,
},
}
func Compare(ctx context.Context, password, hash []byte) (err error) {
ctx, span := otel.GetTracerProvider().Tracer(tracingComponent).Start(ctx, "hash.Compare")
defer otelx.End(span, &err)
for _, h := range supportedHashers {
if h.Is(hash) {
span.SetAttributes(attribute.String("hash.type", h.Name))
return h.Comparator(ctx, password, hash)
}
}
span.SetAttributes(attribute.String("hash.type", "unknown"))
return errors.WithStack(ErrUnknownHashAlgorithm)
}
func CompareMD5Crypt(_ context.Context, password, hash []byte) error {
// the password has successfully been validated (has prefix `$md5-crypt`),
// the decoder expect the module crypt identifier instead (`$1`), which means we need to replace the prefix
// before decoding
hash = bytes.TrimPrefix(hash, []byte("$md5-crypt"))
hash = append([]byte("$1"), hash...)
return compareCryptHelper(password, string(hash))
}
func CompareBcrypt(ctx context.Context, password, hash []byte) error {
if err := validateBcryptPasswordLength(password); err != nil {
return err
}
if err := validateBcryptHashCost(hash); err != nil {
return err
}
// ensure that the context is not canceled before doing the heavy lifting
if ctx.Err() != nil {
return ctx.Err()
}
err := bcrypt.CompareHashAndPassword(hash, password)
if err != nil {
if errors.Is(err, bcrypt.ErrMismatchedHashAndPassword) {
return errors.WithStack(ErrMismatchedHashAndPassword)
}
return err
}
return nil
}
func CompareSHA256Crypt(_ context.Context, password, hash []byte) error {
hash = bytes.TrimPrefix(hash, []byte("$sha256-crypt"))
hash = append([]byte("$5"), hash...)
return compareCryptHelper(password, string(hash))
}
func CompareSHA512Crypt(_ context.Context, password, hash []byte) error {
hash = bytes.TrimPrefix(hash, []byte("$sha512-crypt"))
hash = append([]byte("$6"), hash...)
return compareCryptHelper(password, string(hash))
}
func CompareArgon2id(ctx context.Context, password, hash []byte) error {
// Extract the parameters, salt and derived key from the encoded password
// hash.
p, salt, hash, err := decodeArgon2idHash(string(hash))
if err != nil {
return err
}
// ensure that the context is not canceled before doing the heavy lifting
if ctx.Err() != nil {
return ctx.Err()
}
// Derive the key from the other password using the same parameters.
//nolint:gosec // disable G115
otherHash := argon2.IDKey(password, salt, p.Iterations, uint32(p.Memory), p.Parallelism, p.KeyLength)
return comparePasswordHashConstantTime(hash, otherHash)
}
func CompareArgon2i(ctx context.Context, password, hash []byte) error {
// Extract the parameters, salt and derived key from the encoded password
// hash.
p, salt, hash, err := decodeArgon2idHash(string(hash))
if err != nil {
return err
}
// ensure that the context is not canceled before doing the heavy lifting
if ctx.Err() != nil {
return ctx.Err()
}
// Derive the key from the other password using the same parameters.
otherHash := argon2.Key(password, salt, p.Iterations, uint32(p.Memory), p.Parallelism, p.KeyLength) // #nosec G115 -- memory (KiB) would need to be 2^32 kibibyte to overflow uint32
return comparePasswordHashConstantTime(hash, otherHash)
}
func ComparePbkdf2(ctx context.Context, password, hash []byte) error {
// Extract the parameters, salt and derived key from the encoded password
// hash.
p, salt, hash, err := decodePbkdf2Hash(string(hash))
if err != nil {
return err
}
// ensure that the context is not canceled before doing the heavy lifting
if ctx.Err() != nil {
return ctx.Err()
}
// Derive the key from the other password using the same parameters.
otherHash, err := pbkdf2.Key(getPseudorandomFunctionForPbkdf2(p.Algorithm), string(password), salt, int(p.Iterations), int(p.KeyLength))
if err != nil {
return errors.WithStack(err)
}
return comparePasswordHashConstantTime(hash, otherHash)
}
func CompareScrypt(ctx context.Context, password, 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
}
// ensure that the context is not canceled before doing the heavy lifting
if ctx.Err() != nil {
return ctx.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)
}
return comparePasswordHashConstantTime(hash, otherHash)
}
func CompareSSHA(ctx context.Context, password, hash []byte) error {
hasher, salt, hash, err := decodeSSHAHash(string(hash))
if err != nil {
return err
}
raw := append(password[:], salt[:]...)
// ensure that the context is not canceled before doing the heavy lifting
if ctx.Err() != nil {
return ctx.Err()
}
return CompareSHAHelper(hasher, raw, hash)
}
func CompareSHA(ctx context.Context, password, hash []byte) error {
hasher, pf, salt, hash, err := decodeSHAHash(string(hash))
if err != nil {
return err
}
r := strings.NewReplacer("{SALT}", string(salt), "{PASSWORD}", string(password))
raw := []byte(r.Replace(string(pf)))
// ensure that the context is not canceled before doing the heavy lifting
if ctx.Err() != nil {
return ctx.Err()
}
return CompareSHAHelper(hasher, raw, hash)
}
func CompareFirebaseScrypt(ctx context.Context, password, hash []byte) error {
// Extract the parameters, salt and derived key from the encoded password
// hash.
p, salt, saltSeparator, hash, signerKey, err := decodeFirebaseScryptHash(string(hash))
if err != nil {
return err
}
// ensure that the context is not canceled before doing the heavy lifting
if ctx.Err() != nil {
return ctx.Err()
}
// Derive the key from the other password using the same parameters.
// FirebaseScript algorithm implementation from https://github.com/Aoang/firebase-scrypt
ck, err := scrypt.Key(password, append(salt, saltSeparator...), int(p.Cost), int(p.Block), int(p.Parrellization), 32)
if err != nil {
return errors.WithStack(err)
}
var block cipher.Block
if block, err = aes.NewCipher(ck); err != nil {
return errors.WithStack(err)
}
cipherText := make([]byte, aes.BlockSize+len(signerKey))
stream := cipher.NewCTR(block, cipherText[:aes.BlockSize])
stream.XORKeyStream(cipherText[aes.BlockSize:], signerKey)
otherHash := cipherText[aes.BlockSize:]
return comparePasswordHashConstantTime(hash, otherHash)
}
func CompareMD5(ctx context.Context, password, hash []byte) error {
// Extract the hash from the encoded password
pf, salt, hash, err := decodeMD5Hash(string(hash))
if err != nil {
return err
}
arg := password
if salt != nil {
r := strings.NewReplacer("{SALT}", string(salt), "{PASSWORD}", string(password))
arg = []byte(r.Replace(string(pf)))
}
// ensure that the context is not canceled before doing the heavy lifting
if ctx.Err() != nil {
return ctx.Err()
}
//#nosec G401 -- compatibility for imported passwords
otherHash := md5.Sum(arg)
return comparePasswordHashConstantTime(hash, otherHash[:])
}
func CompareHMAC(ctx context.Context, password, hash []byte) error {
// Extract the hash from the encoded password
hasher, hash, key, err := decodeHMACHash(string(hash))
if err != nil {
return err
}
// ensure that the context is not canceled before doing the heavy lifting
if ctx.Err() != nil {
return ctx.Err()
}
mac := hmac.New(hasher, key)
_, err = mac.Write(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]?\$`)
isSHA256CryptHash = regexp.MustCompile(`^\$sha256-crypt\$`)
isSHA512CryptHash = regexp.MustCompile(`^\$sha512-crypt\$`)
isArgon2idHash = regexp.MustCompile(`^\$argon2id\$`)
isArgon2iHash = regexp.MustCompile(`^\$argon2i\$`)
isPbkdf2Hash = regexp.MustCompile(`^\$pbkdf2-sha[0-9]{1,3}\$`)
isPbkdf2Sha256Hash = regexp.MustCompile(`^\$pbkdf2-sha256\$`)
isScryptHash = regexp.MustCompile(`^\$scrypt\$`)
isSSHAHash = regexp.MustCompile(`^{SSHA(256|512)?}.*`)
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) }
func IsBcryptHash(hash []byte) bool { return isBcryptHash.Match(hash) }
func IsSHA256CryptHash(hash []byte) bool { return isSHA256CryptHash.Match(hash) }
func IsSHA512CryptHash(hash []byte) bool { return isSHA512CryptHash.Match(hash) }
func IsArgon2idHash(hash []byte) bool { return isArgon2idHash.Match(hash) }
func IsArgon2iHash(hash []byte) bool { return isArgon2iHash.Match(hash) }
func IsPbkdf2Hash(hash []byte) bool { return isPbkdf2Hash.Match(hash) }
func IsPbkdf2Sha256Hash(hash []byte) bool { return isPbkdf2Sha256Hash.Match(hash) }
func IsScryptHash(hash []byte) bool { return isScryptHash.Match(hash) }
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 {
for _, h := range supportedHashers {
if h.Is(hash) {
return true
}
}
return false
}
func decodeArgon2idHash(encodedHash string) (p *config.Argon2, salt, hash []byte, err error) {
parts := strings.Split(encodedHash, "$")
if len(parts) != 6 {
return nil, nil, nil, ErrInvalidHash
}
var version int
_, err = fmt.Sscanf(parts[2], "v=%d", &version)
if err != nil {
return nil, nil, nil, err
}
if version != argon2.Version {
return nil, nil, nil, ErrIncompatibleVersion
}
p = new(config.Argon2)
_, err = fmt.Sscanf(parts[3], "m=%d,t=%d,p=%d", &p.Memory, &p.Iterations, &p.Parallelism)
if err != nil {
return nil, nil, nil, err
}
if err := validateArgon2Params(uint64(p.Memory), p.Iterations, p.Parallelism); err != nil {
return nil, nil, nil, err
}
salt, err = base64.RawStdEncoding.Strict().DecodeString(parts[4])
if err != nil {
return nil, nil, nil, err
}
p.SaltLength = uint32(len(salt)) // #nosec G115 -- salt would need to be 2^32 bytes long to overflow uint32
hash, err = base64.RawStdEncoding.Strict().DecodeString(parts[5])
if err != nil {
return nil, nil, nil, err
}
p.KeyLength = uint32(len(hash)) // #nosec G115 -- hash would need to be 2^32 bytes long to overflow uint32
return p, salt, hash, nil
}
// pbkdf2Params carries the cost parameters decoded from a stored PBKDF2 hash.
// The configurable hasher is hash.Pbkdf2; this type only serves comparison of
// stored or imported hashes.
type pbkdf2Params struct {
Algorithm string
Iterations uint32
SaltLength uint32
KeyLength uint32
}
// decodePbkdf2Hash decodes PBKDF2 encoded password hash.
// format: $pbkdf2-<digest>$i=<iterations>,l=<length>$<salt>$<hash>
func decodePbkdf2Hash(encodedHash string) (p *pbkdf2Params, salt, hash []byte, err error) {
parts := strings.Split(encodedHash, "$")
if len(parts) != 5 {
return nil, nil, nil, ErrInvalidHash
}
p = new(pbkdf2Params)
digestParts := strings.SplitN(parts[1], "-", 2)
if len(digestParts) != 2 {
return nil, nil, nil, ErrInvalidHash
}
p.Algorithm = digestParts[1]
_, err = fmt.Sscanf(parts[2], "i=%d,l=%d", &p.Iterations, &p.KeyLength)
if err != nil {
return nil, nil, nil, err
}
if err := validatePbkdf2Params(p.Iterations); err != nil {
return nil, nil, nil, err
}
salt, err = base64.RawStdEncoding.Strict().DecodeString(parts[3])
if err != nil {
return nil, nil, nil, err
}
p.SaltLength = uint32(len(salt)) // #nosec G115 -- salt would need to be 2^32 bytes long to overflow uint32
hash, err = base64.RawStdEncoding.Strict().DecodeString(parts[4])
if err != nil {
return nil, nil, nil, err
}
p.KeyLength = uint32(len(hash)) // #nosec G115 -- hash would need to be 2^32 bytes long to overflow uint32
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
}
if err := validateScryptParams(p.Cost, p.Block, p.Parrellization); 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)) // #nosec G115 -- salt would need to be 2^32 bytes long to overflow uint32
hash, err = base64.StdEncoding.Strict().DecodeString(parts[4])
if err != nil {
return nil, nil, nil, err
}
p.KeyLength = uint32(len(hash)) // #nosec G115 -- hash would need to be 2^32 bytes long to overflow uint32
return p, salt, hash, nil
}
// decodeSHAHash decodes SHA[1|256|512] encoded password hash in custom PHC format.
// format: $sha1$pf=<salting-format>$<salt>$<hash>
func decodeSHAHash(encodedHash string) (hasher string, pf, salt, hash []byte, err error) {
parts := strings.Split(encodedHash, "$")
if len(parts) != 5 {
return "", nil, nil, nil, ErrInvalidHash
}
hasher = parts[1]
_, err = fmt.Sscanf(parts[2], "pf=%s", &pf)
if err != nil {
return "", nil, nil, nil, err
}
pf, err = base64.StdEncoding.Strict().DecodeString(string(pf))
if err != nil {
return "", nil, nil, nil, err
}
salt, err = base64.StdEncoding.Strict().DecodeString(parts[3])
if err != nil {
return "", nil, nil, nil, err
}
hash, err = base64.StdEncoding.Strict().DecodeString(parts[4])
if err != nil {
return "", nil, nil, nil, err
}
return hasher, pf, salt, hash, nil
}
// CompareSHAHelper compares the raw password with the hash using the given hasher.
func CompareSHAHelper(hasher string, raw []byte, hash []byte) error {
var sha []byte
switch hasher {
case "sha1":
sum := sha1.Sum(raw) //#nosec G401 -- compatibility for imported passwords
sha = sum[:]
case "sha256":
sum := sha256.Sum256(raw)
sha = sum[:]
case "sha512":
sum := sha512.Sum512(raw)
sha = sum[:]
default:
return errors.WithStack(ErrMismatchedHashAndPassword)
}
encodedHash := []byte(base64.StdEncoding.EncodeToString(hash))
newEncodedHash := []byte(base64.StdEncoding.EncodeToString(sha))
return comparePasswordHashConstantTime(encodedHash, newEncodedHash)
}
func compareCryptHelper(password []byte, hash string) error {
digest, err := CryptDecoder.Decode(hash)
if err != nil {
return err
}
if digest.MatchBytes(password) {
return nil
}
return errors.WithStack(ErrMismatchedHashAndPassword)
}
var regexSSHA = regexp.MustCompile(`\{([^}]*)\}`)
// decodeSSHAHash decodes SSHA[1|256|512] encoded password hash in usual {SSHA...} format.
func decodeSSHAHash(encodedHash string) (hasher string, salt, hash []byte, err error) {
match := regexSSHA.FindStringSubmatch(string(encodedHash))
var index_of_salt_begin int
var index_of_hash_begin int
switch match[1] {
case "SSHA":
hasher = "sha1"
index_of_hash_begin = 6
index_of_salt_begin = 20
case "SSHA256":
hasher = "sha256"
index_of_hash_begin = 9
index_of_salt_begin = 32
case "SSHA512":
hasher = "sha512"
index_of_hash_begin = 9
index_of_salt_begin = 64
default:
return "", nil, nil, ErrInvalidHash
}
decoded, err := base64.StdEncoding.DecodeString(string(encodedHash[index_of_hash_begin:]))
if err != nil {
return "", nil, nil, ErrInvalidHash
}
if len(decoded) < index_of_salt_begin+1 {
return "", nil, nil, ErrInvalidHash
}
salt = decoded[index_of_salt_begin:]
hash = decoded[:index_of_salt_begin]
return hasher, salt, hash, nil
}
// decodeFirebaseScryptHash decodes Firebase Scrypt encoded password hash.
// format: $firescrypt$ln=<mem_cost>,r=<rounds>,p=<parallelization>$<salt>$<hash>$<salt_separator>$<signer_key>
func decodeFirebaseScryptHash(encodedHash string) (p *Scrypt, salt, saltSeparator, hash, signerKey []byte, err error) {
parts := strings.Split(encodedHash, "$")
if len(parts) != 7 {
return nil, nil, 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, nil, nil, err
}
if err := validateFirebaseScryptParams(p.Cost, p.Block, p.Parrellization); err != nil {
return nil, nil, nil, nil, nil, err
}
// convert from firebase config "mem_cost" to
// scrypt CPU/memory cost parameter, which must be a power of two greater than 1.
p.Cost = 1 << p.Cost
salt, err = base64.StdEncoding.Strict().DecodeString(parts[3])
if err != nil {
return nil, nil, nil, nil, nil, err
}
p.SaltLength = uint32(len(salt)) // #nosec G115 -- salt would need to be 2^32 bytes long to overflow uint32
hash, err = base64.StdEncoding.Strict().DecodeString(parts[4])
if err != nil {
return nil, nil, nil, nil, nil, err
}
// Are all firebase script hashes of length 32?
p.KeyLength = 32
saltSeparator, err = base64.StdEncoding.Strict().DecodeString(parts[5])
if err != nil {
return nil, nil, nil, nil, nil, err
}
signerKey, err = base64.StdEncoding.Strict().DecodeString(parts[6])
if err != nil {
return nil, nil, nil, nil, nil, err
}
return p, salt, saltSeparator, hash, signerKey, nil
}
// decodeMD5Hash decodes MD5 encoded password hash.
// format without salt: $md5$<hash>
// format with salt $md5$pf=<salting-format>$<salt>$<hash>
func decodeMD5Hash(encodedHash string) (pf, salt, hash []byte, err error) {
parts := strings.Split(encodedHash, "$")
switch len(parts) {
case 3:
hash, err := base64.StdEncoding.Strict().DecodeString(parts[2])
return nil, nil, hash, err
case 5:
_, err = fmt.Sscanf(parts[2], "pf=%s", &pf)
if err != nil {
return nil, nil, nil, err
}
pf, err := base64.StdEncoding.Strict().DecodeString(string(pf))
if err != nil {
return nil, nil, nil, err
}
salt, err = base64.StdEncoding.Strict().DecodeString(parts[3])
if err != nil {
return nil, nil, nil, err
}
hash, err = base64.StdEncoding.Strict().DecodeString(parts[4])
if err != nil {
return nil, nil, nil, err
}
return pf, salt, hash, nil
default:
return nil, nil, nil, ErrInvalidHash
}
}
// 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 {
return nil
}
return errors.WithStack(ErrMismatchedHashAndPassword)
}

162
hash/hash_limits.go Normal file
View File

@@ -0,0 +1,162 @@
// Copyright © 2025 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package hash
import (
"github.com/pkg/errors"
"golang.org/x/crypto/bcrypt"
)
// ErrHashParametersOutOfBounds is returned when an imported or stored hash
// declares cost parameters that exceed the bounds enforced by this package.
// The bounds exist to prevent attacker-controlled hash parameters from causing
// memory exhaustion (process OOM) or unbounded CPU consumption when the
// password comparator is invoked.
var ErrHashParametersOutOfBounds = errors.New("hash parameters exceed allowed bounds")
// Bounds for cost parameters embedded in imported password hashes. Each value
// is chosen to comfortably accommodate strong real-world configurations while
// bounding worst-case memory and CPU consumption when the comparator runs at
// login time.
const (
// maxScryptLogN bounds the firebase-scrypt ln parameter.
// firebase-scrypt computes N = 1 << ln. Memory is 128 * N * r bytes.
// Firebase's strongest documented profile is ln=14 (~16 MiB at r=8); 17
// leaves headroom for legacy configs while bounding allocation at
// ~128 MiB at r=8.
maxScryptLogN uint32 = 17
// maxScryptN bounds plain scrypt's N directly. OWASP's Password Storage
// Cheat Sheet recommends N=2^17 (131072) as the minimum for password
// hashing; no widely-deployed password-hash system documents a larger
// value. Higher N appears only in file-encryption tooling (tarsnap, age)
// where multi-second cost is acceptable. The Kratos default and RFC 7914's
// example both use N=16384 (2^14). Combined with the r=8 ceiling, this
// caps memory at 128 MiB per compare.
maxScryptN uint32 = 1 << 17
// maxScryptR bounds scrypt's r (block size). RFC 7914 specifies r=8
// verbatim; all five OWASP recommended scrypt configurations use r=8.
// No documented production password hash uses r > 8. With N=2^17 this
// caps memory at 128 MiB per compare.
maxScryptR uint32 = 8
// maxScryptP bounds scrypt's p (parallelization). p=1 is the standard
// default; OWASP's highest documented config uses p=10. Go's scrypt
// implementation runs p iterations serially, so p multiplies CPU
// linearly without any parallelism gain.
maxScryptP uint32 = 10
// maxArgon2MemoryKiB bounds argon2 memory at 1 GiB. Kratos default is
// 128 MiB; OWASP recommends 64 MiB. 1 GiB matches the existing
// Argon2DefaultDedicatedMemory ceiling in driver/config.
maxArgon2MemoryKiB uint32 = 1 << 20
// maxArgon2Iterations bounds argon2 t. RFC 9106 recommends t∈{1,3};
// OWASP configurations use up to t=5; libsodium SENSITIVE preset uses
// t=4. Bitwarden's user-configurable hard cap is 10, the highest
// documented production value.
maxArgon2Iterations uint32 = 10
// maxArgon2Parallelism bounds argon2 p. Default is 2*NumCPU; 16 covers
// up to 8-core hosts.
maxArgon2Parallelism uint8 = 16
// maxPbkdf2Iterations bounds PBKDF2 i. OWASP 2023 recommends 600k
// (SHA-256) / 210k (SHA-512). 10M ≈ 10× the strongest published
// recommendation and bounds CPU to a few seconds on modern hardware.
maxPbkdf2Iterations uint32 = 10_000_000
// maxBcryptCost bounds bcrypt cost. The format spec allows 4–31, but
// cost grows exponentially: cost 12 (Kratos, PHP, Django default) is
// ~250 ms; cost 14 (high-security guidance) is ~1 s; cost 15 (practical
// max for interactive use) is ~2 s; cost 17 is ~8 s. No mainstream
// platform defaults above cost 12.
maxBcryptCost = 15
)
func validateFirebaseScryptParams(logN, r, p uint32) error {
if logN > maxScryptLogN {
return errors.Wrapf(ErrHashParametersOutOfBounds, "firescrypt ln=%d exceeds max %d", logN, maxScryptLogN)
}
if r == 0 || r > maxScryptR {
return errors.Wrapf(ErrHashParametersOutOfBounds, "firescrypt r=%d not in [1, %d]", r, maxScryptR)
}
if p == 0 || p > maxScryptP {
return errors.Wrapf(ErrHashParametersOutOfBounds, "firescrypt p=%d not in [1, %d]", p, maxScryptP)
}
return nil
}
func validateScryptParams(n, r, p uint32) error {
if n == 0 || n > maxScryptN {
return errors.Wrapf(ErrHashParametersOutOfBounds, "scrypt N=%d not in [1, %d]", n, maxScryptN)
}
if r == 0 || r > maxScryptR {
return errors.Wrapf(ErrHashParametersOutOfBounds, "scrypt r=%d not in [1, %d]", r, maxScryptR)
}
if p == 0 || p > maxScryptP {
return errors.Wrapf(ErrHashParametersOutOfBounds, "scrypt p=%d not in [1, %d]", p, maxScryptP)
}
return nil
}
func validateArgon2Params(memoryKiB uint64, iterations uint32, parallelism uint8) error {
if memoryKiB == 0 || memoryKiB > uint64(maxArgon2MemoryKiB) {
return errors.Wrapf(ErrHashParametersOutOfBounds, "argon2 m=%d KiB not in [1, %d]", memoryKiB, maxArgon2MemoryKiB)
}
if iterations == 0 || iterations > maxArgon2Iterations {
return errors.Wrapf(ErrHashParametersOutOfBounds, "argon2 t=%d not in [1, %d]", iterations, maxArgon2Iterations)
}
if parallelism == 0 || parallelism > maxArgon2Parallelism {
return errors.Wrapf(ErrHashParametersOutOfBounds, "argon2 p=%d not in [1, %d]", parallelism, maxArgon2Parallelism)
}
return nil
}
func validatePbkdf2Params(iterations uint32) error {
if iterations == 0 || iterations > maxPbkdf2Iterations {
return errors.Wrapf(ErrHashParametersOutOfBounds, "pbkdf2 i=%d not in [1, %d]", iterations, maxPbkdf2Iterations)
}
return nil
}
func validateBcryptHashCost(hashed []byte) error {
cost, err := bcrypt.Cost(hashed)
if err != nil {
return err
}
if cost > maxBcryptCost {
return errors.Wrapf(ErrHashParametersOutOfBounds, "bcrypt cost=%d exceeds max %d", cost, maxBcryptCost)
}
return nil
}
// ValidateImportedHash performs cost-parameter bounds checking on a hash
// before it is persisted via the admin identity import API. It is the
// import-time counterpart to the bounds checks in the compare* paths and
// exists so we fail fast on malicious imports rather than persisting a hash
// that would crash the process at every login attempt.
//
// Hashers without attacker-controlled cost parameters (md5-crypt, sha-crypt,
// the static SHA/MD5/HMAC families, SSHA) pass through unchanged.
func ValidateImportedHash(hashed []byte) error {
switch {
case IsBcryptHash(hashed):
return validateBcryptHashCost(hashed)
case IsArgon2idHash(hashed), IsArgon2iHash(hashed):
_, _, _, err := decodeArgon2idHash(string(hashed))
return err
case IsPbkdf2Hash(hashed):
_, _, _, err := decodePbkdf2Hash(string(hashed))
return err
case IsScryptHash(hashed):
_, _, _, err := decodeScryptHash(string(hashed))
return err
case IsFirebaseScryptHash(hashed):
_, _, _, _, _, err := decodeFirebaseScryptHash(string(hashed))
return err
}
return nil
}

150
hash/hash_limits_test.go Normal file
View File

@@ -0,0 +1,150 @@
// Copyright © 2025 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package hash_test
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/ory/kratos/hash"
)
// TestCompareRejectsExtremeHashParameters reproduces the bug-bounty-reported
// DoS class: an attacker-controlled hash with extreme cost parameters causes
// the comparator to allocate gigabytes (or run for hours) and crash or hang
// the Kratos process at login time. Each case here would, without the bounds
// in hash_limits.go, drive the underlying primitive into pathological
// behavior. The comparator must error early instead.
func TestCompareRejectsExtremeHashParameters(t *testing.T) {
t.Parallel()
ctx := t.Context()
cases := []struct {
name string
hash string
}{
{
// The originally reported attack: ln=31 → N=2^31 → ~256 GiB at r=8.
name: "firescrypt ln=31 (reported)",
hash: "$firescrypt$ln=31,r=8,p=1$c2FsdHNhbHRzYWx0$aGFzaA==$Bw==$a2V5a2V5",
},
{
name: "firescrypt ln=18",
hash: "$firescrypt$ln=18,r=8,p=1$c2FsdHNhbHRzYWx0$aGFzaA==$Bw==$a2V5a2V5",
},
{
name: "firescrypt r=1024",
hash: "$firescrypt$ln=14,r=1024,p=1$c2FsdHNhbHRzYWx0$aGFzaA==$Bw==$a2V5a2V5",
},
{
name: "firescrypt p=1024",
hash: "$firescrypt$ln=14,r=8,p=1024$c2FsdHNhbHRzYWx0$aGFzaA==$Bw==$a2V5a2V5",
},
{
// Plain scrypt: ln=2097153 (one above 2^21) — far beyond cap.
name: "scrypt N too large",
hash: "$scrypt$ln=2097153,r=8,p=1$c2FsdHNhbHRzYWx0$aGFzaA==",
},
{
name: "scrypt r=1024",
hash: "$scrypt$ln=16384,r=1024,p=1$c2FsdHNhbHRzYWx0$aGFzaA==",
},
{
// Argon2: 8 GiB requested.
name: "argon2id m=8GiB",
hash: "$argon2id$v=19$m=8388608,t=2,p=4$cm94YnRVOW5jZzFzcVE4bQ$MNzk5BtR2vUhrp6qQEjRNw",
},
{
name: "argon2id t=1000",
hash: "$argon2id$v=19$m=65536,t=1000,p=4$cm94YnRVOW5jZzFzcVE4bQ$MNzk5BtR2vUhrp6qQEjRNw",
},
{
name: "argon2i m=8GiB",
hash: "$argon2i$v=19$m=8388608,t=3,p=4$kk51rW/vxIVCYn+EG4kTSg$NyT88uraJ6im6dyha/M5jhXvpqlEdlS/9fEm7ScMb8c",
},
{
// PBKDF2: 1B iterations.
name: "pbkdf2 i=1_000_000_000",
hash: "$pbkdf2-sha256$i=1000000000,l=32$1jP+5Zxpxgtee/iPxGgOz0RfE9/KJuDElP1ley4VxXc$QJxzfvdbHYBpydCbHoFg3GJEqMFULwskiuqiJctoYpI",
},
{
// Bcrypt cost=20 → ~64s per compare. Format-valid but resource
// exhaustion risk.
name: "bcrypt cost=20",
hash: "$2a$20$abcdefghijklmnopqrstuugn/uitWMnDoOdJaSWqW1pjEXZ8eOSCqK",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
err := hash.Compare(ctx, []byte("any-password"), []byte(tc.hash))
require.Error(t, err, "expected bounds-exceeded error; without it the process would OOM or hang")
require.ErrorIs(t, err, hash.ErrHashParametersOutOfBounds)
})
}
}
// TestValidateImportedHashRejectsExtremeParameters covers the import-time
// counterpart: malicious hashes must be rejected at the admin import API
// boundary, before they are ever persisted, so that a single poisoned row
// cannot crash-loop the service on every login attempt.
func TestValidateImportedHashRejectsExtremeParameters(t *testing.T) {
t.Parallel()
cases := []struct {
name string
hash string
}{
{name: "firescrypt ln=31", hash: "$firescrypt$ln=31,r=8,p=1$c2FsdHNhbHRzYWx0$aGFzaA==$Bw==$a2V5a2V5"},
{name: "scrypt N too large", hash: "$scrypt$ln=2097153,r=8,p=1$c2FsdHNhbHRzYWx0$aGFzaA=="},
{name: "argon2id m=8GiB", hash: "$argon2id$v=19$m=8388608,t=2,p=4$cm94YnRVOW5jZzFzcVE4bQ$MNzk5BtR2vUhrp6qQEjRNw"},
{name: "argon2i t=1000", hash: "$argon2i$v=19$m=65536,t=1000,p=4$kk51rW/vxIVCYn+EG4kTSg$NyT88uraJ6im6dyha/M5jhXvpqlEdlS/9fEm7ScMb8c"},
{name: "pbkdf2 i=1B", hash: "$pbkdf2-sha256$i=1000000000,l=32$1jP+5Zxpxgtee/iPxGgOz0RfE9/KJuDElP1ley4VxXc$QJxzfvdbHYBpydCbHoFg3GJEqMFULwskiuqiJctoYpI"},
{name: "bcrypt cost=20", hash: "$2a$20$abcdefghijklmnopqrstuugn/uitWMnDoOdJaSWqW1pjEXZ8eOSCqK"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
err := hash.ValidateImportedHash([]byte(tc.hash))
require.Error(t, err)
require.ErrorIs(t, err, hash.ErrHashParametersOutOfBounds)
})
}
}
// TestValidateImportedHashAcceptsRealisticHashes guards against bounds being
// set so tightly that legitimate, real-world hashes are rejected. The values
// here mirror the existing valid-hash test cases in TestCompare.
func TestValidateImportedHashAcceptsRealisticHashes(t *testing.T) {
t.Parallel()
cases := []struct {
name string
hash string
}{
{name: "bcrypt cost=12", hash: "$2a$12$o6hx.Wog/wvFSkT/Bp/6DOxCtLRTDj7lm9on9suF/WaCGNVHbkfL6"},
{name: "bcrypt cost=15", hash: "$2a$15$GRvRO2nrpYTEuPQX6AieaOlZ4.7nMGsXpt.QWMev1zrP86JNspZbO"},
{name: "argon2id m=32 t=2 p=4", hash: "$argon2id$v=19$m=32,t=2,p=4$cm94YnRVOW5jZzFzcVE4bQ$MNzk5BtR2vUhrp6qQEjRNw"},
{name: "argon2i m=65536 t=3 p=4", hash: "$argon2i$v=19$m=65536,t=3,p=4$kk51rW/vxIVCYn+EG4kTSg$NyT88uraJ6im6dyha/M5jhXvpqlEdlS/9fEm7ScMb8c"},
{name: "pbkdf2-sha256 i=100000", hash: "$pbkdf2-sha256$i=100000,l=32$1jP+5Zxpxgtee/iPxGgOz0RfE9/KJuDElP1ley4VxXc$QJxzfvdbHYBpydCbHoFg3GJEqMFULwskiuqiJctoYpI"},
{name: "pbkdf2-sha512 i=100000", hash: "$pbkdf2-sha512$i=100000,l=32$bdHBpn7OWOivJMVJypy2UqR0UnaD5prQXRZevj/05YU$+wArTfv1a+bNGO1iZrmEdVjhA+lL11wF4/IxpgYfPwc"},
{name: "scrypt ln=16384 r=8 p=1", hash: "$scrypt$ln=16384,r=8,p=1$2npRo7P03Mt8keSoMbyD/tKFWyUzjiQf2svUaNDSrhA=$MiCzNcIplSMqSBrm4HckjYqYhaVPPjTARTzwB1cVNYE="},
{name: "firescrypt ln=14", hash: "$firescrypt$ln=14,r=8,p=1$sPtDhWcd1MfdAw==$xbSou7FOl6mChCyzpCPIQ7tku7nsQMTFtyOZSXXd7tjBa4NtimOx7v42Gv2SfzPQu1oxM2/k4SsbOu73wlKe1A==$Bw==$YE0dO4bwD4JnJafh6lZZfkp1MtKzuKAXQcDCJNJNyeCHairWHKENOkbh3dzwaCdizzOspwr/FITUVlnOAwPKyw=="},
// Hashes without parametrized cost (no allocation/CPU vector) — must
// pass through without error.
{name: "md5", hash: "$md5$CY9rzUYh03PK3k6DJie09g=="},
{name: "sha256-crypt", hash: "$sha256-crypt$rounds=535000$05R.9KB6UC2kLI3w$Q/zslzx./JjkAVPTwp6th7nW5l7JU91Gte/UmIh.U78"},
{name: "ssha", hash: "{SSHA}JFZFs0oHzxbMwkSJmYVeI8MnTDy/276a"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
require.NoError(t, hash.ValidateImportedHash([]byte(tc.hash)))
})
}
}

23
hash/hasher.go Normal file
View File

@@ -0,0 +1,23 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package hash
import (
"context"
)
// Hasher provides methods for generating and comparing password hashes.
type Hasher interface {
// Generate returns a hash derived from the password or an error if the hash method failed.
Generate(ctx context.Context, password []byte) ([]byte, error)
// Understands returns whether the given hash can be understood by this hasher.
Understands(hash []byte) bool
}
type HashProvider interface {
Hasher(ctx context.Context) Hasher
}
const tracingComponent = "github.com/ory/kratos/hash"

91
hash/hasher_argon2.go Normal file
View File

@@ -0,0 +1,91 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package hash
import (
"bytes"
"context"
"crypto/rand"
"encoding/base64"
"fmt"
"github.com/inhies/go-bytesize"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/trace"
"github.com/pkg/errors"
"golang.org/x/crypto/argon2"
"github.com/ory/kratos/driver/config"
"github.com/ory/x/otelx"
)
var (
ErrInvalidHash = errors.New("the encoded hash is not in the correct format")
ErrIncompatibleVersion = errors.New("incompatible version of argon2")
ErrMismatchedHashAndPassword = errors.New("passwords do not match")
)
type Argon2 struct {
c Argon2Configuration
}
type Argon2Configuration interface {
config.Provider
}
func NewHasherArgon2(c Argon2Configuration) *Argon2 {
return &Argon2{c: c}
}
func toKB(mem bytesize.ByteSize) uint32 {
//nolint:gosec // disable G115
return uint32(mem / bytesize.KB)
}
func (h *Argon2) Generate(ctx context.Context, password []byte) (_ []byte, err error) {
conf := h.c.Config().HasherArgon2(ctx)
_, span := otel.GetTracerProvider().Tracer(tracingComponent).Start(ctx, "hash.Generate", trace.WithAttributes(
attribute.String("hash.type", "argon2id"),
attribute.String("hash.config", fmt.Sprintf("%#v", conf)),
))
defer otelx.End(span, &err)
salt := make([]byte, conf.SaltLength)
if _, err := rand.Read(salt); err != nil {
return nil, err
}
// ensure that the context is not canceled before doing the heavy lifting
if ctx.Err() != nil {
return nil, ctx.Err()
}
// Pass the plaintext password, salt and parameters to the argon2.IDKey
// function. This will generate a hash of the password using the Argon2id
// variant.
hash := argon2.IDKey(password, salt, conf.Iterations, toKB(conf.Memory), conf.Parallelism, conf.KeyLength)
var b bytes.Buffer
if _, err := fmt.Fprintf(
&b,
"$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s",
argon2.Version, toKB(conf.Memory), conf.Iterations, conf.Parallelism,
base64.RawStdEncoding.EncodeToString(salt),
base64.RawStdEncoding.EncodeToString(hash),
); err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return nil, errors.WithStack(err)
}
return b.Bytes(), nil
}
func (h *Argon2) Understands(hash []byte) bool {
return IsArgon2idHash(hash)
}

77
hash/hasher_bcrypt.go Normal file
View File

@@ -0,0 +1,77 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package hash
import (
"context"
"fmt"
"github.com/ory/kratos/text"
"github.com/ory/x/otelx"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
"github.com/ory/kratos/schema"
"golang.org/x/crypto/bcrypt"
"github.com/ory/kratos/driver/config"
)
type Bcrypt struct {
c BcryptConfiguration
}
type BcryptConfiguration interface {
config.Provider
}
func NewHasherBcrypt(c BcryptConfiguration) *Bcrypt {
return &Bcrypt{c: c}
}
func (h *Bcrypt) Generate(ctx context.Context, password []byte) (_ []byte, err error) {
conf := h.c.Config().HasherBcrypt(ctx)
_, span := otel.GetTracerProvider().Tracer(tracingComponent).Start(ctx, "hash.Generate", trace.WithAttributes(
attribute.String("hash.type", "bcrypt"),
attribute.String("hash.config", fmt.Sprintf("%#v", conf)),
))
defer otelx.End(span, &err)
if err := validateBcryptPasswordLength(password); err != nil {
return nil, err
}
// ensure that the context is not canceled before doing the heavy lifting
if ctx.Err() != nil {
return nil, ctx.Err()
}
hash, err := bcrypt.GenerateFromPassword(password, int(conf.Cost))
if err != nil {
return nil, err
}
return hash, nil
}
func validateBcryptPasswordLength(password []byte) error {
// Bcrypt truncates the password to the first 72 bytes, following the OpenBSD implementation,
// so if password is longer than 72 bytes, function returns an error
// See https://en.wikipedia.org/wiki/Bcrypt#User_input
if len(password) > 72 {
return schema.NewPasswordPolicyViolationError(
"#/password",
text.NewErrorValidationPasswordMaxLength(72, len(password)),
)
}
return nil
}
func (h *Bcrypt) Understands(hash []byte) bool {
return IsBcryptHash(hash)
}

95
hash/hasher_pbkdf2.go Normal file
View File

@@ -0,0 +1,95 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package hash
import (
"bytes"
"context"
"crypto/pbkdf2"
"crypto/rand"
"crypto/sha1" //#nosec G505 -- compatibility for imported passwords
"crypto/sha256"
"crypto/sha3"
"crypto/sha512"
"encoding/base64"
"fmt"
"hash"
"github.com/pkg/errors"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
"github.com/ory/x/otelx"
)
type Pbkdf2 struct {
Algorithm string
Iterations uint32
SaltLength uint32
KeyLength uint32
}
func (h *Pbkdf2) Generate(ctx context.Context, password []byte) (_ []byte, err error) {
_, span := otel.GetTracerProvider().Tracer(tracingComponent).Start(ctx, "hash.Generate", trace.WithAttributes(
attribute.String("hash.type", "pbkdf2"),
attribute.String("hash.config", fmt.Sprintf("%#v", h)),
))
defer otelx.End(span, &err)
salt := make([]byte, h.SaltLength)
if _, err := rand.Read(salt); err != nil {
return nil, errors.WithStack(err)
}
// ensure that the context is not canceled before doing the heavy lifting
if ctx.Err() != nil {
return nil, ctx.Err()
}
key, err := pbkdf2.Key(getPseudorandomFunctionForPbkdf2(h.Algorithm), string(password), salt, int(h.Iterations), int(h.KeyLength))
if err != nil {
return nil, errors.WithStack(err)
}
var b bytes.Buffer
if _, err := fmt.Fprintf(
&b,
"$pbkdf2-%s$i=%d,l=%d$%s$%s",
h.Algorithm,
h.Iterations,
h.KeyLength,
base64.RawStdEncoding.EncodeToString(salt),
base64.RawStdEncoding.EncodeToString(key),
); err != nil {
return nil, errors.WithStack(err)
}
return b.Bytes(), nil
}
func (h *Pbkdf2) Understands(hash []byte) bool {
return IsPbkdf2Hash(hash)
}
func getPseudorandomFunctionForPbkdf2(alg string) func() hash.Hash {
switch alg {
case "sha1":
return sha1.New
case "sha224":
// Kept for backwards compatibility: this historically mapped to
// SHA3-224, not SHA2-224. Changing it would break verification of
// previously imported hashes.
return func() hash.Hash { return sha3.New224() }
case "sha256":
return sha256.New
case "sha384":
// Kept for backwards compatibility: SHA3-384, see above.
return func() hash.Hash { return sha3.New384() }
case "sha512":
return sha512.New
default:
return sha256.New
}
}

12
hash/hasher_scrypt.go Normal file
View File

@@ -0,0 +1,12 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package hash
type Scrypt struct {
Cost uint32
Block uint32
Parrellization uint32
SaltLength uint32
KeyLength uint32
}

556
hash/hasher_test.go Normal file
View File

@@ -0,0 +1,556 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package hash_test
import (
"crypto/rand"
"encoding/base64"
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/ory/kratos/hash"
"github.com/ory/kratos/pkg"
)
func mkpw(t *testing.T, length int) []byte {
pw := make([]byte, length)
_, err := rand.Read(pw)
require.NoError(t, err)
return pw
}
func TestArgonHasher(t *testing.T) {
t.Parallel()
ctx := t.Context()
_, reg := pkg.NewVeryFastRegistryWithoutDB(t)
h := hash.NewHasherArgon2(reg)
for _, pwLength := range []int{
8,
16,
32,
64,
128,
} {
pwLength := pwLength
t.Run(fmt.Sprintf("length=%dchars", pwLength), func(t *testing.T) {
t.Parallel()
pw := mkpw(t, pwLength)
hs, err := h.Generate(ctx, pw)
require.NoError(t, err)
assert.NotEqual(t, pw, hs)
t.Logf("hash: %s", hs)
require.NoError(t, hash.CompareArgon2id(ctx, pw, hs))
mod := make([]byte, len(pw))
copy(mod, pw)
mod[len(pw)-1] = ^pw[len(pw)-1]
require.Error(t, hash.CompareArgon2id(ctx, mod, hs))
})
}
}
func TestBcryptHasherGeneratesErrorWhenPasswordIsLong(t *testing.T) {
t.Parallel()
ctx := t.Context()
_, reg := pkg.NewVeryFastRegistryWithoutDB(t)
hasher := hash.NewHasherBcrypt(reg)
password := mkpw(t, 73)
res, err := hasher.Generate(ctx, password)
assert.Error(t, err, "password is too long")
assert.Nil(t, res)
}
func TestBcryptHasherGeneratesHash(t *testing.T) {
t.Parallel()
ctx := t.Context()
_, reg := pkg.NewVeryFastRegistryWithoutDB(t)
hasher := hash.NewHasherBcrypt(reg)
for _, pwLength := range []int{
8,
16,
32,
64,
72,
} {
pwLength := pwLength
t.Run(fmt.Sprintf("length=%dchars", pwLength), func(t *testing.T) {
t.Parallel()
pw := mkpw(t, pwLength)
hs, err := hasher.Generate(ctx, pw)
assert.Nil(t, err)
assert.True(t, hasher.Understands(hs))
// Valid format: $2a$12$[22 character salt][31 character hash]
assert.Equal(t, 60, len(string(hs)), "invalid bcrypt hash length")
assert.Equal(t, "$2a$12$", string(hs)[:7], "invalid bcrypt identifier")
})
}
}
func TestComparatorBcryptFailsWhenPasswordIsTooLong(t *testing.T) {
t.Parallel()
ctx := t.Context()
password := mkpw(t, 73)
err := hash.CompareBcrypt(ctx, password, []byte("hash"))
assert.Error(t, err, "password is too long")
}
func TestComparatorBcryptSuccess(t *testing.T) {
t.Parallel()
ctx := t.Context()
_, reg := pkg.NewVeryFastRegistryWithoutDB(t)
hasher := hash.NewHasherBcrypt(reg)
for _, pwLength := range []int{
8,
16,
32,
64,
72,
} {
pwLength := pwLength
t.Run(fmt.Sprintf("length=%dchars", pwLength), func(t *testing.T) {
t.Parallel()
pw := mkpw(t, pwLength)
hs, err := hasher.Generate(ctx, pw)
assert.Nil(t, err)
assert.True(t, hasher.Understands(hs))
err = hash.CompareBcrypt(ctx, pw, hs)
assert.Nil(t, err, "hash validation fails")
})
}
}
func TestComparatorBcryptFail(t *testing.T) {
t.Parallel()
ctx := t.Context()
for _, pwLength := range []int{
8,
16,
32,
64,
72,
} {
pwLength := pwLength
t.Run(fmt.Sprintf("length=%dchars", pwLength), func(t *testing.T) {
t.Parallel()
pw := mkpw(t, pwLength)
mod := make([]byte, len(pw))
copy(mod, pw)
mod[len(pw)-1] = ^pw[len(pw)-1]
err := hash.CompareBcrypt(ctx, pw, mod)
assert.Error(t, err)
})
}
}
func TestPbkdf2Hasher(t *testing.T) {
t.Parallel()
ctx := t.Context()
for _, pwLength := range []int{
8,
16,
32,
64,
128,
} {
pwLength := pwLength
t.Run(fmt.Sprintf("length=%dchars", pwLength), func(t *testing.T) {
t.Parallel()
for _, algorithm := range []string{
"sha1",
"sha224",
"sha256",
"sha384",
"sha512",
} {
algorithm := algorithm
t.Run(fmt.Sprintf("algorithm=%s", algorithm), func(t *testing.T) {
t.Parallel()
hasher := &hash.Pbkdf2{
Algorithm: algorithm,
Iterations: 100_000,
SaltLength: 32,
KeyLength: 32,
}
pw := mkpw(t, pwLength)
t.Logf("%d", pwLength)
hs, err := hasher.Generate(ctx, pw)
require.NoError(t, err)
assert.NotEqual(t, pw, hs)
t.Logf("hash: %s", hs)
require.NoError(t, hash.ComparePbkdf2(ctx, pw, hs))
assert.True(t, hasher.Understands(hs))
mod := make([]byte, len(pw))
copy(mod, pw)
mod[len(pw)-1] = ^pw[len(pw)-1]
require.Error(t, hash.ComparePbkdf2(ctx, mod, hs))
})
}
})
}
}
func TestCompare(t *testing.T) {
t.Parallel()
ctx := t.Context()
t.Run("unknown", func(t *testing.T) {
t.Parallel()
assert.Error(t, hash.Compare(ctx, []byte("test"), []byte("$unknown$12$o6hx.Wog/wvFSkT/Bp/6DOxCtLRTDj7lm9on9suF/WaCGNVHbkfL6")))
})
t.Run("bcrypt", func(t *testing.T) {
t.Parallel()
assert.Nil(t, hash.Compare(ctx, []byte("test"), []byte("$2a$12$o6hx.Wog/wvFSkT/Bp/6DOxCtLRTDj7lm9on9suF/WaCGNVHbkfL6")))
assert.Nil(t, hash.CompareBcrypt(ctx, []byte("test"), []byte("$2a$12$o6hx.Wog/wvFSkT/Bp/6DOxCtLRTDj7lm9on9suF/WaCGNVHbkfL6")))
assert.Error(t, hash.Compare(ctx, []byte("test"), []byte("$2a$12$o6hx.Wog/wvFSkT/Bp/6DOxCtLRTDj7lm9on9suF/WaCGNVHbkfL7")))
assert.Nil(t, hash.Compare(ctx, []byte("test"), []byte("$2a$15$GRvRO2nrpYTEuPQX6AieaOlZ4.7nMGsXpt.QWMev1zrP86JNspZbO")))
assert.Nil(t, hash.CompareBcrypt(ctx, []byte("test"), []byte("$2a$15$GRvRO2nrpYTEuPQX6AieaOlZ4.7nMGsXpt.QWMev1zrP86JNspZbO")))
assert.Error(t, hash.Compare(ctx, []byte("test"), []byte("$2a$15$GRvRO2nrpYTEuPQX6AieaOlZ4.7nMGsXpt.QWMev1zrP86JNspZb1")))
})
t.Run("Argon2", func(t *testing.T) {
t.Parallel()
assert.Nil(t, hash.Compare(ctx, []byte("test"), []byte("$argon2id$v=19$m=32,t=2,p=4$cm94YnRVOW5jZzFzcVE4bQ$MNzk5BtR2vUhrp6qQEjRNw")))
assert.Nil(t, hash.CompareArgon2id(ctx, []byte("test"), []byte("$argon2id$v=19$m=32,t=2,p=4$cm94YnRVOW5jZzFzcVE4bQ$MNzk5BtR2vUhrp6qQEjRNw")))
assert.Error(t, hash.Compare(ctx, []byte("test"), []byte("$argon2id$v=19$m=32,t=2,p=4$cm94YnRVOW5jZzFzcVE4bQ$MNzk5BtR2vUhrp6qQEjRN2")))
assert.Nil(t, hash.Compare(ctx, []byte("test"), []byte("$argon2i$v=19$m=65536,t=3,p=4$kk51rW/vxIVCYn+EG4kTSg$NyT88uraJ6im6dyha/M5jhXvpqlEdlS/9fEm7ScMb8c")))
assert.Nil(t, hash.CompareArgon2i(ctx, []byte("test"), []byte("$argon2i$v=19$m=65536,t=3,p=4$kk51rW/vxIVCYn+EG4kTSg$NyT88uraJ6im6dyha/M5jhXvpqlEdlS/9fEm7ScMb8c")))
assert.Error(t, hash.Compare(ctx, []byte("test"), []byte("$argon2i$v=19$m=65536,t=3,p=4$pZ+27D6B0bCi0DwSmANF1w$4RNCUu4Uyu7eTIvzIdSuKz+I9idJlX/ykn6J10/W0EU")))
assert.Nil(t, hash.Compare(ctx, []byte("test"), []byte("$argon2id$v=19$m=32,t=5,p=4$cm94YnRVOW5jZzFzcVE4bQ$fBxypOL0nP/zdPE71JtAV71i487LbX3fJI5PoTN6Lp4")))
assert.Nil(t, hash.CompareArgon2id(ctx, []byte("test"), []byte("$argon2id$v=19$m=32,t=5,p=4$cm94YnRVOW5jZzFzcVE4bQ$fBxypOL0nP/zdPE71JtAV71i487LbX3fJI5PoTN6Lp4")))
assert.Error(t, hash.Compare(ctx, []byte("test"), []byte("$argon2id$v=19$m=32,t=5,p=4$cm94YnRVOW5jZzFzcVE4bQ$fBxypOL0nP/zdPE71JtAV71i487LbX3fJI5PoTN6Lp5")))
})
t.Run("pbkdf2", func(t *testing.T) {
t.Parallel()
assert.Nil(t, hash.Compare(ctx, []byte("test"), []byte("$pbkdf2-sha256$i=100000,l=32$1jP+5Zxpxgtee/iPxGgOz0RfE9/KJuDElP1ley4VxXc$QJxzfvdbHYBpydCbHoFg3GJEqMFULwskiuqiJctoYpI")))
assert.Nil(t, hash.ComparePbkdf2(ctx, []byte("test"), []byte("$pbkdf2-sha256$i=100000,l=32$1jP+5Zxpxgtee/iPxGgOz0RfE9/KJuDElP1ley4VxXc$QJxzfvdbHYBpydCbHoFg3GJEqMFULwskiuqiJctoYpI")))
assert.Error(t, hash.Compare(ctx, []byte("test"), []byte("$pbkdf2-sha256$i=100000,l=32$1jP+5Zxpxgtee/iPxGgOz0RfE9/KJuDElP1ley4VxXc$QJxzfvdbHYBpydCbHoFg3GJEqMFULwskiuqiJctoYpp")))
assert.Nil(t, hash.Compare(ctx, []byte("test"), []byte("$pbkdf2-sha512$i=100000,l=32$bdHBpn7OWOivJMVJypy2UqR0UnaD5prQXRZevj/05YU$+wArTfv1a+bNGO1iZrmEdVjhA+lL11wF4/IxpgYfPwc")))
assert.Nil(t, hash.ComparePbkdf2(ctx, []byte("test"), []byte("$pbkdf2-sha512$i=100000,l=32$bdHBpn7OWOivJMVJypy2UqR0UnaD5prQXRZevj/05YU$+wArTfv1a+bNGO1iZrmEdVjhA+lL11wF4/IxpgYfPwc")))
assert.Error(t, hash.Compare(ctx, []byte("test"), []byte("$pbkdf2-sha512$i=100000,l=32$bdHBpn7OWOivJMVJypy2UqR0UnaD5prQXRZevj/05YU$+wArTfv1a+bNGO1iZrmEdVjhA+lL11wF4/IxpgYfPww")))
// The sha224 and sha384 variants historically map to SHA3-224 and
// SHA3-384 (not SHA2), see getPseudorandomFunctionForPbkdf2. These
// fixtures were generated with that mapping and pin it.
assert.Nil(t, hash.Compare(ctx, []byte("test"), []byte("$pbkdf2-sha224$i=100000,l=32$A2nzbFd+wwzPSb+TqkF5Pw$9M4GM2yOYw/fZvsvQbF/0Ec8JO2u9O1MYRTfrDimX6o")))
assert.Nil(t, hash.ComparePbkdf2(ctx, []byte("test"), []byte("$pbkdf2-sha224$i=100000,l=32$A2nzbFd+wwzPSb+TqkF5Pw$9M4GM2yOYw/fZvsvQbF/0Ec8JO2u9O1MYRTfrDimX6o")))
assert.Error(t, hash.Compare(ctx, []byte("wrong"), []byte("$pbkdf2-sha224$i=100000,l=32$A2nzbFd+wwzPSb+TqkF5Pw$9M4GM2yOYw/fZvsvQbF/0Ec8JO2u9O1MYRTfrDimX6o")))
assert.Nil(t, hash.Compare(ctx, []byte("test"), []byte("$pbkdf2-sha384$i=100000,l=32$xDInuz7Y/DPzZUnCaQXJmg$7ICaEa6jEqxbbCBbv4YzrBf39KYNAtc3Bj0QWKeB3Ck")))
assert.Nil(t, hash.ComparePbkdf2(ctx, []byte("test"), []byte("$pbkdf2-sha384$i=100000,l=32$xDInuz7Y/DPzZUnCaQXJmg$7ICaEa6jEqxbbCBbv4YzrBf39KYNAtc3Bj0QWKeB3Ck")))
assert.Error(t, hash.Compare(ctx, []byte("wrong"), []byte("$pbkdf2-sha384$i=100000,l=32$xDInuz7Y/DPzZUnCaQXJmg$7ICaEa6jEqxbbCBbv4YzrBf39KYNAtc3Bj0QWKeB3Ck")))
assert.Error(t, hash.Compare(ctx, []byte("test"), []byte("$pbkdf2-sha256$1jP+5Zxpxgtee/iPxGgOz0RfE9/KJuDElP1ley4VxXc$QJxzfvdbHYBpydCbHoFg3GJEqMFULwskiuqiJctoYpI")))
assert.Error(t, hash.Compare(ctx, []byte("test"), []byte("$pbkdf2-sha256$aaaa$1jP+5Zxpxgtee/iPxGgOz0RfE9/KJuDElP1ley4VxXc$QJxzfvdbHYBpydCbHoFg3GJEqMFULwskiuqiJctoYpI")))
assert.Error(t, hash.Compare(ctx, []byte("test"), []byte("$pbkdf2-sha256$i=100000,l=32$1jP+5Zxpxgtee/iPxGgOz0RfE9/KJuDElP1ley4VxXcc$QJxzfvdbHYBpydCbHoFg3GJEqMFULwskiuqiJctoYpI")))
assert.Error(t, hash.Compare(ctx, []byte("test"), []byte("$pbkdf2-sha256$i=100000,l=32$1jP+5Zxpxgtee/iPxGgOz0RfE9/KJuDElP1ley4VxXc$QJxzfvdbHYBpydCbHoFg3GJEqMFULwskiuqiJctoYpII")))
assert.Error(t, hash.Compare(ctx, []byte("test"), []byte("$pbkdf2-sha512$I=100000,l=32$bdHBpn7OWOivJMVJypy2UqR0UnaD5prQXRZevj/05YU$+wArTfv1a+bNGO1iZrmEdVjhA+lL11wF4/IxpgYfPwc")))
})
t.Run("scrypt", func(t *testing.T) {
t.Parallel()
assert.Nil(t, hash.Compare(ctx, []byte("test"), []byte("$scrypt$ln=16384,r=8,p=1$2npRo7P03Mt8keSoMbyD/tKFWyUzjiQf2svUaNDSrhA=$MiCzNcIplSMqSBrm4HckjYqYhaVPPjTARTzwB1cVNYE=")))
assert.Nil(t, hash.CompareScrypt(ctx, []byte("test"), []byte("$scrypt$ln=16384,r=8,p=1$2npRo7P03Mt8keSoMbyD/tKFWyUzjiQf2svUaNDSrhA=$MiCzNcIplSMqSBrm4HckjYqYhaVPPjTARTzwB1cVNYE=")))
assert.Error(t, hash.Compare(ctx, []byte("test"), []byte("$scrypt$ln=16384,r=8,p=1$2npRo7P03Mt8keSoMbyD/tKFWyUzjiQf2svUaNDSrhA=$MiCzNcIplSMqSBrm4HckjYqYhaVPPjTARTzwB1cVNYF=")))
assert.Error(t, hash.Compare(ctx, []byte("test"), []byte("$scrypt$2npRo7P03Mt8keSoMbyD/tKFWyUzjiQf2svUaNDSrhA=$MiCzNcIplSMqSBrm4HckjYqYhaVPPjTARTzwB1cVNYE=")))
assert.Error(t, hash.Compare(ctx, []byte("test"), []byte("$scrypt$ln=16384,r=8,p=1$(2npRo7P03Mt8keSoMbyD/tKFWyUzjiQf2svUaNDSrhA=$MiCzNcIplSMqSBrm4HckjYqYhaVPPjTARTzwB1cVNYE=")))
assert.Error(t, hash.Compare(ctx, []byte("test"), []byte("$scrypt$ln=16384,r=8,p=1$(2npRo7P03Mt8keSoMbyD/tKFWyUzjiQf2svUaNDSrhA=$(MiCzNcIplSMqSBrm4HckjYqYhaVPPjTARTzwB1cVNYE=")))
assert.Error(t, hash.Compare(ctx, []byte("test"), []byte("$scrypt$ln=16385,r=8,p=1$2npRo7P03Mt8keSoMbyD/tKFWyUzjiQf2svUaNDSrhA=$MiCzNcIplSMqSBrm4HckjYqYhaVPPjTARTzwB1cVNYE=")))
assert.Error(t, hash.Compare(ctx, []byte("tesu"), []byte("$scrypt$ln=16384,r=8,p=1$2npRo7P03Mt8keSoMbyD/tKFWyUzjiQf2svUaNDSrhA=$MiCzNcIplSMqSBrm4HckjYqYhaVPPjTARTzwB1cVNYE=")))
assert.Error(t, hash.Compare(ctx, []byte("tesu"), []byte("$scrypt$ln=abc,r=8,p=1$2npRo7P03Mt8keSoMbyD/tKFWyUzjiQf2svUaNDSrhA=$MiCzNcIplSMqSBrm4HckjYqYhaVPPjTARTzwB1cVNYE=")))
})
t.Run("firescrypt", func(t *testing.T) {
t.Parallel()
assert.Nil(t, hash.Compare(ctx, []byte("8x4WjoDbSxJZdR"), []byte("$firescrypt$ln=14,r=8,p=1$sPtDhWcd1MfdAw==$xbSou7FOl6mChCyzpCPIQ7tku7nsQMTFtyOZSXXd7tjBa4NtimOx7v42Gv2SfzPQu1oxM2/k4SsbOu73wlKe1A==$Bw==$YE0dO4bwD4JnJafh6lZZfkp1MtKzuKAXQcDCJNJNyeCHairWHKENOkbh3dzwaCdizzOspwr/FITUVlnOAwPKyw==")))
assert.Nil(t, hash.CompareFirebaseScrypt(ctx, []byte("8x4WjoDbSxJZdR"), []byte("$firescrypt$ln=14,r=8,p=1$sPtDhWcd1MfdAw==$xbSou7FOl6mChCyzpCPIQ7tku7nsQMTFtyOZSXXd7tjBa4NtimOx7v42Gv2SfzPQu1oxM2/k4SsbOu73wlKe1A==$Bw==$YE0dO4bwD4JnJafh6lZZfkp1MtKzuKAXQcDCJNJNyeCHairWHKENOkbh3dzwaCdizzOspwr/FITUVlnOAwPKyw==")))
assert.Error(t, hash.Compare(ctx, []byte("8x4WjoDbSxJZdR"), []byte("$firescrypt$ln=14,r=8,p=1$sPtDhWcd1MfdAw==$xbSou7FOl6mChCyzpCPIQ7tku7nsQMTFtyOZSXXd7tjBa4NtimOx7v42Gv2SfzPQu1oxM2/k4SsbOu73wlKe1A==$Bw==$YE0dO4bwD4JnJafh6lZZfkp1MtKzuKAXQcDCJNJNyeCHairWHKENOkbh3dzwaCdizzOspwr/FITUVlnOAwPKyc==")))
})
t.Run("SSHA", func(t *testing.T) {
t.Parallel()
assert.Nil(t, hash.Compare(ctx, []byte("test123"), []byte("{SSHA}JFZFs0oHzxbMwkSJmYVeI8MnTDy/276a")))
assert.Nil(t, hash.CompareSSHA(ctx, []byte("test123"), []byte("{SSHA}JFZFs0oHzxbMwkSJmYVeI8MnTDy/276a")))
assert.Error(t, hash.CompareSSHA(ctx, []byte("badtest"), []byte("{SSHA}JFZFs0oHzxbMwkSJmYVeI8MnTDy/276a")))
assert.Error(t, hash.Compare(ctx, []byte(""), []byte("{SSHA}tooshort")))
assert.Nil(t, hash.Compare(ctx, []byte("test123"), []byte("{SSHA256}czO44OTV17PcF1cRxWrLZLy9xHd7CWyVYplr1rOhuMlx/7IK")))
assert.Nil(t, hash.CompareSSHA(ctx, []byte("test123"), []byte("{SSHA256}czO44OTV17PcF1cRxWrLZLy9xHd7CWyVYplr1rOhuMlx/7IK")))
assert.Error(t, hash.CompareSSHA(ctx, []byte("badtest"), []byte("{SSHA256}czO44OTV17PcF1cRxWrLZLy9xHd7CWyVYplr1rOhuMlx/7IK")))
assert.Nil(t, hash.Compare(ctx, []byte("test123"), []byte("{SSHA512}xPUl/px+1cG55rUH4rzcwxdOIPSB2TingLpiJJumN2xyDWN4Ix1WQG3ihnvHaWUE8MYNkvMi5rf0C9NYixHsE6Yh59M=")))
assert.Nil(t, hash.CompareSSHA(ctx, []byte("test123"), []byte("{SSHA512}xPUl/px+1cG55rUH4rzcwxdOIPSB2TingLpiJJumN2xyDWN4Ix1WQG3ihnvHaWUE8MYNkvMi5rf0C9NYixHsE6Yh59M=")))
assert.Error(t, hash.CompareSSHA(ctx, []byte("badtest"), []byte("{SSHA512}xPUl/px+1cG55rUH4rzcwxdOIPSB2TingLpiJJumN2xyDWN4Ix1WQG3ihnvHaWUE8MYNkvMi5rf0C9NYixHsE6Yh59M=")))
assert.Error(t, hash.CompareSSHA(ctx, []byte("test123"), []byte("{SSHAnotExistent}xPUl/px+1cG55rUH4rzcwxdOIPSB2TingLpiJJumN2xyDWN4Ix1WQG3ihnvHaWUE8MYNkvMi5rf0C9NYixHsE6Yh59M=")))
})
t.Run("sha1", func(t *testing.T) {
t.Parallel()
//pf: {SALT}{PASSWORD}
assert.Nil(t, hash.Compare(ctx, []byte("test"), []byte("$sha1$pf=e1NBTFR9e1BBU1NXT1JEfQ==$NW9wbWtnejAzcg==$2qU2SGWP8viTM1md3FiI3+rjWXQ=")))
assert.Error(t, hash.Compare(ctx, []byte("wrongpass"), []byte("$sha1$pf=e1NBTFR9e1BBU1NXT1JEfQ==$NW9wbWtnejAzcg==$2qU2SGWP8viTM1md3FiI3+rjWXQ=")))
assert.Error(t, hash.Compare(ctx, []byte("tset"), []byte("$sha1$pf=e1NBTFR9e1BBU1NXT1JEfQ==$NW9wbWtnejAzcg==$2qU2SGWP8viTM1md3FiI3+rjWXQ=")))
// wrong salt
assert.Error(t, hash.Compare(ctx, []byte("test"), []byte("$sha1$pf=e1NBTFR9e1BBU1NXT1JEfQ==$cDJvb3ZrZGJ6cQ==$2qU2SGWP8viTM1md3FiI3+rjWXQ=")))
// salt not encoded
assert.Error(t, hash.Compare(ctx, []byte("test"), []byte("$sha1$pf=e1NBTFR9e1BBU1NXT1JEfQ==$5opmkgz03r$2qU2SGWP8viTM1md3FiI3+rjWXQ=")))
assert.Nil(t, hash.Compare(ctx, []byte("BwS^514g^cv@Z"), []byte("$sha1$pf=e1NBTFR9e1BBU1NXT1JEfQ==$NW9wbWtnejAzcg==$99h9net4BXl7qdTRaiGUobLROxM=")))
// no format string
assert.Error(t, hash.Compare(ctx, []byte("test"), []byte("$sha1$pf=$NW9wbWtnejAzcg==$2qU2SGWP8viTM1md3FiI3+rjWXQ=")))
assert.Error(t, hash.Compare(ctx, []byte("test"), []byte("$sha1$$NW9wbWtnejAzcg==$2qU2SGWP8viTM1md3FiI3+rjWXQ=")))
// wrong number of parameters
assert.Error(t, hash.Compare(ctx, []byte("test"), []byte("$sha1$NW9wbWtnejAzcg==$2qU2SGWP8viTM1md3FiI3+rjWXQ=")))
// pf: ??staticPrefix??{SALT}{PASSWORD}
assert.Nil(t, hash.Compare(ctx, []byte("test"), []byte("$sha1$pf=Pz9zdGF0aWNQcmVmaXg/P3tTQUxUfXtQQVNTV09SRH0=$NW9wbWtnejAzcg==$SAAxMUn7jxckQXkBmsVF0nHwqso=")))
// pf: {PASSWORD}%%{SALT}
assert.Nil(t, hash.Compare(ctx, []byte("test"), []byte("$sha1$pf=e1BBU1NXT1JEfSUle1NBTFR9$NW9wbWtnejAzcg==$YX0AW8/MW5ojUlnzTaR43ucHCog=")))
// pf: ${PASSWORD}${SALT}$
assert.Nil(t, hash.Compare(ctx, []byte("test"), []byte("$sha1$pf=JHtQQVNTV09SRH0ke1NBTFR9JA==$NW9wbWtnejAzcg==$iE5n1yjX3oAdxRHwZ4u57I4LpQo=")))
})
t.Run("sha256", func(t *testing.T) {
t.Parallel()
//pf: {SALT}{PASSWORD}
assert.Nil(t, hash.Compare(ctx, []byte("test"), []byte("$sha256$pf=e1NBTFR9e1BBU1NXT1JEfQ==$NW9wbWtnejAzcg==$0gfRVLCvtBCk20udLDEY5vNhujWx7RGjwRIS1ebMsLY=")))
assert.Nil(t, hash.CompareSHA(ctx, []byte("test"), []byte("$sha256$pf=e1NBTFR9e1BBU1NXT1JEfQ==$NW9wbWtnejAzcg==$0gfRVLCvtBCk20udLDEY5vNhujWx7RGjwRIS1ebMsLY=")))
assert.Error(t, hash.Compare(ctx, []byte("wrongpass"), []byte("$sha256$pf=e1NBTFR9e1BBU1NXT1JEfQ==$NW9wbWtnejAzcg==$0gfRVLCvtBCk20udLDEY5vNhujWx7RGjwRIS1ebMsLY=")))
//pf: {SALT}$${PASSWORD}
assert.Nil(t, hash.Compare(ctx, []byte("test"), []byte("$sha256$pf=e1NBTFR9JCR7UEFTU1dPUkR9$NW9wbWtnejAzcg==$HokCOi9OtiZaZRvnkgemV3B4UUHpI7kA8zq/EZWH2NY=")))
})
t.Run("sha512", func(t *testing.T) {
t.Parallel()
//pf: {SALT}{PASSWORD}
assert.Nil(t, hash.Compare(ctx, []byte("test"), []byte("$sha512$pf=e1NBTFR9e1BBU1NXT1JEfQ==$NW9wbWtnejAzcg==$6ctpVuApMNp0CgBXcdHw/GC562eFEFGr4gpgANX8ZYsX+j5B19IkdmOY2Fytsz3QUwSWdGcUjbqwgJGTH0UYvw==")))
assert.Nil(t, hash.CompareSHA(ctx, []byte("test"), []byte("$sha512$pf=e1NBTFR9e1BBU1NXT1JEfQ==$NW9wbWtnejAzcg==$6ctpVuApMNp0CgBXcdHw/GC562eFEFGr4gpgANX8ZYsX+j5B19IkdmOY2Fytsz3QUwSWdGcUjbqwgJGTH0UYvw==")))
assert.Error(t, hash.Compare(ctx, []byte("wrongpass"), []byte("$sha512$pf=e1NBTFR9e1BBU1NXT1JEfQ==$NW9wbWtnejAzcg==$6ctpVuApMNp0CgBXcdHw/GC562eFEFGr4gpgANX8ZYsX+j5B19IkdmOY2Fytsz3QUwSWdGcUjbqwgJGTH0UYvw==")))
//pf: {SALT}$${PASSWORD}
assert.Nil(t, hash.Compare(ctx, []byte("test"), []byte("$sha512$pf=e1NBTFR9JCR7UEFTU1dPUkR9$NW9wbWtnejAzcg==$1F9BPW8UtdJkZ9Dhlf+D4X4dJ9xfuH8y04EfuCP2k4aGPPq/aWxU9/xe3LydHmYW1/K3zu3NFO9ETVrZettz3w==")))
})
t.Run("sha unknown", func(t *testing.T) {
t.Parallel()
assert.Error(t, hash.Compare(ctx, []byte("test"), []byte("$shaNotExistent$pf=e1NBTFR9e1BBU1NXT1JEfQ==$NW9wbWtnejAzcg==$6ctpVuApMNp0CgBXcdHw/GC562eFEFGr4gpgANX8ZYsX+j5B19IkdmOY2Fytsz3QUwSWdGcUjbqwgJGTH0UYvw==")))
})
t.Run("md5", func(t *testing.T) {
t.Parallel()
assert.Nil(t, hash.Compare(ctx, []byte("test"), []byte("$md5$CY9rzUYh03PK3k6DJie09g==")))
assert.Nil(t, hash.CompareMD5(ctx, []byte("test"), []byte("$md5$CY9rzUYh03PK3k6DJie09g==")))
assert.Error(t, hash.Compare(ctx, []byte("test"), []byte("$md5$WhBei51A4TKXgNYuoiZdig==")))
assert.Error(t, hash.Compare(ctx, []byte("test"), []byte("$md5$Dk/E5LQLsx4yt8QbUbvpdg==")))
assert.Nil(t, hash.Compare(ctx, []byte("ory"), []byte("$md5$ptoWyof5SobW+pbZu2QXoQ==")))
assert.Nil(t, hash.CompareMD5(ctx, []byte("ory"), []byte("$md5$ptoWyof5SobW+pbZu2QXoQ==")))
assert.Error(t, hash.Compare(ctx, []byte("ory"), []byte("$md5$4skj967KRHFsnPFoL5dMMw==")))
assert.ErrorIs(t, hash.Compare(ctx, []byte("ory"), []byte("$md5$$")), hash.ErrInvalidHash)
assert.Error(t, hash.Compare(ctx, []byte("ory"), []byte("$md5$$$")))
assert.Error(t, hash.Compare(ctx, []byte("ory"), []byte("$md5$pf=$$")))
assert.ErrorIs(t, hash.Compare(ctx, []byte("ory"), []byte("$md5$pf=MTIz$Z$")), base64.CorruptInputError(0))
assert.ErrorIs(t, hash.Compare(ctx, []byte("ory"), []byte("$md5$pf=MTIz$Z$")), base64.CorruptInputError(0))
assert.ErrorIs(t, hash.Compare(ctx, []byte("ory"), []byte("$md5$pf=MTIz$MTIz$Z")), base64.CorruptInputError(0))
assert.Nil(t, hash.Compare(ctx, []byte("test"), []byte("$md5$pf=e1NBTFR9e1BBU1NXT1JEfQ==$MTIz$q+RdKCgc+ipCAcm5ChQwlQ=="))) // pf={SALT}{PASSWORD} salt=123
assert.Error(t, hash.Compare(ctx, []byte("test"), []byte("$md5$pf=e1NBTFR9e1BBU1NXT1JEfQ==$MTIz$hh8ZTp1hGPPZQqcr4+UXSQ==")))
assert.Nil(t, hash.CompareMD5(ctx, []byte("test"), []byte("$md5$pf=e1NBTFR9JCR7UEFTU1dPUkR9$MTIzNA==$ud392Z8rfZ+Ou7ZFXYLKbA=="))) // pf={SALT}$${PASSWORD} salt=1234
assert.Error(t, hash.CompareMD5(ctx, []byte("test1"), []byte("$md5$pf=e1NBTFR9JCR7UEFTU1dPUkR9$MTIzNA==$ud392Z8rfZ+Ou7ZFXYLKbA==")))
assert.Nil(t, hash.CompareMD5(ctx, []byte("ory"), []byte("$md5$pf=e1BBU1NXT1JEfXtTQUxUfSQ/$MTIzNDU2Nzg5$8PhwWanVRnpJAFK4NUjR0w=="))) // pf={PASSWORD}{SALT}$? salt=123456789
assert.Error(t, hash.CompareMD5(ctx, []byte("ory1"), []byte("$md5$pf=e1BBU1NXT1JEfXtTQUxUfSQ/$MTIzNDU2Nzg5$8PhwWanVRnpJAFK4NUjR0w==")))
})
t.Run("md5-crypt", func(t *testing.T) {
t.Parallel()
assert.Nil(t, hash.Compare(ctx, []byte("test"), []byte("$md5-crypt$TVEiiKNb$SN6/pUaRQS/E8Jh46As2C/")))
assert.Nil(t, hash.CompareMD5Crypt(ctx, []byte("test"), []byte("$md5-crypt$TVEiiKNb$SN6/pUaRQS/E8Jh46As2C/")))
assert.Nil(t, hash.Compare(ctx, []byte("test"), []byte("$md5-crypt$$whuMjZj.HMFoaTaZRRtkO0")))
assert.Error(t, hash.Compare(ctx, []byte("test"), []byte("$md5-crypt$xWMlm2eL$GGTOpgZu4p2k6ORprAu3b.")))
assert.Nil(t, hash.Compare(ctx, []byte("ory"), []byte("$md5-crypt$xWMlm2eL$GGTOpgZu4p2k6ORprAu3b.")))
assert.Nil(t, hash.CompareMD5Crypt(ctx, []byte("ory"), []byte("$md5-crypt$xWMlm2eL$GGTOpgZu4p2k6ORprAu3b.")))
assert.Error(t, hash.Compare(ctx, []byte("ory"), []byte("$md5-crypt$E7zjruqF$RTglYR1CzBHwwiTk9nVzx1")))
assert.ErrorIs(t, hash.Compare(ctx, []byte("ory"), []byte("$md5-crypt$$")), hash.ErrMismatchedHashAndPassword)
assert.Error(t, hash.Compare(ctx, []byte("ory"), []byte("$md5-crypt$$$")))
// per crypt(5), `md5crypt` can be run without a salt, but the salt section must still be present
assert.Error(t, hash.Compare(ctx, []byte("test"), []byte("$md5-crypt$whuMjZj.HMFoaTaZRRtkO0")), "md5crypt decode error: provided encoded hash has an invalid format")
})
t.Run("sha256-crypt", func(t *testing.T) {
t.Parallel()
assert.Nil(t, hash.Compare(ctx, []byte("test"), []byte("$sha256-crypt$rounds=535000$05R.9KB6UC2kLI3w$Q/zslzx./JjkAVPTwp6th7nW5l7JU91Gte/UmIh.U78")))
assert.Nil(t, hash.CompareSHA256Crypt(ctx, []byte("test"), []byte("$sha256-crypt$rounds=535000$05R.9KB6UC2kLI3w$Q/zslzx./JjkAVPTwp6th7nW5l7JU91Gte/UmIh.U78")))
assert.Error(t, hash.Compare(ctx, []byte("test"), []byte("$sha256-crypt$rounds=535000$awpcR7lDlnK/S7WE$vHU7KkQwyjfGz6u4MUi7.lH9htK/l63HloTsX1ZMz.3")))
assert.Nil(t, hash.Compare(ctx, []byte("ory"), []byte("$sha256-crypt$rounds=535000$awpcR7lDlnK/S7WE$vHU7KkQwyjfGz6u4MUi7.lH9htK/l63HloTsX1ZMz.3")))
assert.Nil(t, hash.CompareSHA256Crypt(ctx, []byte("ory"), []byte("$sha256-crypt$rounds=535000$awpcR7lDlnK/S7WE$vHU7KkQwyjfGz6u4MUi7.lH9htK/l63HloTsX1ZMz.3")))
assert.Error(t, hash.Compare(ctx, []byte("ory"), []byte("$sha256-crypt$rounds=535000$T95kH8e37IGVdxzJ$gLeaNa6qRog.bx4Bzqp63ceWItH6nSAal6c3WmT5GHB")))
assert.Error(t, hash.Compare(ctx, []byte("ory"), []byte("$sha256-crypt$$")), "shacrypt decode error: provided encoded hash has an invalid format")
assert.Error(t, hash.Compare(ctx, []byte("ory"), []byte("$sha256-crypt$$$")))
})
t.Run("sha512-crypt", func(t *testing.T) {
t.Parallel()
assert.Nil(t, hash.Compare(ctx, []byte("test"), []byte("$sha512-crypt$rounds=656000$3LVbIAVxR//cRajw$uuNasMW.RYxlGzIRFU1Was70BPSa933AjxhZIGJdJBOlqJAHlgqa0yuiuq5JHF/ryNGryJkj87G9i3G2HPSXg1")))
assert.Nil(t, hash.CompareSHA512Crypt(ctx, []byte("test"), []byte("$sha512-crypt$rounds=656000$3LVbIAVxR//cRajw$uuNasMW.RYxlGzIRFU1Was70BPSa933AjxhZIGJdJBOlqJAHlgqa0yuiuq5JHF/ryNGryJkj87G9i3G2HPSXg1")))
assert.Error(t, hash.Compare(ctx, []byte("test"), []byte("$5$rounds=535000$awpcR7lDlnK/S7WE$vHU7KkQwyjfGz6u4MUi7.lH9htK/l63HloTsX1ZMz.3")))
assert.Nil(t, hash.Compare(ctx, []byte("ory"), []byte("$sha512-crypt$rounds=656000$0baQbxBrfpKqvizk$Q9cYk1MeNAlECPgpG3jjfNI2DumLqd0yHbxzLdxiX6nsSD5i9n0awcbiCf8R5DzpIYxeBPznPcb1wtzlgUKtH0")))
assert.Nil(t, hash.CompareSHA512Crypt(ctx, []byte("ory"), []byte("$sha512-crypt$rounds=656000$0baQbxBrfpKqvizk$Q9cYk1MeNAlECPgpG3jjfNI2DumLqd0yHbxzLdxiX6nsSD5i9n0awcbiCf8R5DzpIYxeBPznPcb1wtzlgUKtH0")))
assert.Error(t, hash.Compare(ctx, []byte("ory"), []byte("$sha512-crypt$rounds=656000$hNcDLFO63bkYVDZf$Mt9dhH0xqfxWZ6Pu8zXw.Ku5f15IRTweuaDcUc.ObXWGn7B1h8YIWLmArZd8psd2mrUVswCXLAVptmISr.8iI/")))
assert.Error(t, hash.Compare(ctx, []byte("ory"), []byte("$sha512-crypt$$")), "shacrypt decode error: provided encoded hash has an invalid format")
assert.Error(t, hash.Compare(ctx, []byte("ory"), []byte("$sha512-crypt$$$")))
})
t.Run("hmac errors", func(t *testing.T) {
t.Parallel()
//Missing Key
assert.ErrorIs(t, hash.Compare(ctx, []byte("test"), []byte("$hmac-md5$ZmU4Njk3Zjc0MmQwODA0MDVkMTI3MGU2MTYzMzE2Zjk=")), hash.ErrInvalidHash)
assert.Error(t, hash.CompareHMAC(ctx, []byte("test"), []byte("$hmac-md5$ZmU4Njk3Zjc0MmQwODA0MDVkMTI3MGU2MTYzMzE2Zjk=")))
assert.ErrorIs(t, hash.Compare(ctx, []byte("test"), []byte("$hmac-md5$ZmU4Njk3Zjc0MmQwODA0MDVkMTI3MGU2MTYzMzE2Zjk=$")), hash.ErrMismatchedHashAndPassword)
assert.Error(t, hash.CompareHMAC(ctx, []byte("test"), []byte("$hmac-md5$ZmU4Njk3Zjc0MmQwODA0MDVkMTI3MGU2MTYzMzE2Zjk=$")))
//Missing Password Hash
assert.ErrorIs(t, hash.Compare(ctx, []byte("test"), []byte("$hmac-md5$MTIzNDU=")), hash.ErrInvalidHash)
assert.Error(t, hash.CompareHMAC(ctx, []byte("test"), []byte("$hmac-md5$MTIzNDU=")))
assert.ErrorIs(t, hash.Compare(ctx, []byte("test"), []byte("$hmac-md5$$MTIzNDU=")), hash.ErrMismatchedHashAndPassword)
assert.Error(t, hash.CompareHMAC(ctx, []byte("test"), []byte("$hmac-md5$$MTIzNDU=")))
//Missing Password Hash and Key
assert.ErrorIs(t, hash.Compare(ctx, []byte("test"), []byte("$hmac-md5$")), hash.ErrInvalidHash)
assert.Error(t, hash.CompareHMAC(ctx, []byte("test"), []byte("$hmac-md5$")))
//Missing Hash Algorithm
assert.ErrorIs(t, hash.Compare(ctx, []byte("test"), []byte("$hmac$ZmU4Njk3Zjc0MmQwODA0MDVkMTI3MGU2MTYzMzE2Zjk=$MTIzNDU=")), hash.ErrUnknownHashAlgorithm)
assert.Error(t, hash.CompareHMAC(ctx, []byte("test"), []byte("$hmac$ZmU4Njk3Zjc0MmQwODA0MDVkMTI3MGU2MTYzMzE2Zjk=$MTIzNDU=")))
//Missing Invalid Hash Algorithm
assert.ErrorIs(t, hash.Compare(ctx, []byte("test"), []byte("$hmac-invalid$ZmU4Njk3Zjc0MmQwODA0MDVkMTI3MGU2MTYzMzE2Zjk=$MTIzNDU=")), hash.ErrUnknownHashAlgorithm)
assert.Error(t, hash.CompareHMAC(ctx, []byte("test"), []byte("$hmac-invalid$ZmU4Njk3Zjc0MmQwODA0MDVkMTI3MGU2MTYzMzE2Zjk=$MTIzNDU=")))
})
t.Run("hmac-md4", func(t *testing.T) {
t.Parallel()
//Valid
assert.Nil(t, hash.Compare(ctx, []byte("test"), []byte("$hmac-md4$MWQ5ZTI4Nzc2Zjg4YmE2MTQ5YjQ0OTMyOGE4NWU4YjA=$MTIzNDU=")))
assert.Nil(t, hash.CompareHMAC(ctx, []byte("test"), []byte("$hmac-md4$MWQ5ZTI4Nzc2Zjg4YmE2MTQ5YjQ0OTMyOGE4NWU4YjA=$MTIzNDU=")))
//Wrong Key
assert.Error(t, hash.Compare(ctx, []byte("test"), []byte("$hmac-md4$MWQ5ZTI4Nzc2Zjg4YmE2MTQ5YjQ0OTMyOGE4NWU4YjA=$MTIzNA==")), hash.ErrMismatchedHashAndPassword)
//Different password
assert.Error(t, hash.Compare(ctx, []byte("ory"), []byte("$hmac-md4$MWQ5ZTI4Nzc2Zjg4YmE2MTQ5YjQ0OTMyOGE4NWU4YjA=$MTIzNDU=")))
})
t.Run("hmac-md5", func(t *testing.T) {
t.Parallel()
//Valid
assert.Nil(t, hash.Compare(ctx, []byte("test"), []byte("$hmac-md5$ZmU4Njk3Zjc0MmQwODA0MDVkMTI3MGU2MTYzMzE2Zjk=$MTIzNDU=")))
assert.Nil(t, hash.CompareHMAC(ctx, []byte("test"), []byte("$hmac-md5$ZmU4Njk3Zjc0MmQwODA0MDVkMTI3MGU2MTYzMzE2Zjk=$MTIzNDU=")))
//Wrong Key
assert.Error(t, hash.Compare(ctx, []byte("test"), []byte("$hmac-md5$ZmU4Njk3Zjc0MmQwODA0MDVkMTI3MGU2MTYzMzE2Zjk=$MTIzNA==")))
//Different password
assert.Error(t, hash.Compare(ctx, []byte("ory"), []byte("$hmac-md5$ZmU4Njk3Zjc0MmQwODA0MDVkMTI3MGU2MTYzMzE2Zjk=$MTIzNDU=")))
})
t.Run("hmac-sha1", func(t *testing.T) {
t.Parallel()
//Valid
assert.Nil(t, hash.Compare(ctx, []byte("test"), []byte("$hmac-sha1$NDMyNjcxZTUyY2Y2YTBmYjZjZDE2NjQxYjAwNjFiZjAwOGEzNWM5MA==$MTIzNDU=")))
assert.Nil(t, hash.CompareHMAC(ctx, []byte("test"), []byte("$hmac-sha1$NDMyNjcxZTUyY2Y2YTBmYjZjZDE2NjQxYjAwNjFiZjAwOGEzNWM5MA==$MTIzNDU=")))
//Wrong Key
assert.Error(t, hash.Compare(ctx, []byte("test"), []byte("$hmac-sha1$NDMyNjcxZTUyY2Y2YTBmYjZjZDE2NjQxYjAwNjFiZjAwOGEzNWM5MA==$MTIzNA==")))
//Different password
assert.Error(t, hash.Compare(ctx, []byte("ory"), []byte("$hmac-sha1$NDMyNjcxZTUyY2Y2YTBmYjZjZDE2NjQxYjAwNjFiZjAwOGEzNWM5MA==$MTIzNDU=")))
})
t.Run("hmac-sha224", func(t *testing.T) {
t.Parallel()
//Valid
assert.Nil(t, hash.Compare(ctx, []byte("test"), []byte("$hmac-sha224$YmUwYmYzM2EwNGRlNDE0YjQzNjBhNmIyOThmNmIyYzI4OWQyMzk3MDUwZDFjMzliYjVmMDMyOTQ=$MTIzNDU=")))
assert.Nil(t, hash.CompareHMAC(ctx, []byte("test"), []byte("$hmac-sha224$YmUwYmYzM2EwNGRlNDE0YjQzNjBhNmIyOThmNmIyYzI4OWQyMzk3MDUwZDFjMzliYjVmMDMyOTQ=$MTIzNDU=")))
//Wrong Key
assert.Error(t, hash.Compare(ctx, []byte("test"), []byte("$hmac-sha224$YmUwYmYzM2EwNGRlNDE0YjQzNjBhNmIyOThmNmIyYzI4OWQyMzk3MDUwZDFjMzliYjVmMDMyOTQ=$MTIzNA==")))
//Different password
assert.Error(t, hash.Compare(ctx, []byte("ory"), []byte("$hmac-sha224$YmUwYmYzM2EwNGRlNDE0YjQzNjBhNmIyOThmNmIyYzI4OWQyMzk3MDUwZDFjMzliYjVmMDMyOTQ=$MTIzNDU=")))
})
t.Run("hmac-sha256", func(t *testing.T) {
t.Parallel()
//Valid
assert.Nil(t, hash.Compare(ctx, []byte("test"), []byte("$hmac-sha256$ZTAzMWJhMWMyOTM4YjFkMjgzZjkxOWExZGY5YWM2NmMxOTJhN2RkNzQ0MzJkNWZkNGFkYTI5OTk0MWJhMTA5Zg==$MTIzNDU=")))
assert.Nil(t, hash.CompareHMAC(ctx, []byte("test"), []byte("$hmac-sha256$ZTAzMWJhMWMyOTM4YjFkMjgzZjkxOWExZGY5YWM2NmMxOTJhN2RkNzQ0MzJkNWZkNGFkYTI5OTk0MWJhMTA5Zg==$MTIzNDU=")))
//Wrong Key
assert.Error(t, hash.Compare(ctx, []byte("test"), []byte("$hmac-sha256$ZTAzMWJhMWMyOTM4YjFkMjgzZjkxOWExZGY5YWM2NmMxOTJhN2RkNzQ0MzJkNWZkNGFkYTI5OTk0MWJhMTA5Zg==$MTIzNA==")))
//Different password
assert.Error(t, hash.Compare(ctx, []byte("ory"), []byte("$hmac-sha256$ZTAzMWJhMWMyOTM4YjFkMjgzZjkxOWExZGY5YWM2NmMxOTJhN2RkNzQ0MzJkNWZkNGFkYTI5OTk0MWJhMTA5Zg==$MTIzNDU=")))
})
t.Run("hmac-sha384", func(t *testing.T) {
t.Parallel()
//Valid
assert.Nil(t, hash.Compare(ctx, []byte("test"), []byte("$hmac-sha384$ZWEyMGM3NGE4Y2UzMTljNTdjZTlhZGQyYTZjNDE0MGQ4YjMwYWIwOWM4OTRiNWQ4MmZjODlhMzBhMmQzNGE5NmQ0NDY1NWRhYjQ2ZjhiYjBkNTRmYjk5YWZkZTA1MGY1$MTIzNDU=")))
assert.Nil(t, hash.CompareHMAC(ctx, []byte("test"), []byte("$hmac-sha384$ZWEyMGM3NGE4Y2UzMTljNTdjZTlhZGQyYTZjNDE0MGQ4YjMwYWIwOWM4OTRiNWQ4MmZjODlhMzBhMmQzNGE5NmQ0NDY1NWRhYjQ2ZjhiYjBkNTRmYjk5YWZkZTA1MGY1$MTIzNDU=")))
//Wrong Key
assert.Error(t, hash.Compare(ctx, []byte("test"), []byte("$hmac-sha384$ZWEyMGM3NGE4Y2UzMTljNTdjZTlhZGQyYTZjNDE0MGQ4YjMwYWIwOWM4OTRiNWQ4MmZjODlhMzBhMmQzNGE5NmQ0NDY1NWRhYjQ2ZjhiYjBkNTRmYjk5YWZkZTA1MGY1$MTIzNA==")))
//Different password
assert.Error(t, hash.Compare(ctx, []byte("ory"), []byte("$hmac-sha384$ZWEyMGM3NGE4Y2UzMTljNTdjZTlhZGQyYTZjNDE0MGQ4YjMwYWIwOWM4OTRiNWQ4MmZjODlhMzBhMmQzNGE5NmQ0NDY1NWRhYjQ2ZjhiYjBkNTRmYjk5YWZkZTA1MGY1$MTIzNDU=")))
})
t.Run("hmac-sha512", func(t *testing.T) {
t.Parallel()
//Valid
assert.Nil(t, hash.Compare(ctx, []byte("test"), []byte("$hmac-sha512$OTFmODY0ZTI1NmU0ZjVhYjhiMDViZGFmNGVmNGZmMGVlNTY4ODYwNWJhYTk4MTk2OTgyMzc3NzI1YTc4MzcxMTMzNzZmY2YxYTk5MGMxM2RiZDk2MGFmMmQ1YzRmODdlMGMwYTNkYjcyNjY0NjM4NGE4YzQ2MjNhZDZkN2UxZTE=$MTIzNDU=")))
assert.Nil(t, hash.CompareHMAC(ctx, []byte("test"), []byte("$hmac-sha512$OTFmODY0ZTI1NmU0ZjVhYjhiMDViZGFmNGVmNGZmMGVlNTY4ODYwNWJhYTk4MTk2OTgyMzc3NzI1YTc4MzcxMTMzNzZmY2YxYTk5MGMxM2RiZDk2MGFmMmQ1YzRmODdlMGMwYTNkYjcyNjY0NjM4NGE4YzQ2MjNhZDZkN2UxZTE=$MTIzNDU=")))
//Wrong Key
assert.Error(t, hash.Compare(ctx, []byte("test"), []byte("$hmac-sha512$OTFmODY0ZTI1NmU0ZjVhYjhiMDViZGFmNGVmNGZmMGVlNTY4ODYwNWJhYTk4MTk2OTgyMzc3NzI1YTc4MzcxMTMzNzZmY2YxYTk5MGMxM2RiZDk2MGFmMmQ1YzRmODdlMGMwYTNkYjcyNjY0NjM4NGE4YzQ2MjNhZDZkN2UxZTE=$MTIzNA==")))
//Different password
assert.Error(t, hash.Compare(ctx, []byte("ory"), []byte("$hmac-sha512$OTFmODY0ZTI1NmU0ZjVhYjhiMDViZGFmNGVmNGZmMGVlNTY4ODYwNWJhYTk4MTk2OTgyMzc3NzI1YTc4MzcxMTMzNzZmY2YxYTk5MGMxM2RiZDk2MGFmMmQ1YzRmODdlMGMwYTNkYjcyNjY0NjM4NGE4YzQ2MjNhZDZkN2UxZTE=$MTIzNDU=")))
})
}