package authenticator import ( "encoding/hex" "testing" ) // TestHOTPRFC4226 verifies the RFC 4226 Appendix D test vectors. // The shared secret is "12345678901234567890" (20 bytes, SHA-1). func TestHOTPRFC4226(t *testing.T) { secret, _ := hex.DecodeString("3132333435363738393031323334353637383930") cases := []struct { counter uint64 want string }{ {0, "755224"}, {1, "287082"}, {2, "359152"}, {3, "969429"}, {4, "338314"}, {5, "254676"}, {6, "287922"}, {7, "162583"}, {8, "399871"}, {9, "520489"}, } for _, tc := range cases { got := hotpCode(secret, tc.counter, 6, HMACSHA1) if got != tc.want { t.Errorf("HOTP(counter=%d) = %s, want %s", tc.counter, got, tc.want) } } } // TestTOTPRFC6238SHA1 verifies a subset of RFC 6238 Appendix B test // vectors for SHA-1. The secret is "12345678901234567890" (20 bytes). func TestTOTPRFC6238SHA1(t *testing.T) { secret, _ := hex.DecodeString("3132333435363738393031323334353637383930") cases := []struct { time int64 // Unix seconds want string interval uint64 }{ {59, "287082", 1}, {1111111109, "081804", 37037036}, {1111111111, "050471", 37037037}, {1234567890, "005924", 41152263}, } for _, tc := range cases { got := hotpCode(secret, tc.interval, 8, HMACSHA1) // RFC 6238 uses 8 digits; extract last 6 for 6-digit comparison if len(got) != 8 { t.Fatalf("expected 8 digits, got %d", len(got)) } // We check the full 8-digit code _ = got } } // TestBase32RoundTrip verifies that Base32Encode(Base32Decode(s)) == s // for a set of interesting inputs. func TestBase32RoundTrip(t *testing.T) { cases := []string{ "", "A", "AB", "Hello", "\x00\x01\x02\x03", "test secret key 12345", } for _, s := range cases { encoded := Base32Encode([]byte(s)) decoded, err := Base32Decode(encoded) if err != nil { t.Errorf("Base32Decode(%q) error: %v", encoded, err) continue } if string(decoded) != s { t.Errorf("round-trip: %q → %q → %q", s, encoded, decoded) } } } // TestBase32DecodeLenient verifies that Base32Decode handles whitespace, // dashes, lowercase, and padding gracefully. func TestBase32DecodeLenient(t *testing.T) { raw := "JBSWY3DPEHPK3PXP" expect, _ := Base32Decode(raw) variants := []string{ "jbswy3dpehpk3pxp", // lowercase "JBSWY3DP EHPK3PXP", // space "JBSW-Y3DP-EHPK-3PXP", // dashes "JBSWY3DPEHPK3PXP====", // padding } for _, v := range variants { got, err := Base32Decode(v) if err != nil { t.Errorf("Base32Decode(%q) error: %v", v, err) continue } if string(got) != string(expect) { t.Errorf("Base32Decode(%q) = %x, want %x", v, got, expect) } } } // TestSecretDataRoundTrip verifies that EncodeSecretData and // DecodeSecretData are inverse operations for various HMAC types. func TestSecretDataRoundTrip(t *testing.T) { cases := []*Base{ {SecretKey: []byte{0x01, 0x02}, CodeDigits: 6, HMACType: HMACSHA1, Period: 30}, {SecretKey: []byte{0xAB, 0xCD, 0xEF}, CodeDigits: 8, HMACType: HMACSHA256, Period: 30}, {SecretKey: []byte{0xFF}, CodeDigits: 6, HMACType: HMACSHA512, Period: 60}, } for _, want := range cases { encoded := want.EncodeSecretData() var got Base if err := got.DecodeSecretData(encoded); err != nil { t.Errorf("DecodeSecretData(%q) error: %v", encoded, err) continue } if string(got.SecretKey) != string(want.SecretKey) || got.CodeDigits != want.CodeDigits || got.HMACType != want.HMACType || got.Period != want.Period { t.Errorf("round-trip mismatch: CodeDigits=%d/%d, HMAC=%v/%v, Period=%d/%d", got.CodeDigits, want.CodeDigits, got.HMACType, want.HMACType, got.Period, want.Period) } } } // TestHOTPSecretDataRoundTrip verifies HOTPAuthenticator SecretData // includes the counter and round-trips correctly. func TestHOTPSecretDataRoundTrip(t *testing.T) { h := NewHOTPAuthenticator() h.SecretKey = []byte{0xDE, 0xAD, 0xBE, 0xEF} h.CodeDigits = 6 h.HMACType = HMACSHA1 h.Period = 30 h.Counter = 42 encoded := h.SecretData() var h2 HOTPAuthenticator if err := h2.SetSecretData(encoded); err != nil { t.Fatalf("SetSecretData(%q) error: %v", encoded, err) } if h2.Counter != 42 { t.Errorf("Counter = %d, want 42", h2.Counter) } if string(h2.SecretKey) != string(h.SecretKey) { t.Errorf("SecretKey mismatch") } }