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

104

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

Что делает Google Authenticator

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

  • HOTP - одноразовый пароль на основе HMAC, что означает, что пароль меняется при каждом вызове в соответствии с RFC4226 , и
  • TOTP - Time-based One-Time Password, который меняется каждые 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.

Вопросы у меня есть

Мои вопросы:

  1. Что я делаю не так?
  2. Как я могу сгенерировать HOTP и / или TOTP в Python?
  3. Существуют ли для этого какие-нибудь библиотеки Python?

Подводя итог: дайте мне любые подсказки, которые помогут мне реализовать аутентификацию Google Authenticator в моем коде Python.

Тадек
источник

Ответы:

153

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

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

  1. Сгенерировать secret(это должен быть правильный параметр для base64.b32decode()) - желательно с 16 символами (без =знаков), так как он наверняка работал как для скрипта, так и для Google Authenticator.
  2. Используйте, get_hotp_token()если вы хотите, чтобы одноразовые пароли становились недействительными после каждого использования. В Google Authenticator этот тип паролей, который я упоминал, основан на счетчике. Для проверки на сервере вам нужно будет проверить несколько значений intervals_no(поскольку у вас нет гарантии, что пользователь по какой-то причине не сгенерировал проход между запросами), но не ниже последнего рабочего intervals_noзначения (поэтому вам, вероятно, следует сохранить его где-то).
  3. Используйте get_totp_token(), если хотите, чтобы токен работал с 30-секундными интервалами. Вы должны убедиться, что в обеих системах установлено правильное время (это означает, что они обе генерируют одну и ту же временную метку Unix в любой момент времени).
  4. Обязательно защитите себя от перебора. Если используется пароль на основе времени, то попытка 1000000 значений менее чем за 30 секунд дает 100% шанс угадать пароль. В случае паролей на основе HMAC (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 символов).

Тадек
источник
3
@burhan: Если вам нужен код, я также загрузил его на GitHub (здесь: https://github.com/tadeck/onetimepass ), поэтому его будет довольно легко использовать в проектах как отдельный модуль. Наслаждайтесь!
Tadeck
1
У меня возникла проблема с этим кодом, потому что «секрет», который мне предоставила служба, в которую я пытаюсь войти, был строчными, а не прописными. Изменение строки 4 на "key = base64.b32decode (secret, True)" устранило проблему для меня.
Крис Мур,
1
@ChrisMoore: Я обновил код, casefold=Trueпоэтому у людей не должно быть подобных проблем сейчас. Спасибо за ваш вклад.
Tadeck
3
Мне только что сайт предоставил секрет из 23 символов. Когда я передаю ему этот секрет, ваш код не работает с ошибкой TypeError: неправильное заполнение. Заполнение секрета таким образом устранило проблему: key = base64.b32decode (secret + '====' [: 3 - ((len (secret) -1)% 4)], True)
Крис Мур,
3
для питона 3: изменение: ord(h[19]) & 15в: o = h[19] & 15 Спасибо BTW
Орвилл
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)
Аниш Шах
источник
Интересно, но, возможно, вы захотите сделать его более понятным для читателя. Сделайте имена переменных более значимыми или добавьте строки документации. Кроме того, после PEP8 вы можете получить дополнительную поддержку. Вы сравнивали производительность этих двух решений? Последний вопрос: совместимо ли ваше решение с Google Authenticator (поскольку вопрос касался этого конкретного решения)?
Tadeck
@Tadeck Я добавил несколько комментариев. И я добился своего, используя этот сценарий. так что да, он должен работать отлично.
Аниш Шах