xtablo-source/backend/internal/auth/password_test.go
Arthur Belleville 3bb3828cdc
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)
2026-05-14 21:59:38 +02:00

134 lines
4.2 KiB
Go

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)
}
}