Pull to refresh

TOTP без смартфона

Level of difficultyEasy
Reading time5 min
Views19K

Когда я решил избавиться от необходимости постоянно носить с собой смартфон, одной из проблем оказалась двухфакторная аутентификация (2FA, приложение Google Authenticator). Остаться без возможности авторизации на множестве сервисов было неприемлемо, нужна была альтернатива.

Беглый поиск вывел меня на утилиту oathtool: командная строка, POSIX, OSS — всё, как я люблю, проблема в принципе решена. Но, как и большинство CLI утилит, её удобно использовать в сочетании с другими утилитами, а для этого полезно написать скриптовую обвязку. Собственно этой обвязкой, а также опытом использования, я и решил поделиться.

Про 2FA вообще и TOTP в частности

Регистрация в системе при помощи пары логин / пароль — самый старый, самый распостранённый и, пожалуй, самый критикуемый способ аутентификации. Собственно к написанию статьи меня подтолкнула публикация Почему пароли безнадежно устарели и зачем ими до сих пор пользуются? и особенно комментарии к ней.

Чем же плоха парольная аутентификация (точнее, в чём её основные проблемы)? Во-первых, это пресловутый человеческий фактор — слабые пароли, один пароль на много ресурсов и тому подобное. Во-вторых, это долгое время жизни паролей — мало кто добровольно меняет их достаточно часто, а со временем шансы на утечку разными путями только растут.

Давно родилась идея подкреплять аутентификацию по паролю чем-то другим. Из банковской сферы пришёл «ответ на контрольный вопрос», но по сути он был как второй пароль, слабый и долгоживущий. А вот одноразовые пароли (OTP) это уже было существенно лучше. Сперва их отправляли с помощью SMS или в виде hard copy (напечатанный список из десятка-другого кодов, по исчерпанию надо было получать новый) — это было не всегда удобно, не всегда безопасно, не всегда бесплатно.

Хорошей идеей оказалось генерировать OTP одновременно на стороне сервера и клиента на основании некоего общего секрета (shared secret) и постоянно увеличивающегося числа (например, счётчик попыток регистрации). Из этих двух сущностей однозначно составляли сообщение (message) и затем при помощи хеш-функции SHA-1 получали HMAC. Так появился HOTP (описан в RFC 4226, декабрь 2005 года).

У HOTP оказалось два несовершенства: возможна рассинхронизация счётчика (на стороне клиента генерацию могут вызывать чаще, чем было выполнено успешных аутентификаций, повторной синхронизации посвящён §7.4 RFC) и неограниченный срок жизни OTP (могло представлять угрозу при использовании фишинговых «прокладок»). И тогда было предложено расширение TOTP (описано в RFC 6238, май 2011 года).

В этом расширении в качестве постоянно увеличивающегося числа предложили взять текущее время UNIX time (количество секунд прошедших с 01.01.1970 00:00:00 UTC), делённое на длину интервала валидности OTP (по умолчанию 30 секунд). Таким образом, если часы на сервере и клиенте синхронизированы (несколько секунд туда-сюда роли не играют), то при одинаковом общем секрете сгенерированные значения TOTP совпадут — что и будет требуемым подтверждением аутентификации!

Постановка задачи

Разумеется, при наличии oathtool генерация TOTP возможна сразу же, напрямую:

$ oathtool -b -t SKEY

Но ключ инициализации лучше хранить зашифрованым и передавать не в командной строке, а через конвейер (pipe). В руководстве по oathtool есть рецепт, как это организовать с использованем GNU Privacy Guard (gpg):

$ gpg --decrypt --quiet ~/.my-totp-secret.asc | oathtool --totp -

На этом можно было бы и остановиться, но кроме необходимости запоминать / набирать много букв, есть ещё одна специфическая для TOTP особенность: при стандартном кванте времени 30 секунд можно запустить генерацию в такой момент, когда срок действия сгенерированого кода будет пара секунд, и можно не успеть выделить / скопировать / вставить / отправить его.

Поэтому я решил написать скрипт (назову его mytotp.sh) с таким функционалом:

  • mytotp.sh вывод названий всех добавленых сервисов

  • mytotp.sh SERVICE вывод TOTP для указанного сервиса с максимальным временем валидности

Уже в ходе написания статьи я хотел добавить mytotp.sh --help вывод подсказки по использованию и mytotp.sh --add SERVICE добавление ключа инициализации для SERVICE. Но вовремя вспомнил о принципе YAGNI и дал себе по рукам за попытку превратить простенький скрипт в Программный Продукт.

Предварительные требования

Собственно сама утилита oathtool (в зависимости от используемой ОС может быть установлена в составе пакета oathtool или oath-toolkit) и GNU Privacy Guard (gpg, скорее всего уже установлен). Для хранения ключей инициализации TOTP буду использовать ассиметричное шифрование GPG, так что нужна пара приватный / публичный ключи (можно использовать уже существующий, или сгенерировать новый, специально для TOTP).

Создание пары ключей для GPG специально для TOTP

Парольную фразу лучше поставить свою вместо Slozhnyj-parol, два пробела между $ и gpg не случайность — при установленной переменной окружения HISTCONTROL=ignoreboth строка не попадёт в историю.

$  gpg --yes --batch --passphrase 'Slozhnyj-parol' --quick-generate-key "My TOTP"

И для хранения всех ключей инициализации в одном месте нужно создать каталог:

$ mkdir -p ~/.config/mytotp && chmod 0700 ~/.config/mytotp

Реализация

Собственно говоря, сам скрипт очень тривиален, потому спрячу его под спойлер. Единственная изюминка — задержка с генерацией TOTP до ближайшей :00 или :30 секунды, чтобы полученный код максимально долго (все 30 секунд) был валиден.

Для интересующихся — код скрипта и ссылка на GitHub
#!/bin/bash
#
# Put TOTP key for service SERVID to GPG file crypted for 'My TOTP'
#  gpg -e -r 'My TOTP' > ~/.config/mytotp/SERVID.gpg

KEYDIR=~/.config/mytotp
KEYEXT=.gpg
SERVID=$1

if [ -z "${SERVID}" ] ; then
  echo -e "Usage: $0 SERVID\n\tSERVID is a service ID, abbreviated, w/o ext:"
  find ${KEYDIR}/*${KEYEXT} | sed -e 's/\/home.*\//  /; s/\.gpg//'
  exit 2
fi

if [ ! -f "${KEYDIR}/${SERVID}${KEYEXT}" ] ; then
  echo "No key for ${KEYDIR}/${SERVID}${KEYEXT}"
  exit 1
fi

SKEY=$(gpg -d --quiet "${KEYDIR}/${SERVID}${KEYEXT}")

NOWS=$(date +'%S')
WAIT=$((60 - NOWS))
if [ ${WAIT} -gt 30 ]; then
  WAIT=$((WAIT - 30))
fi
echo -n "Seconds :${NOWS} (wait ${WAIT}) ... "
sleep ${WAIT}

TOTP=$(echo "${SKEY}" | oathtool -b --totp - )

echo "${TOTP}"
OKEY="none"

exit 0

Я раньше всегда обходился SVN, но раз Git теперь «стильно модно молодёжно», то попробую соответствовать: репозиторий mytotp.

Использование (на примере GitHub)

Захожу на github.com, Account / Settings / Password and authentication / Enable two-factor authentication. Под QR-кодом нахожу и нажимаю “setup key”, копирую строку “Your two-factor secret” (на всякий случай сохраняю её и в парольном менеджере, в карточке логина на GitHub). Выполняю в консоли:

$ gpg -e -r 'My TOTP' > ~/.config/mytotp/github.gpg
XXXXXXXXXXXXXXXX	(нажимаю Ctrl+V)
(Ctrl+D)

После запускаю mytotp.sh github, ввожу парольную фразу от ключа, жду появления 6-значного кода, ввожу его на сайте — 2FA активирован и проверен, без смартфона!

Генерация TOTP: было и стало
Генерация TOTP: было и стало

Заключение

Писал для себя, потому что нужно было. Решил поделиться, вдруг ещё кому-то полезным окажется. В планах возможно учесть варианты, когда для какого-то сервиса количество цифр не 6, и/или HMAC функция не SHA-1. Но пока у меня всё работает, так что YAGNI.

Да, ожидаемо не работает с MicroSoft, поскольку там традиционно не ограничились стандартным TOTP.

Добрый совет всем (даже кто не планирует отказываться от смартфона и радостно сканирует QR-коды): при включении 2FA на сервисе сохраняйте также и текстовый ключ инициализации! Когда надумаете на другое приложение переходить — пригодится!


(дополнение от 2024-04-22) По результатам обсуждения этой статьи написал послесловие.

Tags:
Hubs:
Total votes 68: ↑68 and ↓0+68
Comments88

Articles