// Copyright (c) 2026 Petr BalvĂ­n (https://petrbalvin.org) // SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 package totp import ( "testing" "time" ) // rfcSecret is the RFC 6238 appendix B seed for SHA-1. var rfcSecret = []byte("12345678901234567890") func at(unix int64) time.Time { return time.Unix(unix, 0).UTC() } // TestRFCVectors checks the truncation against the appendix B table, // reduced to the six digits the URI format promises. func TestRFCVectors(t *testing.T) { vectors := []struct { unix int64 code string }{ {59, "287082"}, {1111111109, "081804"}, {1111111111, "050471"}, {1234567890, "005924"}, {2000000000, "279037"}, {20000000000, "353130"}, } for _, v := range vectors { if got := Code(rfcSecret, at(v.unix)); got != v.code { t.Fatalf("Code(%d) = %q, want %q", v.unix, got, v.code) } } } func TestValidateAcceptsWindow(t *testing.T) { codeTime := at(1_000_000_000) code := Code(rfcSecret, codeTime) for _, drift := range []time.Duration{-Step, 0, Step} { ok, step := Validate(rfcSecret, code, codeTime.Add(drift), 0) if !ok { t.Fatalf("drift %v rejected", drift) } if step != counter(codeTime) { t.Fatalf("drift %v: step = %d, want the code's counter %d", drift, step, counter(codeTime)) } } } func TestValidateRefusesOutsideWindow(t *testing.T) { code := Code(rfcSecret, at(1_000_000_000)) if ok, _ := Validate(rfcSecret, code, at(1_000_000_000).Add(2*Step), 0); ok { t.Fatal("two steps ahead accepted") } if ok, _ := Validate(rfcSecret, code, at(1_000_000_000).Add(-2*Step), 0); ok { t.Fatal("two steps behind accepted") } } func TestValidateGuardsReplay(t *testing.T) { now := at(1_000_000_000) code := Code(rfcSecret, now) ok, step := Validate(rfcSecret, code, now, 0) if !ok || step == 0 { t.Fatalf("first use failed: ok=%v step=%d", ok, step) } if ok, _ := Validate(rfcSecret, code, now, step); ok { t.Fatal("same counter accepted twice") } if ok, _ := Validate(rfcSecret, code, now.Add(Step), step); ok { t.Fatal("older drifting code accepted after a newer one") } // The next step's code stays valid: the floor is the counter, not // a blanket cooldown. next := Code(rfcSecret, now.Add(2*Step)) if ok, newer := Validate(rfcSecret, next, now.Add(2*Step), step); !ok || newer <= step { t.Fatalf("fresh code refused: ok=%v step=%d", ok, newer) } } func TestNormalise(t *testing.T) { for in, want := range map[string]string{ "123456": "123456", " 123 456 ": "123456", "123-456": "123456", "004203": "004203", } { if got := normalise(in); got != want { t.Fatalf("normalise(%q) = %q, want %q", in, got, want) } } for _, in := range []string{"", "12345", "1234567", "12345a", "12 34 56 78", "12345\n"} { if got := normalise(in); got != "" { t.Fatalf("normalise(%q) = %q, want empty", in, got) } } }