Skip to content

Commit 9dbe6ef

Browse files
authored
feat: fix argon2 parsing and comparison (#1887)
Argon2 parsing and comparison is broken in multiple ways: 1. Incorrect comparison being done using `ConstantTimeCompare`. This Go API is awful as it returns 1 on _equality_ (unlike all other comparison APIs that return 0) so it was missed. 2. All Argon2 comparisons were producing incorrect derived keys due to the multiplication by 1024. The `argon2.Key` and `IDKey` accept *KiB* as arguments (not bytes!) which caused all hashes to always be incorrect. Tests didn't catch this as they only tested for the positive case (which passed with flying colors).
1 parent 6de1e19 commit 9dbe6ef

File tree

2 files changed

+25
-5
lines changed

2 files changed

+25
-5
lines changed

internal/crypto/password.go

+21-5
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ var ErrArgon2MismatchedHashAndPassword = errors.New("crypto: argon2 hash and pas
5757
var ErrScryptMismatchedHashAndPassword = errors.New("crypto: fbscrypt hash and password mismatch")
5858

5959
// argon2HashRegexp https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md#argon2-encoding
60-
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>.+)$")
60+
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>.+)$")
6161
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>.+)$`)
6262

6363
type Argon2HashInput struct {
@@ -210,13 +210,29 @@ func ParseArgon2Hash(hash string) (*Argon2HashInput, error) {
210210
if err != nil {
211211
return nil, fmt.Errorf("crypto: argon2 hash has invalid base64 in the hash section %w", err)
212212
}
213+
if len(rawHash) == 0 {
214+
return nil, errors.New("crypto: argon2 hash is empty")
215+
}
213216

214217
salt, err := base64.RawStdEncoding.DecodeString(saltB64)
215218
if err != nil {
216219
return nil, fmt.Errorf("crypto: argon2 hash has invalid base64 in the salt section %w", err)
217220
}
221+
if len(salt) == 0 {
222+
return nil, errors.New("crypto: argon2 salt is empty")
223+
}
218224

219-
input := Argon2HashInput{alg, v, memory, time, threads, keyid, data, salt, rawHash}
225+
input := Argon2HashInput{
226+
alg: alg,
227+
v: v,
228+
memory: memory,
229+
time: time,
230+
threads: threads,
231+
keyid: keyid,
232+
data: data,
233+
salt: salt,
234+
rawHash: rawHash,
235+
}
220236

221237
return &input, nil
222238
}
@@ -250,13 +266,13 @@ func compareHashAndPasswordArgon2(ctx context.Context, hash, password string) er
250266

251267
switch input.alg {
252268
case "argon2i":
253-
derivedKey = argon2.Key([]byte(password), input.salt, uint32(input.time), uint32(input.memory)*1024, uint8(input.threads), uint32(len(input.rawHash))) // #nosec G115
269+
derivedKey = argon2.Key([]byte(password), input.salt, uint32(input.time), uint32(input.memory), uint8(input.threads), uint32(len(input.rawHash))) // #nosec G115
254270

255271
case "argon2id":
256-
derivedKey = argon2.IDKey([]byte(password), input.salt, uint32(input.time), uint32(input.memory)*1024, uint8(input.threads), uint32(len(input.rawHash))) // #nosec G115
272+
derivedKey = argon2.IDKey([]byte(password), input.salt, uint32(input.time), uint32(input.memory), uint8(input.threads), uint32(len(input.rawHash))) // #nosec G115
257273
}
258274

259-
match = subtle.ConstantTimeCompare(derivedKey, input.rawHash) == 0
275+
match = subtle.ConstantTimeCompare(derivedKey, input.rawHash) == 1
260276

261277
if !match {
262278
return ErrArgon2MismatchedHashAndPassword

internal/crypto/password_test.go

+4
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ func TestArgon2(t *testing.T) {
1919
for _, example := range examples {
2020
assert.NoError(t, CompareHashAndPassword(context.Background(), example, "test"))
2121
}
22+
23+
for _, example := range examples {
24+
assert.Error(t, CompareHashAndPassword(context.Background(), example, "test1"))
25+
}
2226
}
2327

2428
func TestGeneratePassword(t *testing.T) {

0 commit comments

Comments
 (0)