Files
upstream-ory-kratos/hash/hash_comparator.go
upstream-mirror c60cc7ca75
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
snapshot of https://github.com/ory/kratos.git (master, 2026-09-15; blobs > 131072B omitted)
2026-09-15 21:19:12 +05:00

815 lines
23 KiB
Go

// 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)
}