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
815 lines
23 KiB
Go
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)
|
|
}
|