Files
hotp_calc/internal/otp/seed_test.go
T

118 lines
3.8 KiB
Go

package otp
import (
"encoding/hex"
"strings"
"testing"
"time"
)
// Seed из примера задачи: eToken PASS, 20 байт в hex.
const exampleSeed = "765D7268D012090C28172A729BC9CC1AD82B382D"
func TestParseSeed_Hex(t *testing.T) {
for _, in := range []string{
exampleSeed,
strings.ToLower(exampleSeed),
"765D 7268 D012 090C 2817 2A72 9BC9 CC1A D82B 382D",
"76-5D-72-68-D0-12-09-0C-28-17-2A-72-9B-C9-CC-1A-D8-2B-38-2D",
} {
s, err := ParseSeed(in, FormatAuto)
if err != nil {
t.Fatalf("ParseSeed(%q): %v", in, err)
}
if s.Format != FormatHex {
t.Errorf("ParseSeed(%q): формат %s, ожидался hex", in, s.Format)
}
if got := strings.ToUpper(hex.EncodeToString(s.Secret)); got != exampleSeed {
t.Errorf("ParseSeed(%q) = %s", in, got)
}
}
}
func TestParseSeed_Base32(t *testing.T) {
// Base32 от ASCII-ключа "12345678901234567890" из RFC 4226.
const b32 = "GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ"
s, err := ParseSeed(b32, FormatAuto)
if err != nil {
t.Fatalf("ParseSeed: %v", err)
}
if s.Format != FormatBase32 {
t.Errorf("формат %s, ожидался base32", s.Format)
}
if string(s.Secret) != string(rfc4226Key) {
t.Errorf("secret = %q, ожидалось %q", s.Secret, rfc4226Key)
}
// Без padding и в нижнем регистре тоже должно работать.
if s2, err := ParseSeed("gezdgnbvgy3tqojq", FormatAuto); err != nil || len(s2.Secret) != 10 {
t.Errorf("Base32 без padding: %v, %d байт", err, len(s2.Secret))
}
}
func TestParseSeed_ForcedFormat(t *testing.T) {
// "12345678" — валидный hex и валидный набор символов не для Base32.
if _, err := ParseSeed(exampleSeed, FormatBase32); err == nil {
t.Error("hex-строка с цифрами 0/1 не должна разбираться как Base32")
}
if s, err := ParseSeed(exampleSeed, FormatHex); err != nil || len(s.Secret) != 20 {
t.Errorf("принудительный hex: %v", err)
}
}
func TestParseSeed_Errors(t *testing.T) {
for _, in := range []string{"", " ", "zzzz!!!", "765D7268D"} {
if _, err := ParseSeed(in, FormatAuto); err == nil {
t.Errorf("ParseSeed(%q) должен возвращать ошибку", in)
}
}
}
func TestParseSeed_OtpauthURI(t *testing.T) {
uri := "otpauth://totp/ACME:user@example.com?secret=GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ" +
"&issuer=ACME&digits=8&algorithm=SHA256&period=60"
s, err := ParseSeed(uri, FormatAuto)
if err != nil {
t.Fatalf("ParseSeed: %v", err)
}
if !s.FromURI || !s.TOTP {
t.Error("URI должен распознаваться как TOTP")
}
if s.Digits == nil || *s.Digits != 8 {
t.Error("digits из URI не подхвачен")
}
if s.Alg == nil || *s.Alg != SHA256 {
t.Error("algorithm из URI не подхвачен")
}
if s.Step == nil || *s.Step != 60*time.Second {
t.Error("period из URI не подхвачен")
}
if s.Label != "ACME:user@example.com" {
t.Errorf("label = %q", s.Label)
}
hotpURI := "otpauth://hotp/tok?secret=GEZDGNBVGY3TQOJQ&counter=42"
h, err := ParseSeed(hotpURI, FormatAuto)
if err != nil {
t.Fatalf("ParseSeed(hotp): %v", err)
}
if h.TOTP {
t.Error("hotp URI не должен помечаться как TOTP")
}
if h.Counter == nil || *h.Counter != 42 {
t.Error("counter из URI не подхвачен")
}
if _, err := ParseSeed("otpauth://totp/x?issuer=ACME", FormatAuto); err == nil {
t.Error("URI без secret должен отвергаться")
}
}
func TestFingerprint(t *testing.T) {
s, _ := ParseSeed(exampleSeed, FormatAuto)
if got := Fingerprint(s.Secret); got != "765D7268…382D" {
t.Errorf("Fingerprint = %q", got)
}
if got := Fingerprint([]byte{0x01, 0x02}); got != "0102" {
t.Errorf("короткий Fingerprint = %q", got)
}
}