Реализация Google Authenticator в Python
Я пытаюсь использовать одноразовые пароли, которые можно сгенерировать с помощью приложения Google Authenticator.
Что делает Google Authenticator
По сути, Google Authenticator реализует два типа паролей:
- HOTP - одноразовый пароль на основе HMAC, что означает, что пароль изменяется при каждом вызове в соответствии с RFC4226, и
- TOTP - временный одноразовый пароль, который меняется каждые 30 секунд (насколько я знаю).
Google Authenticator также доступен как открытый источник здесь: http://code.google.com/p/google-authenticator/
Текущий код
Я искал существующие решения для генерации паролей HOTP и TOTP, но не нашел много. У меня есть следующий фрагмент кода, который отвечает за генерацию HOTP:
import hmac, base64, struct, hashlib, time
def get_token(secret, digest_mode=hashlib.sha1, intervals_no=None):
if intervals_no == None:
intervals_no = int(time.time()) // 30
key = base64.b32decode(secret)
msg = struct.pack(">Q", intervals_no)
h = hmac.new(key, msg, digest_mode).digest()
o = ord(h[19]) & 15
h = (struct.unpack(">I", h[o:o+4])[0] & 0x7fffffff) % 1000000
return h
Проблема, с которой я сталкиваюсь, заключается в том, что пароль, который я генерирую с помощью приведенного выше кода, не совпадает с паролем, созданным с помощью приложения Google Authenticator для Android. Хотя я пробовал несколько intervals_no
значения (ровно первые 10000, начиная с intervals_no = 0
), с secret
быть равным ключу, предоставленному в приложении GA.
У меня есть вопросы
Мои вопросы:
- Что я делаю неправильно?
- Как я могу генерировать HOTP и / или TOTP в Python?
- Существуют ли какие-либо библиотеки Python для этого?
Подводя итог: пожалуйста, дайте мне любые подсказки, которые помогут мне реализовать аутентификацию Google Authenticator в моем коде Python.
3 ответа
Я хотел назначить награду за мой вопрос, но мне удалось создать решение. Кажется, моя проблема связана с неверным значением secret
ключ (это должен быть правильный параметр для base64.b32decode()
функция).
Ниже я публикую полное рабочее решение с объяснением того, как его использовать.
Код
Следующий код достаточно. Я также загрузил его на GitHub в виде отдельного модуля, называемого onetimepass (доступен здесь: https://github.com/tadeck/onetimepass).
import hmac, base64, struct, hashlib, time
def get_hotp_token(secret, intervals_no):
key = base64.b32decode(secret, True)
msg = struct.pack(">Q", intervals_no)
h = hmac.new(key, msg, hashlib.sha1).digest()
o = ord(h[19]) & 15
h = (struct.unpack(">I", h[o:o+4])[0] & 0x7fffffff) % 1000000
return h
def get_totp_token(secret):
return get_hotp_token(secret, intervals_no=int(time.time())//30)
У него две функции:
get_hotp_token()
генерирует одноразовый токен (который должен стать недействительным после однократного использования),get_totp_token()
генерирует токен на основе времени (изменяется с интервалом в 30 секунд),
параметры
Когда дело доходит до параметров:
secret
является секретным значением, известным серверу (приведенный выше скрипт) и клиенту (Google Authenticator, предоставляя его в качестве пароля в приложении),intervals_no
это число, увеличиваемое после каждого поколения токена (это, вероятно, должно быть решено на сервере путем проверки некоторого конечного числа целых чисел после последнего успешного, проверенного в прошлом)
Как это использовать
- генерировать
secret
(это должен быть правильный параметр дляbase64.b32decode()
) - предпочтительно 16 символов (нет=
признаки), так как это, безусловно, работает как для скрипта, так и для Google Authenticator. - использование
get_hotp_token()
если вы хотите, чтобы одноразовые пароли были недействительными после каждого использования. В Google Authenticator этот тип паролей я упоминал как основанный на счетчике. Для проверки на сервере вам нужно будет проверить несколько значенийintervals_no
(поскольку у вас нет гарантии, что пользователь по какой-то причине не сгенерировал проход между запросами), но не меньше, чем последний рабочийintervals_no
значение (таким образом, вы, вероятно, должны хранить его где-то). - использование
get_totp_token()
, если вы хотите, чтобы токен работал с 30-секундными интервалами. Вы должны убедиться, что в обеих системах установлено правильное время (это означает, что они обе генерируют одну и ту же метку времени Unix в любой данный момент времени). - Обязательно защитите себя от атаки грубой силы. Если используется основанный на времени пароль, то попытка 1000000 значений менее чем за 30 секунд дает 100% шанс угадать пароль. В случае с HMAC passowrds (HOTP) это выглядит еще хуже.
пример
При использовании следующего кода для одноразового пароля на основе HMAC:
secret = 'MZXW633PN5XW6MZX'
for i in xrange(1, 10):
print i, get_hotp_token(secret, intervals_no=i)
вы получите следующий результат:
1 448400
2 656122
3 457125
4 35022
5 401553
6 581333
7 16329
8 529359
9 171710
что соответствует токенам, сгенерированным приложением Google Authenticator (за исключением случаев, когда короче 6 знаков, приложение добавляет нули в начало и достигает длины 6 символов).
Я хотел, чтобы скрипт Python генерировал пароль TOTP. Итак, я написал скрипт на Python. Это моя реализация. У меня есть эта информация в Википедии и некоторые знания о HOTP и TOTP, чтобы написать этот скрипт.
import hmac, base64, struct, hashlib, time, array
def Truncate(hmac_sha1):
"""
Truncate represents the function that converts an HMAC-SHA-1
value into an HOTP value as defined in Section 5.3.
http://tools.ietf.org/html/rfc4226#section-5.3
"""
offset = int(hmac_sha1[-1], 16)
binary = int(hmac_sha1[(offset * 2):((offset * 2) + 8)], 16) & 0x7fffffff
return str(binary)
def _long_to_byte_array(long_num):
"""
helper function to convert a long number into a byte array
"""
byte_array = array.array('B')
for i in reversed(range(0, 8)):
byte_array.insert(0, long_num & 0xff)
long_num >>= 8
return byte_array
def HOTP(K, C, digits=6):
"""
HOTP accepts key K and counter C
optional digits parameter can control the response length
returns the OATH integer code with {digits} length
"""
C_bytes = _long_to_byte_array(C)
hmac_sha1 = hmac.new(key=K, msg=C_bytes, digestmod=hashlib.sha1).hexdigest()
return Truncate(hmac_sha1)[-digits:]
def TOTP(K, digits=6, window=30):
"""
TOTP is a time-based variant of HOTP.
It accepts only key K, since the counter is derived from the current time
optional digits parameter can control the response length
optional window parameter controls the time window in seconds
returns the OATH integer code with {digits} length
"""
C = long(time.time() / window)
return HOTP(K, C, digits=digits)
Следуя правильному ответу от @tadeck и @Anish-Shah, можно получить более простой способ получить код без использования
struct
и избегая лишнего импорта:
""" TOTP """
import hmac
import time
def totp(key: bytes):
""" Calculate TOTP using time and key """
now = int(time.time() // 30)
msg = now.to_bytes(8, "big")
digest = hmac.new(key, msg, "sha1").digest()
offset = digest[19] & 0xF
code = digest[offset : offset + 4]
code = int.from_bytes(code, "big") & 0x7FFFFFFF
code = code % 1000000
return "{:06d}".format(code)
Это работает с Python 3.
Вы можете узнать текущий код TOTP, позвонив по телефону
totp(key)
где «ключ» - это
bytes
(обычно кодированный ключ с базой 32).