Test / test (push) Successful in 7m5s
Release / gates (push) Successful in 7m28s
Release / build (amd64, freebsd) (push) Successful in 2m52s
Release / build (amd64, linux) (push) Successful in 2m46s
Release / build (arm64, freebsd) (push) Successful in 2m22s
Release / build (arm64, linux) (push) Successful in 2m38s
Release / build (loong64, linux) (push) Successful in 2m7s
Release / build (riscv64, linux) (push) Successful in 2m17s
Release / release (push) Successful in 1m0s
Assisted-by: GLM 5.3
100 lines
2.8 KiB
Go
100 lines
2.8 KiB
Go
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (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)
|
|
}
|
|
}
|
|
}
|