Python 中的 Google 身份验证器实现

Posted

技术标签:

【中文标题】Python 中的 Google 身份验证器实现【英文标题】:Google Authenticator implementation in Python 【发布时间】:2012-01-21 16:34:19 【问题描述】:

我正在尝试使用可通过Google Authenticator application 生成的一次性密码。

Google 身份验证器的作用

基本上,Google Authenticator 实现了两种类型的密码:

HOTP - 基于 HMAC 的一次性密码,这意味着每次调用都会更改密码,符合RFC4226,并且 TOTP - 基于时间的一次性密码,每 30 秒更改一次(据我所知)。

Google Authenticator 也可在此处作为开源获得:code.google.com/p/google-authenticator

当前代码

我一直在寻找生成 HOTP 和 TOTP 密码的现有解决方案,但没有找到太多。我拥有的代码是以下 sn-p 负责生成 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 应用程序中提供的键。

我的问题

我的问题是:

    我做错了什么? 如何在 Python 中生成 HOTP 和/或 TOTP? 是否有任何现有的 Python 库用于此?

总结一下:请给我任何有助于我在 Python 代码中实现 Google Authenticator 身份验证的线索。

【问题讨论】:

【参考方案1】:

我想为我的问题设置一个赏金,但我已经成功地创建了解决方案。我的问题似乎与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 是每次生成令牌后递增的数字(这可能应该在服务器上通过在过去最后一次成功检查后检查一些有限数量的整数来解决)

如何使用

    生成 secret(它必须是 base64.b32decode() 的正确参数) - 最好是 16 字符(没有 = 符号),因为它确实适用于脚本和 Google Authenticator。 如果您希望一次性密码在每次使用后失效,请使用get_hotp_token()。在 Google Authenticator 中,我提到的这种类型的密码是基于计数器的。为了在服务器上检查它,您需要检查 intervals_no 的几个值(因为您无法保证用户由于某种原因没有在请求之间生成传递),但不小于最后一个工作 intervals_no 值(因此您可能应该将其存储在某个地方)。 如果您希望令牌以 30 秒的间隔工作,请使用 get_totp_token()。您必须确保两个系统都设置了正确的时间(这意味着它们在任何给定时间都生成相同的 Unix 时间戳)。 确保保护自己免受暴力攻击。如果使用基于时间的密码,那么在不到 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 个字符的长度)。

【讨论】:

@burhan:如果您需要代码,我也已将其上传到 GitHub(此处为:https://github.com/tadeck/onetimepass),因此在项目中作为单独的模块使用它应该很容易。享受吧! 我的代码有问题,因为我尝试登录的服务提供的“秘密”是小写的,而不是大写的。将第 4 行更改为“key = base64.b32decode(secret, True)”为我解决了这个问题。 @ChrisMoore:我已经用casefold=True 更新了代码,所以人们现在不应该有类似的问题。感谢您的意见。 一个网站刚刚给了我一个 23 个字符的秘密。当我给它这个秘密时,你的代码会失败并显示“TypeError:不正确的填充”。像这样填充秘密,解决了问题:key = base64.b32decode(secret + '===='[:3-((len(secret)-1)%4)], True) 对于 python 3:将:ord(h[19]) & 15 更改为:o = h[19] & 15 谢谢顺便说一句【参考方案2】:

我想要一个 python 脚本来生成 TOTP 密码。所以,我写了python脚本。这是我的实现。我在***上有这个 info 以及一些关于 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 我添加了一些 cmets。我用这个脚本完成了我的工作。是的,它应该可以完美运行。【参考方案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 代码,其中“密钥”是 bytes(通常是基数 32 解码的密钥)。

【讨论】:

以上是关于Python 中的 Google 身份验证器实现的主要内容,如果未能解决你的问题,请参考以下文章

Perl 中的 Google 身份验证器实现

如何在 Google Cloud Functions 的 python 环境中检查 HTTP 基本身份验证

Python 爬虫入门—— IP代理使用

使用google身份验证器实现动态口令验证

Azure Google身份验证获取用户个人资料信息

Google App Engine + 基于 JSON 的服务 + 身份验证