@@ -2,6 +2,8 @@ package crypto
22
33import (
44 "context"
5+ "crypto/aes"
6+ "crypto/cipher"
57 "crypto/subtle"
68 "encoding/base64"
79 "errors"
@@ -16,6 +18,7 @@ import (
1618
1719 "golang.org/x/crypto/argon2"
1820 "golang.org/x/crypto/bcrypt"
21+ "golang.org/x/crypto/scrypt"
1922)
2023
2124type HashCost = int
@@ -30,7 +33,9 @@ const (
3033 // useful for tests only.
3134 QuickHashCost HashCost = iota
3235
33- Argon2Prefix = "$argon2"
36+ Argon2Prefix = "$argon2"
37+ FirebaseScryptPrefix = "$fbscrypt"
38+ FirebaseScryptKeyLen = 32 // Firebase uses AES-256 which requires 32 byte keys: https://pkg.go.dev/golang.org/x/crypto/scrypt#Key
3439)
3540
3641// PasswordHashCost is the current pasword hashing cost
4954)
5055
5156var ErrArgon2MismatchedHashAndPassword = errors .New ("crypto: argon2 hash and password mismatch" )
57+ var ErrScryptMismatchedHashAndPassword = errors .New ("crypto: fbscrypt hash and password mismatch" )
5258
5359// argon2HashRegexp https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md#argon2-encoding
5460var 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>.+)$" )
61+ 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>.+)$` )
5562
5663type Argon2HashInput struct {
5764 alg string
@@ -65,9 +72,95 @@ type Argon2HashInput struct {
6572 rawHash []byte
6673}
6774
75+ type FirebaseScryptHashInput struct {
76+ alg string
77+ v string
78+ memory uint64
79+ rounds uint64
80+ threads uint64
81+ saltSeparator []byte
82+ signerKey []byte
83+ salt []byte
84+ rawHash []byte
85+ }
86+
87+ // See: https://github.com/firebase/scrypt for implementation
88+ func ParseFirebaseScryptHash (hash string ) (* FirebaseScryptHashInput , error ) {
89+ submatch := scryptHashRegexp .FindStringSubmatchIndex (hash )
90+ if submatch == nil {
91+ return nil , errors .New ("crypto: incorrect scrypt hash format" )
92+ }
93+
94+ alg := string (scryptHashRegexp .ExpandString (nil , "$alg" , hash , submatch ))
95+ v := string (scryptHashRegexp .ExpandString (nil , "$v" , hash , submatch ))
96+ n := string (scryptHashRegexp .ExpandString (nil , "$n" , hash , submatch ))
97+ r := string (scryptHashRegexp .ExpandString (nil , "$r" , hash , submatch ))
98+ p := string (scryptHashRegexp .ExpandString (nil , "$p" , hash , submatch ))
99+ ss := string (scryptHashRegexp .ExpandString (nil , "$ss" , hash , submatch ))
100+ sk := string (scryptHashRegexp .ExpandString (nil , "$sk" , hash , submatch ))
101+ saltB64 := string (scryptHashRegexp .ExpandString (nil , "$salt" , hash , submatch ))
102+ hashB64 := string (scryptHashRegexp .ExpandString (nil , "$hash" , hash , submatch ))
103+
104+ if alg != "fbscrypt" {
105+ return nil , fmt .Errorf ("crypto: Firebase scrypt hash uses unsupported algorithm %q only fbscrypt supported" , alg )
106+ }
107+ if v != "1" {
108+ return nil , fmt .Errorf ("crypto: Firebase scrypt hash uses unsupported version %q only version 1 is supported" , v )
109+ }
110+ memoryPower , err := strconv .ParseUint (n , 10 , 32 )
111+ if err != nil {
112+ return nil , fmt .Errorf ("crypto: Firebase scrypt hash has invalid n parameter %q %w" , n , err )
113+ }
114+ if memoryPower == 0 {
115+ return nil , fmt .Errorf ("crypto: Firebase scrypt hash has invalid n parameter %q: must be greater than 0" , n )
116+ }
117+ // Exponent is passed in
118+ memory := uint64 (1 ) << memoryPower
119+ rounds , err := strconv .ParseUint (r , 10 , 64 )
120+ if err != nil {
121+ return nil , fmt .Errorf ("crypto: Firebase scrypt hash has invalid r parameter %q: %w" , r , err )
122+ }
123+
124+ threads , err := strconv .ParseUint (p , 10 , 8 )
125+ if err != nil {
126+ return nil , fmt .Errorf ("crypto: Firebase scrypt hash has invalid p parameter %q %w" , p , err )
127+ }
128+
129+ rawHash , err := base64 .StdEncoding .DecodeString (hashB64 )
130+ if err != nil {
131+ return nil , fmt .Errorf ("crypto: Firebase scrypt hash has invalid base64 in the hash section %w" , err )
132+ }
133+
134+ salt , err := base64 .StdEncoding .DecodeString (saltB64 )
135+ if err != nil {
136+ return nil , fmt .Errorf ("crypto: Firebase scrypt salt has invalid base64 in the hash section %w" , err )
137+ }
138+
139+ var saltSeparator , signerKey []byte
140+ if signerKey , err = base64 .StdEncoding .DecodeString (sk ); err != nil {
141+ return nil , err
142+ }
143+ if saltSeparator , err = base64 .StdEncoding .DecodeString (ss ); err != nil {
144+ return nil , err
145+ }
146+
147+ input := & FirebaseScryptHashInput {
148+ alg : alg ,
149+ v : v ,
150+ memory : memory ,
151+ rounds : rounds ,
152+ threads : threads ,
153+ salt : salt ,
154+ rawHash : rawHash ,
155+ saltSeparator : saltSeparator ,
156+ signerKey : signerKey ,
157+ }
158+
159+ return input , nil
160+ }
161+
68162func ParseArgon2Hash (hash string ) (* Argon2HashInput , error ) {
69163 submatch := argon2HashRegexp .FindStringSubmatchIndex (hash )
70-
71164 if submatch == nil {
72165 return nil , errors .New ("crypto: incorrect argon2 hash format" )
73166 }
@@ -172,12 +265,74 @@ func compareHashAndPasswordArgon2(ctx context.Context, hash, password string) er
172265 return nil
173266}
174267
268+ func compareHashAndPasswordFirebaseScrypt (ctx context.Context , hash , password string ) error {
269+ input , err := ParseFirebaseScryptHash (hash )
270+ if err != nil {
271+ return err
272+ }
273+
274+ attributes := []attribute.KeyValue {
275+ attribute .String ("alg" , input .alg ),
276+ attribute .String ("v" , input .v ),
277+ attribute .Int64 ("n" , int64 (input .memory )),
278+ attribute .Int64 ("r" , int64 (input .rounds )),
279+ attribute .Int ("p" , int (input .threads )),
280+ attribute .Int ("len" , len (input .rawHash )),
281+ } // #nosec G115
282+
283+ var match bool
284+ var derivedKey []byte
285+ compareHashAndPasswordSubmittedCounter .Add (ctx , 1 , metric .WithAttributes (attributes ... ))
286+ defer func () {
287+ attributes = append (attributes , attribute .Bool ("match" , match ))
288+ compareHashAndPasswordCompletedCounter .Add (ctx , 1 , metric .WithAttributes (attributes ... ))
289+ }()
290+
291+ switch input .alg {
292+ case "fbscrypt" :
293+ derivedKey , err = firebaseScrypt ([]byte (password ), input .salt , input .signerKey , input .saltSeparator , input .memory , input .rounds , input .threads , FirebaseScryptKeyLen )
294+ if err != nil {
295+ return err
296+ }
297+
298+ match = subtle .ConstantTimeCompare (derivedKey , input .rawHash ) == 1
299+ if ! match {
300+ return ErrScryptMismatchedHashAndPassword
301+ }
302+
303+ default :
304+ return fmt .Errorf ("unsupported algorithm: %s" , input .alg )
305+ }
306+
307+ return nil
308+ }
309+
310+ func firebaseScrypt (password , salt , signerKey , saltSeparator []byte , memCost , rounds , p , keyLen uint64 ) ([]byte , error ) {
311+ ck , err := scrypt .Key (password , append (salt , saltSeparator ... ), int (memCost ), int (rounds ), int (p ), int (keyLen )) // #nosec G115
312+ if err != nil {
313+ return nil , err
314+ }
315+
316+ var block cipher.Block
317+ if block , err = aes .NewCipher (ck ); err != nil {
318+ return nil , err
319+ }
320+
321+ cipherText := make ([]byte , aes .BlockSize + len (signerKey ))
322+ // #nosec G407 -- Firebase scrypt requires deterministic IV for consistent results. See: JaakkoL/firebase-scrypt-python@master/firebasescrypt/firebasescrypt.py#L58
323+ stream := cipher .NewCTR (block , cipherText [:aes .BlockSize ])
324+ stream .XORKeyStream (cipherText [aes .BlockSize :], signerKey )
325+ return cipherText [aes .BlockSize :], nil
326+ }
327+
175328// CompareHashAndPassword compares the hash and
176329// password, returns nil if equal otherwise an error. Context can be used to
177330// cancel the hashing if the algorithm supports it.
178331func CompareHashAndPassword (ctx context.Context , hash , password string ) error {
179332 if strings .HasPrefix (hash , Argon2Prefix ) {
180333 return compareHashAndPasswordArgon2 (ctx , hash , password )
334+ } else if strings .HasPrefix (hash , FirebaseScryptPrefix ) {
335+ return compareHashAndPasswordFirebaseScrypt (ctx , hash , password )
181336 }
182337
183338 // assume bcrypt
0 commit comments