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
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:
814
hash/hash_comparator.go
Normal file
814
hash/hash_comparator.go
Normal 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
162
hash/hash_limits.go
Normal 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
150
hash/hash_limits_test.go
Normal 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
23
hash/hasher.go
Normal 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
91
hash/hasher_argon2.go
Normal 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
77
hash/hasher_bcrypt.go
Normal 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
95
hash/hasher_pbkdf2.go
Normal 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
12
hash/hasher_scrypt.go
Normal 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
556
hash/hasher_test.go
Normal 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=")))
|
||||
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user