test(02): RED — failing argon2id password tests
- Six tests: HashVerify, VerifyWrong, VerifyMalformed, VerifyVersion, DistinctSaltsPerCall, DefaultParamsShape - Tests use auth.TestParams for fast wall time (Pitfall 4 guard) - Guards ErrInvalidHash + ErrIncompatibleVersion sentinel errors - All tests fail with undefined: auth.Hash / auth.TestParams / auth.Verify (implementation missing)
This commit is contained in:
parent
fb9aac30ba
commit
3bb3828cdc
1 changed files with 134 additions and 0 deletions
134
backend/internal/auth/password_test.go
Normal file
134
backend/internal/auth/password_test.go
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
package auth_test
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"backend/internal/auth"
|
||||
)
|
||||
|
||||
// TestPassword_HashVerify verifies that Hash returns a valid PHC string
|
||||
// starting with the expected argon2id prefix and that Verify confirms the
|
||||
// original password.
|
||||
func TestPassword_HashVerify(t *testing.T) {
|
||||
const pw = "correct-horse-battery-staple"
|
||||
phc, err := auth.Hash(pw, auth.TestParams)
|
||||
if err != nil {
|
||||
t.Fatalf("Hash returned error: %v", err)
|
||||
}
|
||||
if phc == "" {
|
||||
t.Fatal("Hash returned empty string")
|
||||
}
|
||||
|
||||
// TestParams: Memory=8192, Iterations=1, Parallelism=2
|
||||
const wantPrefix = "$argon2id$v=19$m=8192,t=1,p=2$"
|
||||
if !strings.HasPrefix(phc, wantPrefix) {
|
||||
t.Errorf("PHC string does not start with expected prefix\n got: %s\n want prefix: %s", phc, wantPrefix)
|
||||
}
|
||||
|
||||
ok, err := auth.Verify(phc, pw)
|
||||
if err != nil {
|
||||
t.Fatalf("Verify returned unexpected error: %v", err)
|
||||
}
|
||||
if !ok {
|
||||
t.Fatal("Verify returned false for the original password")
|
||||
}
|
||||
}
|
||||
|
||||
// TestPassword_VerifyWrong ensures that Verify returns (false, nil) — not an
|
||||
// error — when the supplied password does not match the hash.
|
||||
func TestPassword_VerifyWrong(t *testing.T) {
|
||||
const pw = "correct-horse-battery-staple"
|
||||
phc, err := auth.Hash(pw, auth.TestParams)
|
||||
if err != nil {
|
||||
t.Fatalf("Hash returned error: %v", err)
|
||||
}
|
||||
|
||||
ok, err := auth.Verify(phc, pw+"x")
|
||||
if err != nil {
|
||||
t.Fatalf("Verify returned an unexpected error on wrong password: %v", err)
|
||||
}
|
||||
if ok {
|
||||
t.Fatal("Verify returned true for a wrong password")
|
||||
}
|
||||
}
|
||||
|
||||
// TestPassword_VerifyMalformed checks that malformed or foreign-algorithm PHC
|
||||
// strings return (false, ErrInvalidHash).
|
||||
func TestPassword_VerifyMalformed(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
encoded string
|
||||
}{
|
||||
{"plain string", "not-a-phc-string"},
|
||||
{"too few segments", "$argon2id$v=19$m=8192,t=1,p=2$only-three-segments"},
|
||||
{"wrong algorithm", "$bcrypt$v=19$m=8192,t=1,p=2$saltb64$hashb64"},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
ok, err := auth.Verify(tc.encoded, "anypassword")
|
||||
if ok {
|
||||
t.Error("Verify returned true for malformed input")
|
||||
}
|
||||
if err != auth.ErrInvalidHash {
|
||||
t.Errorf("Verify error = %v, want ErrInvalidHash", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestPassword_VerifyVersion ensures that a synthetic PHC string with v=18
|
||||
// (one less than argon2.Version == 19) returns (false, ErrIncompatibleVersion).
|
||||
func TestPassword_VerifyVersion(t *testing.T) {
|
||||
// Craft a syntactically valid PHC string with an old version number.
|
||||
// The segment count must be correct (6 after split on "$"):
|
||||
// $argon2id$v=18$m=8192,t=1,p=2$<validb64salt>$<validb64hash>
|
||||
synthetic := "$argon2id$v=18$m=8192,t=1,p=2$c29tZXNhbHQxNjBiYXNl$c29tZWhhc2gzMmJ5dGVzMDAwMDAwMDAwMDAwMDA"
|
||||
|
||||
ok, err := auth.Verify(synthetic, "anypassword")
|
||||
if ok {
|
||||
t.Error("Verify returned true for incompatible version")
|
||||
}
|
||||
if err != auth.ErrIncompatibleVersion {
|
||||
t.Errorf("Verify error = %v, want ErrIncompatibleVersion", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestPassword_DistinctSaltsPerCall ensures that two Hash calls for the same
|
||||
// password produce different PHC strings (random salt per call — Pitfall guard).
|
||||
func TestPassword_DistinctSaltsPerCall(t *testing.T) {
|
||||
const pw = "correct-horse-battery-staple"
|
||||
phc1, err := auth.Hash(pw, auth.TestParams)
|
||||
if err != nil {
|
||||
t.Fatalf("first Hash returned error: %v", err)
|
||||
}
|
||||
phc2, err := auth.Hash(pw, auth.TestParams)
|
||||
if err != nil {
|
||||
t.Fatalf("second Hash returned error: %v", err)
|
||||
}
|
||||
if phc1 == phc2 {
|
||||
t.Error("Two Hash calls for the same password returned identical PHC strings (salt reuse detected)")
|
||||
}
|
||||
}
|
||||
|
||||
// TestPassword_DefaultParamsShape guards against accidental drift in DefaultParams.
|
||||
// Values must match OWASP 2024 baseline as locked in D-08.
|
||||
func TestPassword_DefaultParamsShape(t *testing.T) {
|
||||
p := auth.DefaultParams
|
||||
if p.Memory != 64*1024 {
|
||||
t.Errorf("DefaultParams.Memory = %d, want %d", p.Memory, 64*1024)
|
||||
}
|
||||
if p.Iterations != 1 {
|
||||
t.Errorf("DefaultParams.Iterations = %d, want 1", p.Iterations)
|
||||
}
|
||||
if p.Parallelism != 4 {
|
||||
t.Errorf("DefaultParams.Parallelism = %d, want 4", p.Parallelism)
|
||||
}
|
||||
if p.SaltLength != 16 {
|
||||
t.Errorf("DefaultParams.SaltLength = %d, want 16", p.SaltLength)
|
||||
}
|
||||
if p.KeyLength != 32 {
|
||||
t.Errorf("DefaultParams.KeyLength = %d, want 32", p.KeyLength)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue