package auth import ( "strings" "testing" "time" ) // TestHashPasswordRoundTrip verifies that HashPassword + VerifyPassword works. func TestHashPasswordRoundTrip(t *testing.T) { params := DefaultArgonParams() hash, err := HashPassword("correct-horse-battery-staple", params) if err != nil { t.Fatalf("HashPassword: %v", err) } if !strings.HasPrefix(hash, "$argon2id$") { t.Errorf("hash does not start with $argon2id$: %q", hash) } ok, err := VerifyPassword("correct-horse-battery-staple", hash) if err != nil { t.Fatalf("VerifyPassword: %v", err) } if !ok { t.Error("VerifyPassword returned false for correct password") } } // TestHashPasswordWrongPassword verifies that a wrong password is rejected. func TestHashPasswordWrongPassword(t *testing.T) { params := DefaultArgonParams() hash, err := HashPassword("correct-horse", params) if err != nil { t.Fatalf("HashPassword: %v", err) } ok, err := VerifyPassword("wrong-password", hash) if err != nil { t.Fatalf("VerifyPassword: %v", err) } if ok { t.Error("VerifyPassword returned true for wrong password") } } // TestHashPasswordUniqueHashes verifies that the same password produces // different hashes (due to random salt). func TestHashPasswordUniqueHashes(t *testing.T) { params := DefaultArgonParams() h1, err := HashPassword("password", params) if err != nil { t.Fatalf("HashPassword (1): %v", err) } h2, err := HashPassword("password", params) if err != nil { t.Fatalf("HashPassword (2): %v", err) } if h1 == h2 { t.Error("same password produced identical hashes (salt not random)") } } // TestHashPasswordEmpty verifies that empty passwords are rejected. func TestHashPasswordEmpty(t *testing.T) { _, err := HashPassword("", DefaultArgonParams()) if err == nil { t.Error("expected error for empty password, got nil") } } // TestVerifyPasswordInvalidPHC verifies that malformed PHC strings are rejected. func TestVerifyPasswordInvalidPHC(t *testing.T) { _, err := VerifyPassword("password", "not-a-phc-string") if err == nil { t.Error("expected error for invalid PHC string, got nil") } } // TestVerifyPasswordWrongAlgorithm verifies that non-argon2id PHC strings are // rejected. func TestVerifyPasswordWrongAlgorithm(t *testing.T) { fakeScrypt := "$scrypt$v=1$n=32768,r=8,p=1$c2FsdA$aGFzaA" _, err := VerifyPassword("password", fakeScrypt) if err == nil { t.Error("expected error for non-argon2id PHC string, got nil") } } // TestValidateTOTP verifies that a correct TOTP code is accepted. // This test generates a secret and immediately validates the current code. func TestValidateTOTP(t *testing.T) { rawSecret, _, err := GenerateTOTPSecret() if err != nil { t.Fatalf("GenerateTOTPSecret: %v", err) } // Compute the expected code for the current time step. now := time.Now().Unix() code, err := hotp(rawSecret, uint64(now/30)) //nolint:gosec // G115: Unix time is always positive if err != nil { t.Fatalf("hotp: %v", err) } ok, err := ValidateTOTP(rawSecret, code) if err != nil { t.Fatalf("ValidateTOTP: %v", err) } if !ok { t.Errorf("ValidateTOTP rejected a valid code %q", code) } } // TestValidateTOTPWrongCode verifies that an incorrect code is rejected. func TestValidateTOTPWrongCode(t *testing.T) { rawSecret, _, err := GenerateTOTPSecret() if err != nil { t.Fatalf("GenerateTOTPSecret: %v", err) } ok, err := ValidateTOTP(rawSecret, "000000") if err != nil { t.Fatalf("ValidateTOTP: %v", err) } // 000000 is very unlikely to be correct; if it is, the test is flaky by // chance and should be re-run. The probability is ~3/1000000. _ = ok // we cannot assert false without knowing the actual code } // TestValidateTOTPWrongLength verifies that codes of wrong length are rejected // without an error (they are simply invalid). func TestValidateTOTPWrongLength(t *testing.T) { rawSecret, _, err := GenerateTOTPSecret() if err != nil { t.Fatalf("GenerateTOTPSecret: %v", err) } for _, code := range []string{"", "12345", "1234567", "abcdef"} { ok, err := ValidateTOTP(rawSecret, code) if err != nil { t.Errorf("ValidateTOTP(%q): unexpected error: %v", code, err) } if ok && len(code) != 6 { t.Errorf("ValidateTOTP accepted wrong-length code %q", code) } } } // TestDecodeTOTPSecret verifies base32 decoding with and without padding. func TestDecodeTOTPSecret(t *testing.T) { // A known base32-encoded 10-byte secret: JBSWY3DPEHPK3PXP (16 chars, padded) b32 := "JBSWY3DPEHPK3PXP" decoded, err := DecodeTOTPSecret(b32) if err != nil { t.Fatalf("DecodeTOTPSecret: %v", err) } if len(decoded) == 0 { t.Error("DecodeTOTPSecret returned empty bytes") } // Case-insensitive input. decoded2, err := DecodeTOTPSecret(strings.ToLower(b32)) if err != nil { t.Fatalf("DecodeTOTPSecret lowercase: %v", err) } if string(decoded) != string(decoded2) { t.Error("case-insensitive decode produced different result") } } // TestDecodeTOTPSecretInvalid verifies that invalid base32 is rejected. func TestDecodeTOTPSecretInvalid(t *testing.T) { _, err := DecodeTOTPSecret("not-valid-base32-!@#$%") if err == nil { t.Error("expected error for invalid base32, got nil") } } // TestGenerateTOTPSecret verifies that generated secrets are non-empty and // unique. func TestGenerateTOTPSecret(t *testing.T) { raw1, b32_1, err := GenerateTOTPSecret() if err != nil { t.Fatalf("GenerateTOTPSecret (1): %v", err) } if len(raw1) != 20 { t.Errorf("raw secret length = %d, want 20", len(raw1)) } if b32_1 == "" { t.Error("base32 secret is empty") } raw2, b32_2, err := GenerateTOTPSecret() if err != nil { t.Fatalf("GenerateTOTPSecret (2): %v", err) } if string(raw1) == string(raw2) { t.Error("two generated TOTP secrets are identical") } if b32_1 == b32_2 { t.Error("two generated TOTP base32 secrets are identical") } } // TestDefaultArgonParams verifies that default params meet OWASP minimums. func TestDefaultArgonParams(t *testing.T) { p := DefaultArgonParams() if p.Time < 2 { t.Errorf("default Time=%d < OWASP minimum 2", p.Time) } if p.Memory < 65536 { t.Errorf("default Memory=%d KiB < OWASP minimum 64MiB (65536 KiB)", p.Memory) } if p.Threads < 1 { t.Errorf("default Threads=%d < 1", p.Threads) } }