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 | # https://github.com/pyauth/pyotp |
工作流程
客户端和服务器各有一个计数器C,并且事先将计数值同步。
客户端用K,C计算一次性密码。
计算完成之后客户端计数器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不能太大,否则会降低安全性
服务实现
- 服务端随机生成一个secret key,并且把这个密钥保存在数据库中。
- 在页面上显示一个二维码,内容是一个URI地址(
otpauth://totp/GitHub:LarryLuTW?secret={secret_key}&issuer=GitHub
) - 客户端用某个应用扫描二维码,把secret key保存。
- 客户端每30秒使用secret key和时间戳生成一个6位数字的一次性密码
- 用户输入这个一次性密码,然后服务端进行验证
recovery code
服务端有时会提供一个或一组recovery code,用于在你把手机砸了或者其他故障后能够登陆账户。
我们可以做一个简单的实现:
随便用某些方法一组唯一且不能伪造的recovery code,将它们存在数据库,并对应用户和用户的secret key
用户输入recovery code后,验证recovery code是否对应该用户,验证成功后销毁recovery code和secret key,取消用户的2FA并给用户态(或者把用户跳转到setting并引导重新设置)