抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

TOTP和2FA实现

很多网站的登陆系统都会提供一个开启2FA认证的安全选项,一些对安全性要求较高的还会要求必须使用2FA。

2FA是啥?二步验证(双因素验证,Two-factor authentication, 2FA)。在检查口令之外,还检查一个一次性认证码。典型的二步认证手段包括有:

  • 短信/邮箱验证码
  • 银行发的密码器
  • 基于 HOTP/TOTP 协议的应用程序

2FA 中使用的是一次性密码(One Time Password,OTP),也被称作动态密码。一般 OTP 有两种策略:HOTP ( HMAC-based One Time Password) 和 TOTP ( Time-based One-time Password)

TOTP工作流程

如果用户选择打开 2FA,网站会生成一个预共享的 secret key。后续的验证码都是从这个 secret key 派生出来的,这个 secret key 应该严格保密,只有服务器和客户应当知晓。

用户登录时,首先完成传统的第一步认证,然后提供 6 位数字认证码(这个认证码由 secret key 与当前时间计算得出,每 30s 变一次),若与服务器的计算结果一致,则完成认证。

(由于Google Authenticator界面下我截不了图,偷一下应用市场的)

TOTP计算原理

参考RFC4226

HOTP

HMAC-based One Time Password,由 RFC 4226 定义。一次性验证码是secret key $K$与计数器$C$的函数,其中 HMAC 过程使用的哈希函数$H$默认是 SHA-1。
$$
HOTP(K,C) = Truncate(HMAC_{H}(K,C))
$$
HMAC 算法得出的值位数比较多,不方便用户输入,因此需要截断(Truncate)成为一组不太长十进制数。要用六位数,就是$HOTP(K,C) \mod10^6$

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# https://github.com/pyauth/pyotp

def generate_otp(self, input: int) -> str:
"""
:param input: the HMAC counter value to use as the OTP input.
Usually either the counter, or the computed integer based on the Unix timestamp
"""
if input < 0:
raise ValueError("input must be positive integer")
hasher = hmac.new(self.byte_secret(), self.int_to_bytestring(input), self.digest)
hmac_hash = bytearray(hasher.digest())
# 从160位的 SHA-1 结果中,提取出31位
offset = hmac_hash[-1] & 0xF
code = (
(hmac_hash[offset] & 0x7F) << 24
| (hmac_hash[offset + 1] & 0xFF) << 16
| (hmac_hash[offset + 2] & 0xFF) << 8
| (hmac_hash[offset + 3] & 0xFF)
)
# 得到digits位的一次性密码
str_code = str(10_000_000_000 + (code % 10**self.digits))
return str_code[-self.digits :]

工作流程

  1. 客户端和服务器各有一个计数器C,并且事先将计数值同步。

  2. 客户端用K,C计算一次性密码。

  3. 计算完成之后客户端计数器C计数值加1,验证通过,服务器端将计数值C加1。

如果验证失败或者客户端不小心多进行了一次生成密码操作,那么服务器和客户端之间的计数器C将不再同步,因此需要有一个重新同步(Resynchronization)的机制:

Resynchronization

定义一个向前查找的参数s,服务器可以重新计算接下来的s个HOTP值,并将它们与接收到的HOTP客户端进行比较

计数器同步只需服务器计算下一个HOTP值并确定是否匹配。系统可以要求用户发送一系列(例如C=2, 3)的HOTP值以进行重新同步,因为伪造连续的HOTP值比猜测单个HOTP值更难。

TOTP

HOTP 与 TOTP 的区别在于:HOTP 是用一个整数 counter 来计算一次性验证码,而 TOTP 是基于当前时间来计算。显然,实现了前者就能实现后者——只需要把当前时间除以 $T$ 向下取整,就能得到一个整数 counter 送进 HOTP 协议里。

Google Authenticator把$T$设置为了30s.

由于网络延时,用户输入延迟等因素,可能当服务器端接收到一次性密码时,T的数值已经改变,这样就会导致服务器计算的一次性密码值与用户输入的不同,验证失败。这可以通过同样的思路来解决,服务器计算当前时间片以及前面的n个时间片内的一次性密码值,只要其中有一个与用户输入的密码相同,则验证通过。当然,n不能太大,否则会降低安全性

服务实现

  1. 服务端随机生成一个secret key,并且把这个密钥保存在数据库中。
  2. 在页面上显示一个二维码,内容是一个URI地址(otpauth://totp/GitHub:LarryLuTW?secret={secret_key}&issuer=GitHub
  3. 客户端用某个应用扫描二维码,把secret key保存。
  4. 客户端每30秒使用secret key和时间戳生成一个6位数字的一次性密码
  5. 用户输入这个一次性密码,然后服务端进行验证

recovery code

服务端有时会提供一个或一组recovery code,用于在你把手机砸了或者其他故障后能够登陆账户。

我们可以做一个简单的实现:

  • 随便用某些方法一组唯一且不能伪造的recovery code,将它们存在数据库,并对应用户和用户的secret key

  • 用户输入recovery code后,验证recovery code是否对应该用户,验证成功后销毁recovery code和secret key,取消用户的2FA并给用户态(或者把用户跳转到setting并引导重新设置)

评论