Initial commit: hotpcalc CLI

This commit is contained in:
2026-09-20 20:04:18 +03:00
commit 8f42dc690e
24 changed files with 5182 additions and 0 deletions
+108
View File
@@ -0,0 +1,108 @@
package otp
import (
"testing"
"time"
)
// RFC 4226, Appendix D: ключ ASCII "12345678901234567890".
var rfc4226Key = []byte("12345678901234567890")
func TestHOTP_RFC4226(t *testing.T) {
want := []string{
"755224", "287082", "359152", "969429", "338314",
"254676", "287922", "162583", "399871", "520489",
}
c := Config{Secret: rfc4226Key, Digits: 6, Alg: SHA1, Step: DefaultStep}
for i, w := range want {
if got := c.HOTP(uint64(i)); got != w {
t.Errorf("HOTP(%d) = %s, ожидалось %s", i, got, w)
}
}
}
// RFC 6238, Appendix B. Ключи для SHA256/SHA512 — продолжение той же
// последовательности "12345678901234567890..." до размера блока хеша.
func seedRepeat(n int) []byte {
base := "12345678901234567890"
out := make([]byte, 0, n)
for len(out) < n {
out = append(out, base...)
}
return out[:n]
}
func TestTOTP_RFC6238(t *testing.T) {
cases := []struct {
unix int64
alg Algorithm
key []byte
want string
}{
{59, SHA1, seedRepeat(20), "94287082"},
{59, SHA256, seedRepeat(32), "46119246"},
{59, SHA512, seedRepeat(64), "90693936"},
{1111111109, SHA1, seedRepeat(20), "07081804"},
{1111111111, SHA1, seedRepeat(20), "14050471"},
{1234567890, SHA1, seedRepeat(20), "89005924"},
{2000000000, SHA1, seedRepeat(20), "69279037"},
{20000000000, SHA1, seedRepeat(20), "65353130"},
{1111111109, SHA256, seedRepeat(32), "68084774"},
{20000000000, SHA256, seedRepeat(32), "77737706"},
{1111111109, SHA512, seedRepeat(64), "25091201"},
{20000000000, SHA512, seedRepeat(64), "47863826"},
}
for _, tc := range cases {
c := Config{Secret: tc.key, Digits: 8, Alg: tc.alg, Step: DefaultStep}
got := c.TOTP(time.Unix(tc.unix, 0).UTC())
if got != tc.want {
t.Errorf("TOTP(%d, %s) = %s, ожидалось %s", tc.unix, tc.alg, got, tc.want)
}
}
}
func TestHOTP_LeadingZeros(t *testing.T) {
c := Config{Secret: rfc4226Key, Digits: 6, Alg: SHA1, Step: DefaultStep}
for i := uint64(0); i < 5000; i++ {
if got := c.HOTP(i); len(got) != 6 {
t.Fatalf("HOTP(%d) = %q: длина %d вместо 6", i, got, len(got))
}
}
}
func TestTimeStepAndStepStart(t *testing.T) {
c := Config{Secret: rfc4226Key, Digits: 6, Alg: SHA1, Step: DefaultStep}
if got := c.TimeStep(time.Unix(59, 0)); got != 1 {
t.Errorf("TimeStep(59) = %d, ожидалось 1", got)
}
if got := c.StepStart(1).Unix(); got != 30 {
t.Errorf("StepStart(1) = %d, ожидалось 30", got)
}
c60 := Config{Secret: rfc4226Key, Digits: 6, Alg: SHA1, Step: 60 * time.Second}
if got := c60.TimeStep(time.Unix(119, 0)); got != 1 {
t.Errorf("TimeStep(119, шаг 60с) = %d, ожидалось 1", got)
}
}
func TestValidate(t *testing.T) {
if err := (Config{Digits: 6, Step: DefaultStep}).Validate(); err == nil {
t.Error("пустой seed должен отвергаться")
}
if err := (Config{Secret: rfc4226Key, Digits: 4, Step: DefaultStep}).Validate(); err == nil {
t.Error("разрядность 4 должна отвергаться")
}
if err := (Config{Secret: rfc4226Key, Digits: 6}).Validate(); err == nil {
t.Error("нулевой шаг должен отвергаться")
}
}
func TestParseAlgorithm(t *testing.T) {
for _, s := range []string{"sha1", "SHA-1", ""} {
if a, err := ParseAlgorithm(s); err != nil || a != SHA1 {
t.Errorf("ParseAlgorithm(%q) = %v, %v", s, a, err)
}
}
if _, err := ParseAlgorithm("md5"); err == nil {
t.Error("md5 должен отвергаться")
}
}