双因素认证2FA

2FA 概念

认证(authentication)即是确认用户身份,密码是最常见的认证方法。但是密码容易泄露和冒充,因此越来越多的网站开始采用双因素认证(Two-factor authentication,简称 2FA)。即通过使用除密码外的其他因素,来确认用户身份,增加冒充的难度。

常用的认证因素可以分为以下几类:

  • 秘密信息:只有该用户知道、其他人不知道的某种信息,比如密码。
  • 个人物品:该用户的私人物品,比如身份证、钥匙、U 盾等。
  • 生理特征:该用户的遗传特征,比如指纹、相貌、虹膜等等。

因素越多,证明力就越强,身份就越可靠。要求更高的的系统可以采用多因素认证(MFA)。

2FA 认证方案

常见的 2FA 认证方案有:

  • 基于硬件的方式,密码 + 某种个人物品,例如 网银 U 盾、智能卡等,缺点要随身携带;
  • 基于手机 SMS 的方式,密码 + 短信验证码,缺点是 容易被拦截和伪造(ss7信令漏洞),手机丢失或更换手机号成本较高;
  • 基于一次性密码(One Time Password,简称OTP);

OTP 密码不易泄露,即是泄漏也难以受到重放攻击,另外其公开协议 + 高强度算法方式也难以猜测,是 2FA 方案的首选。

OTP 有两种:

  • HOTP(HMAC-Based One-Time Password)基于 HMAC 的一次性密码,使用计数器计算下一个密码值。标准写入 RFC4226;
  • TOTP(Time-Based One-Time Password)基于时间的一次性密码,使用时间计算下一个密码值。标准写入 RFC6238;

TOTP 从 HOTP 发展而来,目前已经成为主流的验证方式。

TOTP

使用步骤

  1. 用户开启双因素认证后,服务器生成一个密钥;
  2. 服务器提示用户扫描二维码(或其他方式),用户保存该密钥到硬件生成器或软件生成器中,此时服务器和用户拥有了相同的密钥;
  3. 用户登录时,手机客户端使用这个密钥和当前时间戳,生成一个哈希,有效期默认为30秒。用户在有效期内,把这个哈希提交给服务器。
  4. 服务器也使用密钥和当前时间戳,生成一个哈希,跟用户提交的哈希比对。只要两者不一致,就拒绝登录。

实际应用过程中,还要做到:

  • 提供备用的固定哈希代码,用于 TOTP 密钥丢失的情况;
  • 在第2步用户绑定密钥时,就让用户输入一次哈希,验证绑定正确,用户与服务器使用了同一个密钥。

算法原理

首先计算时间计数器 TC,TOTP 用 TC 代替了 HOTP 中的计数器。

TC = floor((unixtime(now) − unixtime(T0)) / TS)

unixtime(now)是当前 Unix 时间戳,unixtime(T0)是约定的起始时间点的时间戳,默认是0,也就是1970年1月1日。TS 则是哈希有效期的时间长度,默认是30秒。通过整除,能够保证在 30 秒内,TC 都是同一个值。

然后使用 TC 计算哈希:

TOTP = HASH(SecretKey, TC)

HASH就是约定的哈希函数,默认是 SHA-1。

根据TOTP的算法特性,服务器时间与 TOTP 客户端时间差如果超过 TC ,那么一定无法生成一致的哈希。实际应用中最好能够保持时间同步。

TOTP 客户端

TOTP 客户端可以分为 TOTP 硬件生成器 和 TOTP 软件生成器。软件生成器与硬件生成器相比,优势在于:

  • 可以绑定多个密钥;
  • 可以方便的绑定解绑;
  • 时间准确性高;

常用 TOTP 软件生成器:

  • Google Authenticator
  • Microsoft Authenticator(可为微软系网站生成8位字符令牌,为其他网站生成6位字符令牌)
  • FreeOTP

服务端 Python 实现

Python 可以选择使用 PyOTP 库,该库实现了 HOTP 和 TOTP。可以通过 pip 安装:

pip install pyotp

TOTP 示例:

totp = pyotp.TOTP('base32secret3232')   # 字符串长度一定要是16

totp.now() # => '492039'

# OTP verified for current time
totp.verify('492039') # => True
time.sleep(30)
totp.verify('492039') # => False

HOTP 示例:

hotp = pyotp.HOTP('base32secret3232')   # 字符串长度一定要是16

# HOTP 计算时要传入计数值
hotp.at(0) # => '260182'
hotp.at(1) # => '055283'
hotp.at(1401) # => '316439'

# OTP verified with a counter
hotp.verify('316439', 1401) # => True
hotp.verify('316439', 1402) # => False

生成 base32 随机密钥:

# 返回 16 长度的 base32 密钥,与 Google Authenticator 和其他 OTP 客户端兼容
pyotp.random_base32()

生成 Google Authenticator 兼容链接:

totp = pyotp.TOTP('JBSWY3DPEHPK3PXP')
totp.provisioning_uri("alice@google.com", issuer_name="Secure App")

>>> 'otpauth://totp/Secure%20App:alice%40google.com?secret=JBSWY3DPEHPK3PXP&issuer=Secure%20App'

hotp = pyotp.HOTP('JBSWY3DPEHPK3PXP')
hotp.provisioning_uri("alice@google.com", initial_count=0, issuer_name="Secure App")

>>> 'otpauth://hotp/Secure%20App:alice%40google.com?secret=JBSWY3DPEHPK3PXP&issuer=Secure%20App&counter=0'

Google Authenticator 需要传入账户名称和签发机构 issuer_name 参数。得到的链接可以生成二维码供 Google Authenticator 扫描。

2FA 缺点

双因素认证有一个最大的问题,那就是帐户的恢复。一旦密钥丢失,想要恢复登录,势必就要绕过双因素认证,这就形成了一个安全漏洞。除非准备两套双因素认证,一套用来登录,另一套用来恢复账户(前文所说的备用哈希代码)。

参考链接

目录