Skip to content

Commit

Permalink
fix: update implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
J0 committed Sep 25, 2024
1 parent 5d88639 commit c437900
Show file tree
Hide file tree
Showing 4 changed files with 69 additions and 59 deletions.
112 changes: 59 additions & 53 deletions internal/crypto/password.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,17 +59,6 @@ var ErrScryptMismatchedHashAndPassword = errors.New("crypto: scrypt hash and pas
var argon2HashRegexp = regexp.MustCompile("^[$](?P<alg>argon2(d|i|id))[$]v=(?P<v>(16|19))[$]m=(?P<m>[0-9]+),t=(?P<t>[0-9]+),p=(?P<p>[0-9]+)(,keyid=(?P<keyid>[^,]+))?(,data=(?P<data>[^$]+))?[$](?P<salt>[^$]+)[$](?P<hash>.+)$")
var scryptHashRegexp = regexp.MustCompile(`^\$(?P<alg>fbscrypt)\$v=(?P<v>[0-9]+),n=(?P<n>[0-9]+),r=(?P<r>[0-9]+),p=(?P<p>[0-9]+)(?:,ss=(?P<ss>[^,]+))?(?:,sk=(?P<sk>[^$]+))?\$(?P<salt>[^$]+)\$(?P<hash>.+)$`)

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
v string
Expand All @@ -88,10 +77,10 @@ type FirebaseScryptHashInput struct {
memory uint64
rounds uint64
threads uint64
saltSeparator []byte
signerKey []byte
saltSeparator string
signerKey string
salt []byte
decodedHash []byte
rawHash []byte
}

func ParseFirebaseScryptHash(hash string) (*FirebaseScryptHashInput, error) {
Expand All @@ -116,16 +105,14 @@ func ParseFirebaseScryptHash(hash string) (*FirebaseScryptHashInput, error) {
if v != "1" {
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)
memoryPower, err := strconv.ParseUint(n, 10, 32)
if err != nil {
return nil, fmt.Errorf("crypto: Firebase scrypt hash has invalid n parameter %q %w", memory, err)
return nil, fmt.Errorf("crypto: Firebase scrypt hash has invalid n parameter %q %w", n, err)
}

if memory <= 1 || (memory&(memory-1)) != 0 {
return nil, fmt.Errorf("crypto: Firebase scrypt hash has invalid n parameter %q: must be a power of 2 greater than 1", n)
if memoryPower == 0 {
return nil, fmt.Errorf("crypto: Firebase scrypt hash has invalid n parameter %q: must be greater than 0", n)
}

memory := uint64(1) << memoryPower
rounds, err := strconv.ParseUint(r, 10, 64)
if err != nil {
return nil, fmt.Errorf("crypto: Firebase scrypt hash has invalid r parameter %q: %w", r, err)
Expand All @@ -139,30 +126,31 @@ func ParseFirebaseScryptHash(hash string) (*FirebaseScryptHashInput, error) {
if rounds*threads >= 1<<30 {
return nil, fmt.Errorf("crypto: Firebase scrypt hash has invalid r and p parameters: r * p must be < 2^30")
}

salt, err := flexibleBase64Decode(saltB64)
rawHash, err := base64.StdEncoding.DecodeString(hashB64)
if err != nil {
return nil, fmt.Errorf("crypto: Firebase scrypt hash has invalid base64 in the salt section: %w", err)
return nil, fmt.Errorf("crypto: Firebase scrypt hash has invalid base64 in the hash section %w", err)
}

decodedHash, err := flexibleBase64Decode(hashB64)
salt, err := base64.StdEncoding.DecodeString(saltB64)
if err != nil {
return nil, fmt.Errorf("crypto: Firebase scrypt hash has invalid base64 in the hash section: %w", err)
return nil, fmt.Errorf("crypto: Firebase scrypt salt has invalid base64 in the hash section %w", err)
}

var saltSeparator, signerKey []byte
if ss != "" {
saltSeparator, err = flexibleBase64Decode(ss)
if err != nil {
return nil, fmt.Errorf("crypto: Firebase scrypt hash has invalid base64 in the salt separator section: %w", err)
}
}
if sk != "" {
signerKey, err = flexibleBase64Decode(sk)
if err != nil {
return nil, fmt.Errorf("crypto: Firebase scrypt hash has invalid base64 in the signer key section: %w", err)
}
}
// var saltSeparator, signerKey []byte
// if ss != "" {
// saltSeparator, err = base64.StdEncoding.DecodeString(ss)
// if err != nil {
// return nil, err
// }

// }
// if sk != "" {
// signerKey, err = base64.StdEncoding.DecodeString(sk)
// if err != nil {
// return nil, err
// }

// }

input := &FirebaseScryptHashInput{
alg: alg,
Expand All @@ -171,9 +159,9 @@ func ParseFirebaseScryptHash(hash string) (*FirebaseScryptHashInput, error) {
rounds: rounds,
threads: threads,
salt: salt,
decodedHash: decodedHash,
saltSeparator: saltSeparator,
signerKey: signerKey,
rawHash: rawHash,
saltSeparator: ss,
signerKey: sk,
}

return input, nil
Expand Down Expand Up @@ -297,7 +285,7 @@ func compareHashAndPasswordFirebaseScrypt(ctx context.Context, hash, password st
attribute.Int64("n", int64(input.memory)),
attribute.Int64("r", int64(input.rounds)),
attribute.Int("p", int(input.threads)),
attribute.Int("len", len(input.decodedHash)),
attribute.Int("len", len(input.rawHash)),
}

var match bool
Expand All @@ -310,24 +298,42 @@ func compareHashAndPasswordFirebaseScrypt(ctx context.Context, hash, password st

switch input.alg {
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))
const keyLen = 32
derivedKey, err = firebaseScrypt([]byte(password), input.salt, input.signerKey, input.saltSeparator, input.memory, input.rounds, input.threads, keyLen)
if err != nil {
return err
}

match = subtle.ConstantTimeCompare(derivedKey, input.rawHash) == 1
if !match {
return ErrScryptMismatchedHashAndPassword
}

default:
return fmt.Errorf("unsupported algorithm: %s", input.alg)
}

match = subtle.ConstantTimeCompare(derivedKey, input.decodedHash) == 1
if !match {
return ErrScryptMismatchedHashAndPassword
}
return nil
}

func firebaseScrypt(password, salt, signerKey []byte, N, r, p uint64, keyLen int) ([]byte, error) {
ck, err := scrypt.Key(password, salt, 1<<N, int(r), int(p), keyLen)
func firebaseScrypt(password, salt []byte, signerKey, saltSeparator string, memCost, rounds, p, keyLen uint64) ([]byte, error) {
var (
sk, ss []byte
err error
)

if sk, err = base64.StdEncoding.DecodeString(signerKey); err != nil {
return nil, err
}
if ss, err = base64.StdEncoding.DecodeString(saltSeparator); err != nil {
return nil, err
}

return key(password, salt, sk, ss, memCost, rounds, p, keyLen)
}

func key(password, salt, signerKey, saltSeparator []byte, memCost, rounds, p, keyLen uint64) ([]byte, error) {
ck, err := scrypt.Key(password, append(salt, saltSeparator...), int(memCost), int(rounds), int(p), int(keyLen))
if err != nil {
return nil, err
}
Expand Down
7 changes: 3 additions & 4 deletions internal/crypto/password_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,15 +95,14 @@ type scryptTestCase struct {
func TestScrypt(t *testing.T) {
testCases := []scryptTestCase{
{
name: "Firebase Scrypt: appropriate hash",

hash: "$fbscrypt$v=1,n=16,r=8,p=1,ss=Bw==,sk=ou9tdYTGyYm8kuR6Dt0Bp0kDuAYoXrK16mbZO4yGwAn3oLspjnN0/c41v8xZnO1n14J3MjKj1b2g6AUCAlFwMw==$qV3N5ZhVttJ3YQ==$AwQW7J1yTqKV6neJmb1GbN9zTyNGfhotrOkPS+mtxarhNcLaB4ha49lJxVMsdV3BQx0G4XD0rOe3KaZN0dSyNg==",
name: "Firebase Scrypt: appropriate hash",
hash: "$fbscrypt$v=1,n=14,r=8,p=1,ss=Bw==,sk=ou9tdYTGyYm8kuR6Dt0Bp0kDuAYoXrK16mbZO4yGwAn3oLspjnN0/c41v8xZnO1n14J3MjKj1b2g6AUCAlFwMw==$C0sHCg9ek77hsg==$zKVTMvnWVw5BBOZNUdnsalx4c4c7y/w7IS5p6Ut2+CfEFFlz37J9huyQfov4iizN8dbjvEJlM5tQaJP84+hfTw==",
password: "mytestpassword",
shouldPass: true,
},
{
name: "Firebase Scrypt: incorrect hash",
hash: "$fbscrypt$v=1,n=16,r=8,p=1,ss=Bw==,sk=ou9tdYTGyYm8kuR6Dt0Bp0kDuAYoXrK16mbZO4yGwAn3oLspjnN0/c41v8xZnO1n14J3MjKj1b2g6AUCAlFwMw==$qV3N5ZhVttJ3YQ==$differenthash",
hash: "$fbscrypt$v=1,n=14,r=8,p=1,ss=Bw==,sk=ou9tdYTGyYm8kuR6Dt0Bp0kDuAYoXrK16mbZO4yGwAn3oLspjnN0/c41v8xZnO1n14J3MjKj1b2g6AUCAlFwMw==$C0sHCg9ek77hsg==$ZGlmZmVyZW50aGFzaA==",
password: "mytestpassword",
shouldPass: false,
},
Expand Down
5 changes: 5 additions & 0 deletions internal/models/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,11 @@ func NewUserWithPasswordHash(phone, email, passwordHash, aud string, userData ma
if err != nil {
return nil, err
}
} else if strings.HasPrefix(passwordHash, crypto.FirebaseScryptPrefix) {
_, err := crypto.ParseFirebaseScryptHash(passwordHash)
if err != nil {
return nil, err
}
} else {
// verify that the hash is a bcrypt hash
_, err := bcrypt.Cost([]byte(passwordHash))
Expand Down
4 changes: 2 additions & 2 deletions internal/models/user_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -387,7 +387,7 @@ func (ts *UserTestSuite) TestNewUserWithPasswordHashSuccess() {
},
{
desc: "Valid Firebase scrypt hash",
hash: "$scrypt$v=1$n=14,r=8,p=1,ss=Bw==,sk=ou9tdYTGyYm8kuR6Dt0Bp0kDuAYoXrK16mbZO4yGwAn3oLspjnN0/c41v8xZnO1n14J3MjKj1b2g6AUCAlFwMw==$qV3N5ZhVttJ3YQ==$AwQW7J1yTqKV6neJmb1GbN9zTyNGfhotrOkPS+mtxarhNcLaB4ha49lJxVMsdV3BQx0G4XD0rOe3KaZN0dSyNg==",
hash: "$fbscrypt$v=1,n=14,r=8,p=1,ss=Bw==,sk=ou9tdYTGyYm8kuR6Dt0Bp0kDuAYoXrK16mbZO4yGwAn3oLspjnN0/c41v8xZnO1n14J3MjKj1b2g6AUCAlFwMw==$C0sHCg9ek77hsg==$ZGlmZmVyZW50aGFzaA==",
},
}

Expand Down Expand Up @@ -415,7 +415,7 @@ func (ts *UserTestSuite) TestNewUserWithPasswordHashFailure() {
},
{
desc: "Invalid scrypt hash",
hash: "$scrypt$invalid",
hash: "$fbscrypt$invalid",
},
}

Expand Down

0 comments on commit c437900

Please sign in to comment.