Реализация 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.

У меня есть вопросы

Мои вопросы:

  1. Что я делаю неправильно?
  2. Как я могу генерировать HOTP и / или TOTP в Python?
  3. Существуют ли какие-либо библиотеки 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 это число, увеличиваемое после каждого поколения токена (это, вероятно, должно быть решено на сервере путем проверки некоторого конечного числа целых чисел после последнего успешного, проверенного в прошлом)

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

  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 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).

Другие вопросы по тегам