TOC

谷歌身份认证器

基础概念

身份验证器

Authenticator

密码就算是身份验证器。
加密密钥,比如 SSH 的公钥私钥(密钥对)

动态密码

One-time Password, 缩写: OTP, 又叫做一次性密码

我们常用的的短信验证码就是一种非常方便快捷的动态密码形式。
早些年一些网络服务,比如谷歌、网易通行证等,可能会提供一个动态口令表,包含十几个密码,可以保存为图片,或者纯文本,上面的密码可以逐个使用,每个密码只能用一次。

  • RFC4226 HOTP: An HMAC-Based One-Time Password Algorithm
  • RFC6238 TOTP: Time-Based One-Time Password Algorithm

HOTP 基于 HMAC 算法

  • HOTP  value = HOTP(K, C) mod 10d
  • HOTP(K, C) = truncate(HMACH(K, C))
  • truncate(MAC) = extract31(MAC, MAC[(19 × 8) + 4:(19 × 8) + 7] × 8)
  • MAC[(19 × 8) + 4:(19 × 8) + 7] × 8
    取第十九字节的后四位(小端序), 转成一个有符号整型数(小端序),作为截取 MAC 的位置
  • extract31(MAC, i) = MAC[i × 8 + 1:i × 8 + (4 × 8) − 1]
  • MAC[i × 8 + 1:i × 8 + (4 × 8) − 1]
    从上一步得到的位置处,取四个字节,去掉符号位(小端序)

TOTP 基于时间

  1. 协商起始时间 T0 和时间间隔 TX
  2. 双方分别计算时间计数器 CT
  3. TOTP value(K) = HOTP value(K, CT)

多重要素验证

Multi-factor authentication, 缩写: MFA
Two-factor authentication, 缩写: 2FA

谷歌身份验证器

Google 身份验证器是一款 TOTP 与 HOTP 的两步验证软件令牌,此软件用于 Google 的认证服务。此项服务所使用的算法已列于 RFC 6238 和 RFC 4226 中。
Google 身份验证器给予用户一个六位到八位的一次性密码用于进行登录 Google 或其他站点时的附加验证。其同样可以给第三方应用生成口令,例如密码管理员或网络硬盘。先前版本的 Google 身份验证器开放源代码,但之后的版本以专有软件的形式公开。

谷歌验证器基于 TOTP,但是更进一步简化,以约定代替了协商过程。

  1. T0 为 Unix 时间
  2. TX 为 30 秒
  3. 哈希算法为 sha1

此外:

虽然不是很大的创新,而且这个软件验证器实现很简单,但是免费、开放(不需要做任何谷歌服务绑定),加上谷歌的强大影响力,这个软件验证器被很多系统采用。
最后,其他提供动态口令的应用都需要来兼容谷歌身份验证器。

PS: RedHat 开发并维护了开源的 FreeOTP 分支项目。

PS: 微软也有一个 Microsoft Authenticator,阿里云 APP 中有一个 虚拟MFA 功能,都是一个意思。
微软家的为自己提供 8 位密码,别人家的就 6 位,区别对待(虽然感觉好像也并没有什么影响)
阿里云 MFA 是需要手机 APP 登录进去之后才能使用的。

Just4Fun

import base64
import hashlib
import hmac
import time

DEFAULT_INTERVAL = 30  # Google Authenticator: 30 秒
DEFAULT_HASH = hashlib.sha1


def get_hotp_token(secret: str, message: bytes or int, hash_algorithm=DEFAULT_HASH, length=6):
    padding_len = 8 - len(secret) % 8
    if padding_len != 8:
        assert 1 <= padding_len <= 7
        secret += '=' * padding_len
    key = base64.b32decode(secret, True)
    mac = hmac.new(key, message, hash_algorithm).digest()
    loc = mac[19] & 15
    # token = (struct.unpack('>I', mac[loc:loc+4])[0] & 0x7fffffff) % (10 ** length)
    token = (int.from_bytes(mac[loc:loc+4], 'big') & 0x7fffffff) % (10 ** length)
    return token


def get_totp_token(secret, interval=DEFAULT_INTERVAL):
    time_counter = int(time.time()) // interval
    # message = struct.pack('>Q', time_counter)
    message = time_counter.to_bytes(8, 'big')
    return get_hotp_token(secret, message)


def main():
    import os

    secret = 'MZXW633PN5XW6MZX'

    for i in range(100000000000000000, 100000000000000020):
        message = (i & 0xffffffffffffffff).to_bytes(8, 'big')
        token = get_hotp_token(secret, message, length=8)
        print('%2d. %06d' % (i + 1, token))

    print('=' * 60)
    for i in range(10):
        token = get_hotp_token(secret, os.urandom(4))
        print('%2d. %06d' % (i + 1, token))

    print('=' * 60)
    for i in range(10):
        token = get_totp_token(secret)
        print('%2d. %06d' % (i + 1, token))
        time.sleep(10)


if __name__ == '__main__':
    main()

参考资料与拓展阅读