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
+353
View File
@@ -0,0 +1,353 @@
# hotpcalc
Инструмент для работы с токенами **SafeNet eToken PASS** — HOTP (RFC 4226)
и TOTP (RFC 6238). Умеет три вещи:
1. **Калькулятор.** По вектору инициализации (seed) и **двум последовательным**
значениям, снятым с токена, определяет его текущую позицию (счётчик для HOTP,
временной шаг для TOTP) и рассчитывает следующие N значений.
2. **Программный токен.** Хранит seed и счётчик в ini-файле и по команде выдаёт
следующее значение — как физический брелок.
3. **Выпуск токенов.** Генерирует новый токен и выгружает файл инициализации
в XML-формате SafeNet/Aladdin для импорта на сервер аутентификации.
Написан на Go без внешних зависимостей; собирается в один статический бинарник
под Linux, macOS и Windows (amd64 и arm64).
## Сборка
```sh
make build # локальный бинарник ./hotpcalc
make test # тесты, включая контрольные векторы RFC 4226 и RFC 6238
make dist # бинарники под linux/macos/windows, amd64/arm64 в каталоге dist/
```
Требуется Go 1.26 или новее. Готовые бинарники в `dist/` ни от чего не зависят —
достаточно скопировать нужный файл и сделать его исполняемым:
```sh
chmod +x hotpcalc-linux-amd64
./hotpcalc-linux-amd64 -version
```
На macOS файл, скачанный через браузер, помечается карантином; снять метку:
```sh
xattr -d com.apple.quarantine hotpcalc-darwin-arm64
```
В консоли Windows перед запуском может понадобиться переключение на UTF-8,
иначе русский текст выведется нечитаемо (в Windows Terminal это не нужно):
```
chcp 65001
hotpcalc-windows-amd64.exe -seed 765D... -otp1 941595 -otp2 057938
```
## Команды
```
hotpcalc [флаги] [seed] [код1] [код2] калькулятор (режим по умолчанию)
hotpcalc new выпустить новый токен: seed, серийник, запись в ini, выгрузка XML
hotpcalc add добавить в ini уже известный токен по его seed
hotpcalc import загрузить токены из XML-файла инициализации
hotpcalc list список токенов в хранилище
hotpcalc show параметры и состояние токена (счётчик не двигается)
hotpcalc next выдать следующее значение — «нажатие кнопки»
hotpcalc token интерактивный режим: Enter = нажатие кнопки
hotpcalc sync найти позицию по двум кодам и записать её в хранилище
hotpcalc set изменить параметры токена
hotpcalc export выгрузить XML-файл инициализации
hotpcalc remove удалить токен из хранилища
hotpcalc calc явный вызов калькулятора
```
Справка по любой команде — `hotpcalc <команда> -h`.
## Калькулятор
```
hotpcalc [флаги] [seed] [код1] [код2]
```
Самый короткий вызов — три позиционных аргумента:
```sh
./hotpcalc 765D7268D012090C28172A729BC9CC1AD82B382D 941595 057938
```
```
Seed: 765D7268…382D (20 байт, hex)
Режим: HOTP, SHA1, 6 разрядов
Позиция: код 941595 → счётчик 41, код 057938 → счётчик 42
Следующие значения (10):
# Счётчик Код
1 43 937300
2 44 991694
3 45 146545
...
```
Без аргументов программа спрашивает seed и коды в диалоге:
```sh
./hotpcalc
Введите seed (hex или Base32): ...
Введите первый код с токена: ...
Введите второй (следующий) код: ...
```
### Флаги
| Флаг | Назначение | По умолчанию |
|------|------------|--------------|
| `-mode` | режим: `hotp` или `totp` | `hotp` |
| `-seed` | вектор инициализации | — |
| `-format` | формат seed: `auto`, `hex`, `base32` | `auto` |
| `-otp1`, `-otp2` | два последовательных значения с токена | — |
| `-n` | сколько следующих значений рассчитать | `10` |
| `-digits` | разрядность кода (6 или 8) | по длине введённых кодов |
| `-alg` | `sha1`, `sha256`, `sha512` | `sha1` |
| `-step` | шаг времени TOTP | `30s` |
| `-start`, `-span` | диапазон перебора счётчика (HOTP) | `0`, `1000000` |
| `-window` | окно перебора вокруг текущего времени (TOTP); понимает `d` и `w` | `90d` |
| `-counter` | считать прямо с указанного счётчика/шага, без поиска | — |
| `-at` | считать текущим моментом указанное время, RFC 3339 (TOTP) | — |
| `-all` | вывести результат для всех найденных совпадений | выкл. |
| `-json` | машиночитаемый вывод | выкл. |
| `-version` | показать версию | — |
Флаги можно ставить и после позиционных аргументов: `./hotpcalc <seed> <код1> <код2> -n 20`.
### Примеры
Двадцать следующих значений в режиме HOTP:
```sh
./hotpcalc -seed 765D7268D012090C28172A729BC9CC1AD82B382D -otp1 941595 -otp2 057938 -n 20
```
Восьмиразрядный токен (разрядность определяется автоматически по длине кодов):
```sh
./hotpcalc -seed 765D... -otp1 12345678 -otp2 87654321
```
TOTP с шагом 60 секунд и seed в Base32:
```sh
./hotpcalc -mode totp -step 60s -seed GEZDGNBVGY3TQOJQ -otp1 123456 -otp2 654321
```
Расчёт прямо с известного счётчика, без поиска:
```sh
./hotpcalc -seed 765D... -counter 100 -n 5
```
Токен с большим значением счётчика — расширяем диапазон перебора:
```sh
./hotpcalc -seed 765D... -otp1 941595 -otp2 057938 -span 10000000
```
Машиночитаемый вывод:
```sh
./hotpcalc -seed 765D... -otp1 941595 -otp2 057938 -n 3 -json
```
## Программный токен
Хранилище — ini-файл, по умолчанию `tokens.ini` рядом с исполняемым файлом.
Путь меняется флагом `-store` или переменной окружения `HOTPCALC_STORE`.
Выпуск токена: генерируется случайный seed (`crypto/rand`, 20 байт), серийный номер
и файл инициализации для сервера.
```sh
$ ./hotpcalc new -label "Иванов И.И." -xml ivanov.xml
Выпущено токенов: 1
0004003D473F7D (Иванов И.И.)
тип HOTP, SHA1, 6 разрядов
seed 281CCDCD…84F1 (20 байт)
счётчик 0 (значение следующего нажатия)
выпущен 2026-09-04 11:23:14
Хранилище: /opt/hotpcalc/tokens.ini
XML: ivanov.xml
```
Дальше токен работает как брелок — каждое `next` это одно нажатие кнопки:
```sh
$ ./hotpcalc next 0004003D473F7D
521232
```
Код уходит в stdout, служебная строка (`0004003D473F7D: счётчик 0 → 1`) — в stderr,
поэтому команду можно подставлять прямо в скрипты. Флаг `-n` выдаёт несколько
значений за раз.
Интерактивный режим ближе к настоящему токену:
```sh
$ ./hotpcalc token 0004003D473F7D
Токен 0004003D473F7D (Иванов И.И.)
HOTP, SHA1, 6 разрядов
Enter — нажатие кнопки, q — выход.
> 448181 (счётчик 0)
> 257719 (счётчик 1)
> q
```
Остальные команды:
```sh
./hotpcalc list # что вообще есть в хранилище
./hotpcalc show 0004003D473F7D -peek 5 # состояние и будущие коды, счётчик не двигая
./hotpcalc sync 0004003D473F7D 394216 767221 # ресинхронизация по двум кодам
./hotpcalc set 0004003D473F7D -counter 100 # правка вручную
./hotpcalc add -label "Старый токен" 765D7268... # завести токен по известному seed
./hotpcalc remove 0004003D473F7D # удалить
```
Состояние сохраняется **до** вывода кода, а хранилище на время работы блокируется
файлом `tokens.ini.lock`, поэтому параллельные вызовы не могут получить один и тот же
счётчик. Замок старше минуты считается брошенным и снимается автоматически.
### Формат хранилища
```ini
; hotpcalc — хранилище токенов
; Файл содержит секреты в открытом виде: защищайте его как пароль.
; Одна секция — один токен, имя секции — серийный номер.
[0004003D473F7D]
type = hotp
seed = 281CCDCD4E26ACFD57A09450358FF257FEA484F1
digits = 6
algorithm = sha1
counter = 0
label = Иванов И.И.
created = 2026-09-04T11:23:14+03:00
last_used = 2026-09-04T11:23:24+03:00
```
Файл можно править вручную: комментарии, порядок секций и порядок ключей
сохраняются при перезаписи. Для TOTP вместо `counter` пишутся `period` и `last_step`.
## Файл инициализации (XML)
`new -xml`, `add -xml` и `export` выгружают токены в родном формате SafeNet/Aladdin —
том самом, что поставляется с закупленными eToken PASS. Его импортируют SafeNet
Authentication Manager, privacyIDEA, LinOTP и совместимые серверы.
```xml
<?xml version="1.0" encoding="UTF-8"?>
<Tokens>
<Token serial="0004003D473F7D">
<CaseModel>5</CaseModel>
<Model>101</Model>
<ProductionDate>09/04/2026</ProductionDate>
<ProductName>eToken PASS</ProductName>
<Applications>
<Application ConnectorID="{ab1397d2-ddb6-4705-b66e-9f83f322deb9}">
<Seed>281CCDCD4E26ACFD57A09450358FF257FEA484F1</Seed>
<MovingFactor>0</MovingFactor>
</Application>
</Applications>
</Token>
</Tokens>
```
Заводские поля меняются флагами `-connector-id`, `-product-name`, `-case-model`,
`-model`, `-production-date`.
**Ограничение формата.** В нём нет полей для разрядности, режима и периода: он
описывает шестиразрядный HOTP, а алгоритм импортирующая сторона выводит из длины
seed (64 hex-символа — SHA256, иначе SHA1). Если выгружается токен с другими
параметрами, программа предупреждает об этом явно:
```
Внимание: формат не хранит разрядность — сервер импортирует токен как 6-разрядный, а не 8-разрядный
```
Выпуск партии токенов одним файлом:
```sh
./hotpcalc new -count 50 -label "Отдел продаж" -xml batch.xml
./hotpcalc export -all -o all.xml # выгрузить всё хранилище
./hotpcalc import postavka.xml # загрузить закупленные токены
```
## Форматы seed
Формат определяется автоматически:
* **hex** — как на бланке eToken PASS: 40 шестнадцатеричных символов (20 байт).
Пробелы, дефисы и двоеточия игнорируются;
* **Base32** — как в Google Authenticator, регистр и padding `=` не важны;
* **`otpauth://` URI** — из QR-кода. Параметры `digits`, `algorithm`, `period`
и `counter` берутся из URI, если не заданы флагами явно.
## Как работает поиск позиции
Токен eToken PASS в режиме HOTP хранит счётчик, который увеличивается при каждом
нажатии кнопки. Два подряд снятых значения однозначно задают позицию: программа
перебирает счётчики в диапазоне `[-start, -start + -span)` и ищет тот, для которого
первый код приходится на счётчик `C`, а второй — на `C+1`.
В режиме TOTP перебираются временные шаги вокруг текущего момента в пределах `±-window`,
что позволяет заодно измерить расхождение часов токена и этой машины.
Если совпадений найдено несколько (вероятность порядка `span / 10^12` — практически
исключено), программа предупреждает об этом; полный список выводится с флагом `-all`.
Если совпадений нет, стоит проверить seed, разрядность, режим и порядок кодов,
а затем расширить диапазон перебора.
## Проверка корректности
Реализация проходит контрольные векторы:
* RFC 4226, Приложение D — все десять значений HOTP;
* RFC 6238, Приложение B — TOTP для SHA1, SHA256 и SHA512.
Отдельно проверяется, что программный токен и калькулятор дают одно и то же:
коды из `next` сверяются с расчётом по seed из выгруженного XML, а параллельные
нажатия не выдают повторов.
```sh
make test
```
Сверка с внешним инструментом:
```sh
oathtool --hotp -c 0 3132333435363738393031323334353637383930 # 755224
./hotpcalc -seed 3132333435363738393031323334353637383930 -counter 0 -n 1
```
## Безопасность
Seed — это секрет, эквивалентный самому токену. Программа никуда его не передаёт:
сетевых обращений нет. По умолчанию seed печатается сокращённо (`765D7268…382D`),
полностью — только по флагу `-show-seed` и в выводе `-json`.
Ini-хранилище и XML-файлы создаются с правами `0600` (на Windows биты прав не
действуют — полагайтесь на права каталога). Оба содержат секреты открытым текстом:
- удаляйте XML-файл инициализации после импорта на сервер — об этом программа
предупреждает сама;
- относитесь к `tokens.ini` как к файлу паролей;
- шифрование хранилища не реализовано: если это важно, держите файл на
зашифрованном разделе.
Seed, переданный флагом, попадает в историю командной строки и в список процессов —
в чувствительных случаях вводите его в диалоге.