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) } }