191 lines
6.5 KiB
Go
191 lines
6.5 KiB
Go
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)
|
||
}
|
||
}
|