Python中的Google Authenticator实现

129

我正在尝试使用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 应用程序提供的密钥。
我的问题如下:
  1. 我做错了什么?
  2. 如何在 Python 中生成 HOTP 和/或 TOTP?
  3. 是否存在此类Python库?
总之,给我任何线索,帮助我在我的Python代码中实现Google Authenticator身份验证。
3个回答

187

我原本想对我的问题设置悬赏,但是我已经成功创建了解决方案。我的问题似乎与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 的密码(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
如果你需要这个代码,我也已经将它上传到GitHub上了(在此处:https://github.com/tadeck/onetimepass),因此在项目中将其作为单独的模块使用应该非常容易。祝使用愉快! - Tadeck
1
@ChrisMoore:我已经更新了代码,加入了casefold=True,所以现在人们不应该再遇到类似的问题了。感谢您的建议。 - Tadeck
3
我刚从一个网站得到了一个23个字符的秘密。当我把它输入你的代码时,它会出现“TypeError: Incorrect padding”的错误。像这样填充秘密可以解决问题:key = base64.b32decode(secret + '===='[:3-((len(secret)-1)%4)], True)。 - Chris Moore
13
将Python 3中的ord(h[19]) & 15改为o = h[19] & 15 - Orville
2
我建议将get_totp_token()更改为始终返回一个6位数字字符串。用return '{:06}'.format(h)替换return h - Noam
显示剩余8条评论

8

我想要一个用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 我添加了一些注释。使用这个脚本我完成了我的任务。所以,它应该完美地工作。 - Anish Shah

3

通过遵循@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(通常是base32解码密钥)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接