diff --git a/internal/crypto/password.go b/internal/crypto/password.go index a09811d1e8..73efeb8055 100644 --- a/internal/crypto/password.go +++ b/internal/crypto/password.go @@ -33,8 +33,8 @@ const ( // useful for tests only. QuickHashCost HashCost = iota - Argon2Prefix = "$argon2" - ScryptPrefix = "$scrypt" + Argon2Prefix = "$argon2" + FirebaseScryptPrefix = "$fbscrypt" ) // PasswordHashCost is the current pasword hashing cost @@ -57,7 +57,18 @@ var ErrScryptMismatchedHashAndPassword = errors.New("crypto: scrypt hash and pas // argon2HashRegexp https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md#argon2-encoding var argon2HashRegexp = regexp.MustCompile("^[$](?Pargon2(d|i|id))[$]v=(?P(16|19))[$]m=(?P[0-9]+),t=(?P[0-9]+),p=(?P

[0-9]+)(,keyid=(?P[^,]+))?(,data=(?P[^$]+))?[$](?P[^$]+)[$](?P.+)$") -var scryptHashRegexp = regexp.MustCompile(`^\$scrypt\$n=(?P[0-9]+),r=(?P[0-9]+),p=(?P

[0-9]+)(,ss=(?P[^,]+))?(,sk=(?P[^,]+))?\$(?P[^$]+)\$(?P.+)$`) +var scryptHashRegexp = regexp.MustCompile(`^\$(?Pfbscrypt)\$v=(?P[0-9]+),n=(?P[0-9]+),r=(?P[0-9]+),p=(?P

[0-9]+)(?:,ss=(?P[^,]+))?(?:,sk=(?P[^$]+))?\$(?P[^$]+)\$(?P.+)$`) + +func flexibleBase64Decode(data string) ([]byte, error) { + // Try StdEncoding first + decoded, err := base64.StdEncoding.DecodeString(data) + if err == nil { + return decoded, nil + } + + // If StdEncoding fails, try RawStdEncoding + return base64.RawStdEncoding.DecodeString(data) +} type Argon2HashInput struct { alg string @@ -71,25 +82,25 @@ type Argon2HashInput struct { rawHash []byte } -type ScryptHashInput struct { +type FirebaseScryptHashInput struct { alg string v string memory uint64 rounds uint64 threads uint64 - saltSeparator []byte // Optional: Salt separator used in Firebase-style scrypt - signerKey []byte // Optional: Signer key used in Firebase-style scrypt + saltSeparator []byte + signerKey []byte salt []byte - rawHash []byte + decodedHash []byte } -func ParseScryptHash(hash string) (*ScryptHashInput, error) { +func ParseFirebaseScryptHash(hash string) (*FirebaseScryptHashInput, error) { submatch := scryptHashRegexp.FindStringSubmatchIndex(hash) if submatch == nil { return nil, errors.New("crypto: incorrect scrypt hash format") } - alg := string(argon2HashRegexp.ExpandString(nil, "$alg", hash, submatch)) + alg := string(scryptHashRegexp.ExpandString(nil, "$alg", hash, submatch)) v := string(scryptHashRegexp.ExpandString(nil, "$v", hash, submatch)) n := string(scryptHashRegexp.ExpandString(nil, "$n", hash, submatch)) r := string(scryptHashRegexp.ExpandString(nil, "$r", hash, submatch)) @@ -99,68 +110,68 @@ func ParseScryptHash(hash string) (*ScryptHashInput, error) { saltB64 := string(scryptHashRegexp.ExpandString(nil, "$salt", hash, submatch)) hashB64 := string(scryptHashRegexp.ExpandString(nil, "$hash", hash, submatch)) - if alg != "scrypt" { - return nil, fmt.Errorf("crypto: scrypt hash uses unsupported algorithm %q only scrypt supported", alg) + if alg != "fbscrypt" { + return nil, fmt.Errorf("crypto: Firebase scrypt hash uses unsupported algorithm %q only fbscrypt supported", alg) } if v != "1" { - return nil, fmt.Errorf("crypto: scrypt hash uses unsupported version $q only version 1 is supported", v) + return nil, fmt.Errorf("crypto: Firebase scrypt hash uses unsupported version %q only version 1 is supported", v) } memory, err := strconv.ParseUint(n, 10, 32) if err != nil { - return nil, fmt.Errorf("crypto: scrypt hash has invalid n parameter %q %w", memory, err) + return nil, fmt.Errorf("crypto: Firebase scrypt hash has invalid n parameter %q %w", memory, err) } if memory <= 1 || (memory&(memory-1)) != 0 { - return nil, fmt.Errorf("crypto: scrypt hash has invalid n parameter %q: must be a power of 2 greater than 1", n) + return nil, fmt.Errorf("crypto: Firebase scrypt hash has invalid n parameter %q: must be a power of 2 greater than 1", n) } rounds, err := strconv.ParseUint(r, 10, 64) if err != nil { - return nil, fmt.Errorf("crypto: scrypt hash has invalid r parameter %q: %w", r, err) + return nil, fmt.Errorf("crypto: Firebase scrypt hash has invalid r parameter %q: %w", r, err) } threads, err := strconv.ParseUint(p, 10, 8) if err != nil { - return nil, fmt.Errorf("crypto: argon2 hash has invalid p parameter %q %w", p, err) + return nil, fmt.Errorf("crypto: Firebase scrypt hash has invalid p parameter %q %w", p, err) } if rounds*threads >= 1<<30 { - return nil, fmt.Errorf("crypto: scrypt hash has invalid r and p parameters: r * p must be < 2^30") + return nil, fmt.Errorf("crypto: Firebase scrypt hash has invalid r and p parameters: r * p must be < 2^30") } - salt, err := base64.RawStdEncoding.DecodeString(saltB64) + salt, err := flexibleBase64Decode(saltB64) if err != nil { - return nil, fmt.Errorf("crypto: scrypt hash has invalid base64 in the salt section: %w", err) + return nil, fmt.Errorf("crypto: Firebase scrypt hash has invalid base64 in the salt section: %w", err) } - rawHash, err := base64.RawStdEncoding.DecodeString(hashB64) + decodedHash, err := flexibleBase64Decode(hashB64) if err != nil { - return nil, fmt.Errorf("crypto: scrypt hash has invalid base64 in the hash section: %w", err) + return nil, fmt.Errorf("crypto: Firebase scrypt hash has invalid base64 in the hash section: %w", err) } var saltSeparator, signerKey []byte if ss != "" { - saltSeparator, err = base64.RawStdEncoding.DecodeString(ss) + saltSeparator, err = flexibleBase64Decode(ss) if err != nil { - return nil, fmt.Errorf("crypto: scrypt hash has invalid base64 in the salt separator section: %w", err) + return nil, fmt.Errorf("crypto: Firebase scrypt hash has invalid base64 in the salt separator section: %w", err) } } if sk != "" { - signerKey, err = base64.RawStdEncoding.DecodeString(sk) + signerKey, err = flexibleBase64Decode(sk) if err != nil { - return nil, fmt.Errorf("crypto: scrypt hash has invalid base64 in the signer key section: %w", err) + return nil, fmt.Errorf("crypto: Firebase scrypt hash has invalid base64 in the signer key section: %w", err) } } - input := &ScryptHashInput{ + input := &FirebaseScryptHashInput{ alg: alg, v: v, memory: memory, rounds: rounds, threads: threads, salt: salt, - rawHash: rawHash, + decodedHash: decodedHash, saltSeparator: saltSeparator, signerKey: signerKey, } @@ -274,8 +285,8 @@ func compareHashAndPasswordArgon2(ctx context.Context, hash, password string) er return nil } -func compareHashAndPasswordScrypt(ctx context.Context, hash, password string) error { - input, err := ParseScryptHash(hash) +func compareHashAndPasswordFirebaseScrypt(ctx context.Context, hash, password string) error { + input, err := ParseFirebaseScryptHash(hash) if err != nil { return err } @@ -286,8 +297,7 @@ func compareHashAndPasswordScrypt(ctx context.Context, hash, password string) er attribute.Int64("n", int64(input.memory)), attribute.Int64("r", int64(input.rounds)), attribute.Int("p", int(input.threads)), - attribute.Int("len", len(input.rawHash)), - attribute.Bool("is_firebase", len(input.saltSeparator) > 0), + attribute.Int("len", len(input.decodedHash)), } var match bool @@ -299,23 +309,17 @@ func compareHashAndPasswordScrypt(ctx context.Context, hash, password string) er }() switch input.alg { - case "scrypt": - if len(input.saltSeparator) > 0 { - // Firebase-style scrypt - combinedSalt := append(input.salt, input.saltSeparator...) - derivedKey, err = firebaseScrypt([]byte(password), combinedSalt, input.signerKey, input.memory, input.rounds, input.threads, len(input.rawHash)) - } else { - // Standard scrypt - derivedKey, err = scrypt.Key([]byte(password), input.salt, int(input.memory), int(input.rounds), int(input.threads), len(input.rawHash)) - } - if err != nil { - return fmt.Errorf("failed to derive scrypt key: %w", err) - } + case "fbscrypt": + // Firebase-style scrypt + combinedSalt := append(input.salt, input.saltSeparator...) + // TODO: move into constant above + derivedKey, err = firebaseScrypt([]byte(password), combinedSalt, input.signerKey, input.memory, input.rounds, input.threads, len(input.decodedHash)) + default: return fmt.Errorf("unsupported algorithm: %s", input.alg) } - match = subtle.ConstantTimeCompare(derivedKey, input.rawHash) == 1 + match = subtle.ConstantTimeCompare(derivedKey, input.decodedHash) == 1 if !match { return ErrScryptMismatchedHashAndPassword } @@ -323,7 +327,6 @@ func compareHashAndPasswordScrypt(ctx context.Context, hash, password string) er } func firebaseScrypt(password, salt, signerKey []byte, N, r, p uint64, keyLen int) ([]byte, error) { - ck, err := scrypt.Key(password, salt, 1<