Skip to content

Commit

Permalink
Merge 76a861c into 1064b32
Browse files Browse the repository at this point in the history
  • Loading branch information
ci42 authored Apr 25, 2023
2 parents 1064b32 + 76a861c commit 4711e09
Show file tree
Hide file tree
Showing 3 changed files with 206 additions and 1 deletion.
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) {
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
}
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

0 comments on commit 4711e09

Please sign in to comment.