Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: allow import peppered/salted password hashes #2946 #3057

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 97 additions & 1 deletion hash/hash_comparator.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ import (
"crypto/subtle"
"encoding/base64"
"fmt"
"hash"
"regexp"
"strconv"
"strings"

"github.com/pkg/errors"
Expand Down Expand Up @@ -48,6 +50,8 @@ func Compare(ctx context.Context, password []byte, hash []byte) error {
return CompareFirebaseScrypt(ctx, password, hash)
case IsMD5Hash(hash):
return CompareMD5(ctx, password, hash)
case IsLegacyHash(hash):
return CompareLegacy(ctx, password, hash)
default:
return errors.WithStack(ErrUnknownHashAlgorithm)
}
Expand Down Expand Up @@ -232,6 +236,18 @@ func CompareMD5(_ context.Context, password []byte, hash []byte) error {
return errors.WithStack(ErrMismatchedHashAndPassword)
}

func CompareLegacy(ctx context.Context, pwd, encodedHash []byte) error {
phc, alg, format, iterations, salt, pepper, err := decodeLegacyHash(string(encodedHash))
if err != nil {
return err
}
hash, err := legacyHash(alg, format, iterations, salt, pepper, pwd)
if err != nil {
return err
}
return CompareArgon2id(ctx, hash, []byte(phc))
}

var (
isBcryptHash = regexp.MustCompile(`^\$2[abzy]?\$`)
isArgon2idHash = regexp.MustCompile(`^\$argon2id\$`)
Expand All @@ -242,6 +258,7 @@ var (
isSHAHash = regexp.MustCompile(`^\$sha(1|256|512)\$`)
isFirebaseScryptHash = regexp.MustCompile(`^\$firescrypt\$`)
isMD5Hash = regexp.MustCompile(`^\$md5\$`)
isLegacyHash = regexp.MustCompile(`^\$legacy\$`)
)

func IsBcryptHash(hash []byte) bool { return isBcryptHash.Match(hash) }
Expand All @@ -253,6 +270,7 @@ func IsSSHAHash(hash []byte) bool { return isSSHAHash.Match(hash) }
func IsSHAHash(hash []byte) bool { return isSHAHash.Match(hash) }
func IsFirebaseScryptHash(hash []byte) bool { return isFirebaseScryptHash.Match(hash) }
func IsMD5Hash(hash []byte) bool { return isMD5Hash.Match(hash) }
func IsLegacyHash(hash []byte) bool { return isLegacyHash.Match(hash) }

func IsValidHashFormat(hash []byte) bool {
if IsBcryptHash(hash) ||
Expand All @@ -263,7 +281,8 @@ func IsValidHashFormat(hash []byte) bool {
IsSSHAHash(hash) ||
IsSHAHash(hash) ||
IsFirebaseScryptHash(hash) ||
IsMD5Hash(hash) {
IsMD5Hash(hash) ||
IsLegacyHash(hash) {
return true
} else {
return false
Expand Down Expand Up @@ -558,3 +577,80 @@ func decodeMD5Hash(encodedHash string) (pf, salt, hash []byte, err error) {
return nil, nil, nil, ErrInvalidHash
}
}

// decodeLegacyHash decodes argon2id wrapped legacy password hash.
// format: $legacy$alg=<md5|sha1|sha256|sha512>,format=<base64 encoded format, e.g: '{PEPPER}-{PWD}-{SALT}'>,i=<number of iterations>,(salt=<base64 encoded salt>,pepper=<base64 encoded pepper>)$<argon2id encoded hash>
func decodeLegacyHash(encodedHash string) (phc, alg, format string, iterations int, salt, pepper []byte, err error) {
ci42 marked this conversation as resolved.
Show resolved Hide resolved
parts := strings.SplitN(encodedHash, "$", 4)
if len(parts) != 4 {
return "", "", "", 0, nil, nil, ErrInvalidHash
}
phc = "$" + parts[3]
legacyParams := strings.Split(parts[2], ",")
for _, lp := range legacyParams {
k, v, found := strings.Cut(lp, "=")
if !found {
return "", "", "", 0, nil, nil, ErrInvalidHash
}
switch k {
case "alg":
alg = v
case "format":
f, err := base64.RawStdEncoding.DecodeString(v)
if err != nil {
return "", "", "", 0, nil, nil, ErrInvalidHash
}
format = string(f)
case "i":
iterations, err = strconv.Atoi(v)
if err != nil {
return "", "", "", 0, nil, nil, ErrInvalidHash
}
case "salt":
salt, err = base64.RawStdEncoding.DecodeString(v)
if err != nil {
return "", "", "", 0, nil, nil, ErrInvalidHash
}
case "pepper":
pepper, err = base64.RawStdEncoding.DecodeString(v)
if err != nil {
return "", "", "", 0, nil, nil, ErrInvalidHash
}
}
}
return
}

func legacyHash(alg, format string, iterations int, salt, pepper, pwd []byte) ([]byte, error) {
if len(pwd) < 1 || !strings.Contains(format, "{PWD}") || iterations < 1 || !(alg == "md5" || alg == "sha1" || alg == "sha256" || alg == "sha512") {
return nil, ErrInvalidHash
}
ci42 marked this conversation as resolved.
Show resolved Hide resolved
replacements := []string{"{PWD}", string(pwd)}
if salt != nil {
replacements = append(replacements, "{SALT}", string(salt))
}
if pepper != nil {
replacements = append(replacements, "{PEPPER}", string(pepper))
}
toHash := []byte(strings.NewReplacer(replacements...).Replace(format))

var hasher hash.Hash
switch alg {
case "md5":
hasher = md5.New() //#nosec G401 -- compatibility for imported passwords
case "sha1":
hasher = sha1.New() //#nosec G401 -- compatibility for imported passwords
case "sha256":
hasher = sha256.New()
case "sha512":
hasher = sha512.New()
default:
return nil, ErrUnknownHashAlgorithm
}
for i := 1; i <= iterations; i++ {
hasher.Write(toHash)
toHash = hasher.Sum([]byte{})
hasher.Reset()
}
return toHash, nil
}
Loading