Реализация Google Authenticator в Python

Я пытаюсь использовать одноразовые пароли, которые могут быть сгенерированы с помощью приложения Google Authenticator.

Что делает Google Authenticator

В основном, Google Authenticator реализует два типа паролей:

  • HOTP - одноразовый пароль на основе HMAC, что означает, что пароль изменяется при каждом вызове в соответствии с RFC4226 и
  • TOTP - одноразовый пароль, основанный на времени, который изменяется на каждые 30 секунд (насколько мне известно).

Google Authenticator также доступен здесь с открытым исходным кодом: 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.

Ответ 1

Я хотел установить щедрость по моему вопросу, но мне удалось создать решение. Моя проблема, похоже, связана с неправильным значением клавиши secret (это должен быть правильный параметр для функции base64.b32decode()).

Ниже я публикую полное рабочее решение с объяснением того, как его использовать.

код

Достаточно следующего кода. Я также загрузил его в GitHub как отдельный модуль с именем ontimepass (здесь: 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 - секретное значение, известное серверу (выше script) и клиенту (Google Authenticator, предоставляя его как пароль в приложении),
  • intervals_no - это число, увеличивающееся после каждого поколения токена (это, вероятно, должно быть разрешено на сервере путем проверки некоторого конечного числа целых чисел после последнего успешного завершения проверки в прошлом)

Как использовать его

  • Создать secret (это должен быть правильный параметр для base64.b32decode()) - желательно 16- char (нет = знаков), так как он, безусловно, работал как для script, так и для Google Authenticator.
  • Используйте get_hotp_token(), если вы хотите, чтобы одноразовые пароли были недействительными после каждого использования. В Google Authenticator этот тип паролей, о которых я упоминал, основывается на счетчике. Для проверки на сервере вам нужно будет проверить несколько значений intervals_no (так как у вас нет гарантии того, что пользователь не генерировал пропуск между запросами по какой-либо причине), но не менее последнего рабочего значения intervals_no ( таким образом, вы должны, вероятно, хранить его где-нибудь).
  • Используйте get_totp_token(), если вы хотите, чтобы токен работал с интервалом в 30 секунд. Вы должны убедиться, что обе системы имеют правильное время (это означает, что они оба генерируют одну и ту же временную метку Unix в любой момент времени).
  • Обязательно защитите себя от атаки грубой силы. Если используется пароль, основанный на времени, то попытка 1000000 значений менее чем за 30 секунд дает 100% вероятность угадать пароль. В случае пассивных пакетов HMAC (HOTPs) это выглядит еще хуже.

Пример

При использовании следующего кода для одноразового пароля на основе 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 символов).

Ответ 2

Я хотел, чтобы python script генерировал пароль TOTP. Итак, я написал python script. Это моя реализация. У меня есть информация о википедии и некоторые знания о HOTP и TOTP, чтобы написать этот script.

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)