Двухфакторная защита
Одноразовые коды внутри профиля
- RFC 6238 поверх RFC 4226
- Стандарт
- 6 знаков, шаг 30 секунд
- Код
- SHA1, SHA256, SHA512
- Алгоритмы
- На вашей машине, без сети
- Где считается
Телефон в руке не тянет пятьдесят аккаунтов
Двухфакторная защита перестала быть необязательной. Площадки, рекламные кабинеты и социальные сети просят шесть знаков при каждом входе.
С двумя аккаунтами телефон устраивает всех. С пятьюдесятью он становится самой долгой частью дня: разблокировать, найти нужную запись среди полусотни похожих, прочитать цифры, успеть их вписать, повторить.
Сам ключ, из которого берутся эти цифры, и так лежит рядом с аккаунтом: в таблице, в заметке, в общем хранилище паролей. ARMANOS переносит его на шаг ближе, в тот профиль, который этим аккаунтом и пользуется.
Речь про аккаунты, с которыми вы работаете внутри профилей. Второй шаг входа в вашу собственную учётную запись ARMANOS это другое дело: он живёт на сервере и с запасными кодами.
Второе устройство не нужно
Код появляется в том же окне, где живёт профиль.
Не надо искать нужную запись
Ключ принадлежит профилю, сопоставлять по названию нечего.
Не надо спешно перепечатывать
Код копируется кнопкой, а в форму его может вписать сценарий.
В сеть ничего не уходит
Код считается из ключа прямо на этом компьютере.
Ключ ложится рядом с тем аккаунтом, которому принадлежит
Откройте меню на карточке профиля и выберите «Одноразовый код». Если ключа ещё нет, окно попросит его вместо того, чтобы показывать цифры.
Вставить можно любой вид. Годится и голый ключ, который сайт печатает рядом с картинкой QR, и целая строка otpauth://, которая в этой картинке зашита. Выгрузки из приложений-аутентификаторов дают как раз такую строку.
Из строки программа читает издателя, имя аккаунта, число знаков, шаг и алгоритм. У голого ключа ничего этого нет, и тогда берутся привычные значения. Если у сайта настройки необычные, вставляйте именно строку.
Ключ, из которого коды не получаются, отвергается сразу и на диск не ложится. Неверный код это неудачный вход, а несколько неудачных входов подряд запирают аккаунт. Строка hotp:// считает входы, а не секунды, и её тоже не принимают.
- 1
Откройте меню профиля
«Одноразовый код» стоит в том же меню, что «Отпечаток» и «Данные для входа».
- 2
Вставьте ключ
JBSWY3DPEHPK3PXP или otpauth://totp/... Пробелы, дефисы, нижний регистр и знаки = не мешают.
- 3
Сохраните
Всё, что не разбирается как base32, возвращается ошибкой, и на диск ничего не пишется.
- 4
Посмотрите на карточку
На профиле появляется отметка 2FA: сразу видно, каким аккаунтам телефон уже не нужен.
Код считается по часам, и считается он у вас на машине
Расчёт идёт по RFC 6238 поверх RFC 4226. Текущее время делится на шаг, получается счётчик. Счётчик прогоняется через HMAC с вашим ключом, и знаки вырезаются из полученного отпечатка.
Счётчик пишется двумя половинами по 32 бита. Одной 32-битной записи хватает лишь до примерно четырёх миллиардов, дальше код молча становится другим, поэтому проверка закрепляет этот случай эталоном на t=20000000000.
Никуда ничего не отправляется. Ключ остаётся в файле, отпечаток считается в самой программе, и вместе с кодом возвращается остаток текущего окна. Поэтому код получается и при полностью отключённой сети.
| Настройка | По умолчанию | Что принимается |
|---|---|---|
| Число знаков | 6 | 6, 7 или 8, если так сказано в строке otpauth |
| Шаг | 30 секунд | от 1 до 300 секунд из той же строки |
| Алгоритм | HMAC-SHA1 | SHA1, SHA256 и SHA512 |
| Вид ключа | base32 | пробелы, дефисы, нижний регистр и знаки = |
В окне видно и код, и сколько ему осталось жить
Код стоит крупно посередине и разбит по три знака. «482 913» глазами переносится в форму легче, чем «482913», а кнопка копирования кладёт его в буфер обмена уже без пробела.
Полоска под кодом убывает вместе с окном, значение обновляется раз в секунду. Последние пять секунд отмечены отдельно, чтобы никто не начинал печатать код, который вот-вот сменится.
Строка выше называет издателя и имя аккаунта, прочитанные из строки otpauth. Именно она подтверждает, что код относится к нужному аккаунту, когда профиль один из пятидесяти.
Кнопка «Заменить ключ» ставит новый: так делают после повторной привязки аккаунта. «Убрать» сначала спрашивает, и только потом профиль перестаёт выдавать коды.
Скрипт спрашивает код в ту секунду, когда он нужен
В программе есть местный интерфейс HTTP для программ на этом же компьютере. Он выключен, пока его не включат, слушает только 127.0.0.1, и каждый вызов несёт токен.
Вызов GET /api/v1/profile/code принимает profileId и отвечает текущим кодом и остатком секунд. Ради этого он и нужен: скрипту, который входит в аккаунт, второй шаг требуется ровно в тот миг, когда форма его просит, а не за полминуты до.
Запросы, похожие на пришедшие со страницы, отклоняются. Заголовок Origin, значение Sec-Fetch-Site, отличное от none, или чужой Host обрывают вызов. Открытая вами страница читать это не должна ни при каких условиях.
ИИ-помощник ходит по тому же адресу через встроенный сервер MCP, средством get_one_time_code. Там тот же токен и те же отказы.
- Вызов
- GET /api/v1/profile/code?profileId=p-42
- Токен
- параметр token, заголовок x-api-token или Authorization: Bearer
- Ответ
- { "code": 0, "data": { "code": "482913", "secondsRemaining": 17, "issuer": "Shop" } }
- У профиля нет ключа
- code -1, msg no_2fa_key
- Токена нет или он не тот
- HTTP 401
- Запрос пришёл со страницы
- Отказ, каким бы ни был токен
Ноль это состояние ответа, общее для всех вызовов. Сами шесть знаков лежат в data.code.

Сценарий вписывает код сам
В сборщике сценариев любое текстовое поле принимает {{profile.totp}}. Поставьте это в шаг ввода, нацеленный на поле кода, и сценарий заполнит его наравне с остальной формой.
Переменная здесь не значение, а вычисление. Карта переменных строится один раз на прогон, и записанный в неё код к третьему шагу был бы уже просрочен. Поэтому он считается в момент подстановки.
В журнал прогона код не попадает. Шаг ввода, значение которого пришло из переменной, записывается четырьмя точками: журнал лежит на диске и показывается в окне.
Шаг ставит курсор в поле, очищает его, вводит значение и подаёт события input и change, чтобы форма входа, следящая за настоящим набором, всё равно откликнулась.
Ключ остаётся на этом компьютере
Ключ лежит в profiles.json в папке данных программы, рядом с остальными настройками профиля. Запись неделимая: сперва соседний файл, потом доведение до диска, потом переименование. После обрыва питания на месте оказывается либо целый старый файл, либо целый новый.
Файл пишется с правами «только хозяин», а файлы, оставшиеся открытыми от прежних версий, закрываются при запуске. Эти права уезжают вместе с файлом в резервную копию или в облачную папку.
На Windows такие права меняют лишь признак «только для чтения» и не значат там ничего. Защищает файл сама папка пользователя, куда другие учётные записи без прав администратора не заходят.
Куда ключ уезжает, а куда нет, решается для каждого действия отдельно. Выгрузка настроек сначала спрашивает и по умолчанию вырезает секреты. Копия профиля ключ не берёт: копию заводят под другой аккаунт. Вход на второй машине его сохраняет, потому что на сервере такого поля попросту нет. Перенос профиля везёт ключ под вашим паролем, и текст перед вводом пароля прямо его называет. Из отчёта о проблеме он вырезан, а в журнале правок остаётся только сам факт замены, без значения.
На диске ключ не зашифрован
Пятьдесят ключей приезжают из таблицы
Ввоз из таблицы узнаёт столбец с ключом по названию. Подходят 2fa, totp, otp, authenticator, secretkey и русские написания, так что готовую таблицу обычно переименовывать не нужно.
Ничего не создаётся, пока вы не увидели, какой столбец каким полем стал. Строки, где ключ негоден, помечаются в предпросмотре, и одна отметка их пропускает вместо того, чтобы завести профиль со сломанным ключом.
С восстановлением из выгрузки та же осторожность. Число знаков, шаг и алгоритм едут вместе с ключом. Если прочитать только голый ключ, восьмизначный ключ с шагом в минуту молча стал бы обычным, и аккаунт просто перестал бы пускать.
Отдельного действия «раздать ключ уже готовым профилям» нет. Ключи попадают в программу либо по одному в окне профиля, либо столбцом при создании профилей.
Чего это не делает
- Это не хранилище паролей. Один ключ на профиль, без общего сейфа, без поиска по ключам и без места для запасных кодов, которые выдаёт сайт.
- На диске ключ не шифруется. На macOS и Linux его закрывают права на файл, на Windows только папка пользователя.
- Потерянный ключ потерян. На сервер он не уходит, и восстановить его по выданным кодам программа не может.
- Картинки не читаются. Вы вставляете текст ключа или строку otpauth://, а снимок QR или экран телефона программа не разбирает.
- Код идёт по часам этого компьютера. Если часы уехали, сайт код не примет, и программа об этой причине не предупредит.
Как проверить
За каждым утверждением выше стоит файл, а у большинства ещё и стенд, который падает, когда утверждение перестаёт быть правдой.
- Коды сходятся с эталонными векторами RFC 6238, включая счётчик за пределами 32 бит
- apps/desktop/src/lib/totp.js · apps/desktop/test/totp.js
- Файл с ключом пишется правами «только хозяин», а оставшиеся открытыми файлы закрываются при запуске
- apps/desktop/src/lib/secureFile.js · apps/desktop/test/secrets-on-disk.js
- Код отдаётся только по токену и никогда веб-странице
- apps/desktop/src/lib/local-api.js · apps/desktop/test/local-api-gate.js
- Сценарий считает код в момент подстановки
- apps/desktop/src/main/main.js · apps/web/test/страница-возможностей.js
- Восстановленный профиль сохраняет число знаков, шаг и алгоритм
- apps/desktop/src/main/main.js · apps/desktop/test/config-import-2fa.js
Что читать дальше
Профили
У каждого аккаунта свой браузер: куки, хранилище, отпечаток и прокси.
Местный интерфейс
Тот самый вызов, который отдаёт код скрипту, и токен, который его стережёт.
Сборщик сценариев
Куда ставится {{profile.totp}}, чтобы код попал в форму входа.
Ввоз из таблицы
Имена, прокси и ключи 2FA приезжают из готовой таблицы.
Пусть ключ лежит там, где живёт аккаунт
Поставьте ARMANOS, добавьте ключ одному профилю и увидите код в том же окне.