Files

191 lines
6.5 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package initfile
import (
"bytes"
"encoding/hex"
"strings"
"testing"
"time"
"hotpcalc/internal/otp"
"hotpcalc/internal/store"
)
const testSeedHex = "765D7268D012090C28172A729BC9CC1AD82B382D"
func testToken(t *testing.T) store.Token {
t.Helper()
secret, err := hex.DecodeString(testSeedHex)
if err != nil {
t.Fatal(err)
}
return store.Token{
Serial: "0004000ABCDEF1", Type: store.TypeHOTP, Secret: secret,
Digits: 6, Alg: otp.SHA1, Counter: 1,
}
}
// Эталон формата: структура сверена с parseSafeNetXML из privacyIDEA.
const golden = `<?xml version="1.0" encoding="UTF-8"?>
<Tokens>
<Token serial="0004000ABCDEF1">
<CaseModel>5</CaseModel>
<Model>101</Model>
<ProductionDate>09/04/2026</ProductionDate>
<ProductName>eToken PASS</ProductName>
<Applications>
<Application ConnectorID="{ab1397d2-ddb6-4705-b66e-9f83f322deb9}">
<Seed>765D7268D012090C28172A729BC9CC1AD82B382D</Seed>
<MovingFactor>1</MovingFactor>
</Application>
</Applications>
</Token>
</Tokens>
`
func TestWriteSafeNetMatchesGolden(t *testing.T) {
var buf bytes.Buffer
opts := Options{ProductionDate: time.Date(2026, 9, 4, 0, 0, 0, 0, time.UTC)}
if err := WriteSafeNet(&buf, []store.Token{testToken(t)}, opts); err != nil {
t.Fatalf("WriteSafeNet: %v", err)
}
if buf.String() != golden {
t.Errorf("вывод не совпал с эталоном:\n--- получено ---\n%s\n--- ожидалось ---\n%s",
buf.String(), golden)
}
}
func TestRoundTrip(t *testing.T) {
want := []store.Token{testToken(t)}
want = append(want, store.Token{
Serial: "0004000ABCDEF2", Type: store.TypeHOTP, Secret: want[0].Secret,
Digits: 6, Alg: otp.SHA1, Counter: 123456,
})
var buf bytes.Buffer
if err := WriteSafeNet(&buf, want, Options{}); err != nil {
t.Fatal(err)
}
got, err := ReadSafeNet(&buf)
if err != nil {
t.Fatalf("ReadSafeNet: %v", err)
}
if len(got) != len(want) {
t.Fatalf("прочитано %d токенов, ожидалось %d", len(got), len(want))
}
for i := range want {
if got[i].Serial != want[i].Serial || got[i].Counter != want[i].Counter ||
!bytes.Equal(got[i].Secret, want[i].Secret) || got[i].Alg != want[i].Alg ||
got[i].Digits != want[i].Digits || got[i].Type != want[i].Type {
t.Errorf("токен %d: получено %+v, ожидалось %+v", i, got[i], want[i])
}
}
}
func TestReadSafeNetSample(t *testing.T) {
// Образец в том виде, в каком формат описан у privacyIDEA/LinOTP.
const sample = `<?xml version="1.0" encoding="utf-8"?>
<Tokens>
<Token serial="000400081DFFA5">
<CaseModel>5</CaseModel>
<Model>101</Model>
<ProductionDate>02/19/2009</ProductionDate>
<ProductName>Safeword Alpine</ProductName>
<Applications>
<Application ConnectorID="{ab1397d2-ddb6-4705-b66e-9f83f322deb9}">
<Seed>3132333435363738393031323334353637383930</Seed>
<MovingFactor>1</MovingFactor>
</Application>
</Applications>
</Token>
</Tokens>`
got, err := ReadSafeNet(strings.NewReader(sample))
if err != nil {
t.Fatalf("ReadSafeNet: %v", err)
}
if len(got) != 1 {
t.Fatalf("прочитано %d токенов", len(got))
}
tok := got[0]
if tok.Serial != "000400081DFFA5" || tok.Counter != 1 || tok.Label != "Safeword Alpine" {
t.Errorf("получено %+v", tok)
}
// Seed из RFC 4226: счётчик 0 даёт 755224.
if code := tok.Config().HOTP(0); code != "755224" {
t.Errorf("HOTP(0) = %s, ожидалось 755224 — seed прочитан неверно", code)
}
}
func TestReadSafeNetAlgorithmFromSeedLength(t *testing.T) {
long := strings.Repeat("AB", 32) // 64 hex-символа
x := `<Tokens><Token serial="X"><Applications><Application>` +
`<Seed>` + long + `</Seed><MovingFactor>0</MovingFactor>` +
`</Application></Applications></Token></Tokens>`
got, err := ReadSafeNet(strings.NewReader(x))
if err != nil {
t.Fatal(err)
}
if got[0].Alg != otp.SHA256 {
t.Errorf("для 64-символьного seed ожидался SHA256, получено %s", got[0].Alg)
}
}
func TestReadSafeNetErrors(t *testing.T) {
cases := map[string]string{
"не XML": "просто текст",
"чужой корень": `<KeyContainer/>`,
"нет токенов": `<Tokens></Tokens>`,
"нет serial": `<Tokens><Token><Applications><Application><Seed>AA</Seed></Application></Applications></Token></Tokens>`,
"нет Application": `<Tokens><Token serial="X"><Applications></Applications></Token></Tokens>`,
"нет Seed": `<Tokens><Token serial="X"><Applications><Application><MovingFactor>0</MovingFactor></Application></Applications></Token></Tokens>`,
"Seed не hex": `<Tokens><Token serial="X"><Applications><Application><Seed>ZZZZ</Seed></Application></Applications></Token></Tokens>`,
"MovingFactor не чис": `<Tokens><Token serial="X"><Applications><Application><Seed>` + testSeedHex + `</Seed><MovingFactor>abc</MovingFactor></Application></Applications></Token></Tokens>`,
}
for name, in := range cases {
if _, err := ReadSafeNet(strings.NewReader(in)); err == nil {
t.Errorf("%s: ожидалась ошибка", name)
}
}
}
func TestWriteSafeNetRejectsEmpty(t *testing.T) {
var buf bytes.Buffer
if err := WriteSafeNet(&buf, nil, Options{}); err == nil {
t.Error("пустой список должен отвергаться")
}
}
func TestWarnings(t *testing.T) {
ok := testToken(t)
if w := Warnings(ok); len(w) != 0 {
t.Errorf("для обычного HOTP-токена предупреждений быть не должно: %v", w)
}
totp := ok
totp.Type = store.TypeTOTP
if w := Warnings(totp); len(w) != 1 || !strings.Contains(w[0], "только HOTP") {
t.Errorf("TOTP: %v", w)
}
eight := ok
eight.Digits = 8
if w := Warnings(eight); len(w) != 1 || !strings.Contains(w[0], "разрядность") {
t.Errorf("8 разрядов: %v", w)
}
// SHA256 при 20-байтном seed сервер прочитает как SHA1.
sha := ok
sha.Alg = otp.SHA256
if w := Warnings(sha); len(w) != 1 || !strings.Contains(w[0], "алгоритм") {
t.Errorf("SHA256 с коротким seed: %v", w)
}
// SHA256 при 32-байтном seed передаётся корректно.
good := ok
good.Alg = otp.SHA256
good.Secret = bytes.Repeat([]byte{1}, 32)
if w := Warnings(good); len(w) != 0 {
t.Errorf("SHA256 с 32-байтным seed не должен давать предупреждений: %v", w)
}
}