Fix F-07: pre-compute real Argon2 dummy hash via sync.Once
- auth/auth.go: add DummyHash() which uses sync.Once to compute
HashPassword("dummy-password-for-timing-only", DefaultArgonParams())
on first call; subsequent calls return the cached PHC string;
add sync to imports
- auth/auth_test.go: TestDummyHashIsValidPHC verifies the hash
parses and verifies correctly; TestDummyHashIsCached verifies
sync.Once behaviour; TestDummyHashMatchesDefaultParams verifies
embedded m/t/p match DefaultArgonParams()
- server/server.go, grpcserver/auth.go, ui/ui.go: replace five
hardcoded PHC strings with auth.DummyHash() calls
- AUDIT.md: mark F-07 as fixed
Security: the previous hardcoded hash used a 6-byte salt and
6-byte output ("testsalt"/"testhash" in base64), which Argon2id
verifies faster than a real 16-byte-salt / 32-byte-output hash.
This timing gap was measurable and could aid user enumeration.
auth.DummyHash() uses identical parameters and full-length salt
and output, so dummy verification timing matches real timing
exactly, regardless of future parameter changes.
This commit is contained in:
@@ -214,3 +214,51 @@ func TestDefaultArgonParams(t *testing.T) {
|
||||
t.Errorf("default Threads=%d < 1", p.Threads)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDummyHashIsValidPHC verifies DummyHash returns a non-empty string that
|
||||
// is a valid Argon2id PHC hash verifiable by VerifyPassword.
|
||||
func TestDummyHashIsValidPHC(t *testing.T) {
|
||||
h := DummyHash()
|
||||
if h == "" {
|
||||
t.Fatal("DummyHash returned empty string")
|
||||
}
|
||||
// Must parse as a valid PHC string with OWASP-compatible parameters.
|
||||
// VerifyPassword with the correct password must succeed.
|
||||
ok, err := VerifyPassword("dummy-password-for-timing-only", h)
|
||||
if err != nil {
|
||||
t.Fatalf("VerifyPassword against DummyHash: %v", err)
|
||||
}
|
||||
if !ok {
|
||||
t.Error("VerifyPassword against DummyHash returned false (hash mismatch)")
|
||||
}
|
||||
}
|
||||
|
||||
// TestDummyHashIsCached verifies that consecutive calls return the same string
|
||||
// (sync.Once caching).
|
||||
func TestDummyHashIsCached(t *testing.T) {
|
||||
h1 := DummyHash()
|
||||
h2 := DummyHash()
|
||||
if h1 != h2 {
|
||||
t.Errorf("DummyHash returned different values on consecutive calls:\n first: %q\n second: %q", h1, h2)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDummyHashMatchesDefaultParams verifies the embedded parameters in the
|
||||
// returned PHC string match DefaultArgonParams (m, t, p).
|
||||
func TestDummyHashMatchesDefaultParams(t *testing.T) {
|
||||
h := DummyHash()
|
||||
params, _, _, err := parsePHC(h)
|
||||
if err != nil {
|
||||
t.Fatalf("parsePHC(DummyHash()): %v", err)
|
||||
}
|
||||
def := DefaultArgonParams()
|
||||
if params.Memory != def.Memory {
|
||||
t.Errorf("Memory = %d, want %d", params.Memory, def.Memory)
|
||||
}
|
||||
if params.Time != def.Time {
|
||||
t.Errorf("Time = %d, want %d", params.Time, def.Time)
|
||||
}
|
||||
if params.Threads != def.Threads {
|
||||
t.Errorf("Threads = %d, want %d", params.Threads, def.Threads)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user