Python中Google身份验证器的实现

129 投票
3 回答
74731 浏览
提问于 2025-04-17 08:28

我正在尝试使用一次性密码,这些密码可以通过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

我遇到的问题是,使用上面的代码生成的密码与在Android的Google Authenticator应用中生成的密码不一致。尽管我尝试了多个intervals_no值(确切地说是前10000个,从intervals_no = 0开始),而且secret与GA应用中提供的密钥相同。

我有的一些问题

我的问题是:

  1. 我哪里做错了?
  2. 我如何在Python中生成HOTP和/或TOTP?
  3. 有没有现成的Python库可以使用?

总之:请给我一些线索,帮助我在Python代码中实现Google Authenticator的认证。

3 个回答

4

根据 @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(key) 来获取当前的 TOTP 代码,其中 "key" 是一个 bytes 类型的值(通常是经过 base 32 解码的密钥)。

8

我想要一个Python脚本来生成TOTP密码。所以,我写了这个Python脚本。这是我的实现。我在维基百科上找到了这个关于TOTP的信息,并且对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)
188

我想给我的问题设置一个悬赏,但我已经成功找到了解决方案。我的问题似乎和 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. 如果你想要一个每30秒有效的令牌,使用 get_totp_token()。你必须确保两个系统的时间设置正确(也就是说,它们在任何时刻生成相同的 Unix 时间戳)。
  4. 确保保护自己免受暴力破解攻击。如果使用基于时间的密码,那么在30秒内尝试1000000个值就有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个字符的长度)。

撰写回答