7
7
"context"
8
8
"crypto/aes"
9
9
"crypto/cipher"
10
+ "crypto/md5" // #nosec G501
10
11
"crypto/subtle"
11
12
"encoding/base64"
12
13
"fmt"
@@ -38,6 +39,8 @@ func Compare(ctx context.Context, password []byte, hash []byte) error {
38
39
return CompareScrypt (ctx , password , hash )
39
40
case IsFirebaseScryptHash (hash ):
40
41
return CompareFirebaseScrypt (ctx , password , hash )
42
+ case IsMD5Hash (hash ):
43
+ return CompareMD5 (ctx , password , hash )
41
44
default :
42
45
return errors .WithStack (ErrUnknownHashAlgorithm )
43
46
}
@@ -173,13 +176,38 @@ func CompareFirebaseScrypt(_ context.Context, password []byte, hash []byte) erro
173
176
return errors .WithStack (ErrMismatchedHashAndPassword )
174
177
}
175
178
179
+ func CompareMD5 (_ context.Context , password []byte , hash []byte ) error {
180
+ // Extract the hash from the encoded password
181
+ pf , salt , hash , err := decodeMD5Hash (string (hash ))
182
+ if err != nil {
183
+ return err
184
+ }
185
+
186
+ arg := password
187
+ if salt != nil {
188
+ r := strings .NewReplacer ("{SALT}" , string (salt ), "{PASSWORD}" , string (password ))
189
+ arg = []byte (r .Replace (string (pf )))
190
+ }
191
+ // #nosec G401
192
+ otherHash := md5 .Sum (arg )
193
+
194
+ // Check that the contents of the hashed passwords are identical. Note
195
+ // that we are using the subtle.ConstantTimeCompare() function for this
196
+ // to help prevent timing attacks.
197
+ if subtle .ConstantTimeCompare (hash , otherHash [:]) == 1 {
198
+ return nil
199
+ }
200
+ return errors .WithStack (ErrMismatchedHashAndPassword )
201
+ }
202
+
176
203
var (
177
204
isBcryptHash = regexp .MustCompile (`^\$2[abzy]?\$` )
178
205
isArgon2idHash = regexp .MustCompile (`^\$argon2id\$` )
179
206
isArgon2iHash = regexp .MustCompile (`^\$argon2i\$` )
180
207
isPbkdf2Hash = regexp .MustCompile (`^\$pbkdf2-sha[0-9]{1,3}\$` )
181
208
isScryptHash = regexp .MustCompile (`^\$scrypt\$` )
182
209
isFirebaseScryptHash = regexp .MustCompile (`^\$firescrypt\$` )
210
+ isMD5Hash = regexp .MustCompile (`^\$md5\$` )
183
211
)
184
212
185
213
func IsBcryptHash (hash []byte ) bool {
@@ -206,6 +234,10 @@ func IsFirebaseScryptHash(hash []byte) bool {
206
234
return isFirebaseScryptHash .Match (hash )
207
235
}
208
236
237
+ func IsMD5Hash (hash []byte ) bool {
238
+ return isMD5Hash .Match (hash )
239
+ }
240
+
209
241
func decodeArgon2idHash (encodedHash string ) (p * config.Argon2 , salt , hash []byte , err error ) {
210
242
parts := strings .Split (encodedHash , "$" )
211
243
if len (parts ) != 6 {
@@ -350,3 +382,40 @@ func decodeFirebaseScryptHash(encodedHash string) (p *Scrypt, salt, saltSeparato
350
382
351
383
return p , salt , saltSeparator , hash , signerKey , nil
352
384
}
385
+
386
+ // decodeMD5Hash decodes MD5 encoded password hash.
387
+ // format without salt: $md5$<hash>
388
+ // format with salt $md5$pf=<salting-format>$<salt>$<hash>
389
+ func decodeMD5Hash (encodedHash string ) (pf , salt , hash []byte , err error ) {
390
+ parts := strings .Split (encodedHash , "$" )
391
+
392
+ switch len (parts ) {
393
+ case 3 :
394
+ hash , err := base64 .StdEncoding .Strict ().DecodeString (parts [2 ])
395
+ return nil , nil , hash , err
396
+ case 5 :
397
+ _ , err = fmt .Sscanf (parts [2 ], "pf=%s" , & pf )
398
+ if err != nil {
399
+ return nil , nil , nil , err
400
+ }
401
+
402
+ pf , err := base64 .StdEncoding .Strict ().DecodeString (string (pf ))
403
+ if err != nil {
404
+ return nil , nil , nil , err
405
+ }
406
+
407
+ salt , err = base64 .StdEncoding .Strict ().DecodeString (parts [3 ])
408
+ if err != nil {
409
+ return nil , nil , nil , err
410
+ }
411
+
412
+ hash , err = base64 .StdEncoding .Strict ().DecodeString (parts [4 ])
413
+ if err != nil {
414
+ return nil , nil , nil , err
415
+ }
416
+
417
+ return pf , salt , hash , nil
418
+ default :
419
+ return nil , nil , nil , ErrInvalidHash
420
+ }
421
+ }
0 commit comments