验证码是怎么算出来的:时间窗口与 HMAC
从一张二维码说起
绑定时你扫的那张二维码,载荷是一把共享密钥(setup key)——从这一刻起,平台和你的验证器各持同一把钥匙(二维码里装了什么有拆解)。之后每次出码,都是这把钥匙和当前时间的一次联合运算。
三步算出一枚码
- 数格子:把当前时间除以 30 秒,取整数——得到当前处于第几个时间窗口。世界协调时下,每 30 秒这个数字加一;
- 做运算:以密钥为钥匙、窗口数为输入,做一次 HMAC-SHA1 运算,得到一串固定长度的乱数;
- 裁数字:按标准规则从乱数中截取并折算出 6 位十进制数字——这就是屏幕上的验证码。
T = 当前时间 ÷ 30(取整数部分)
HMAC = HMAC-SHA1(共享密钥, T)
码 = 从 HMAC 中截取的 6 位数字
为什么手机和平台必然算出同一个数
两边输入完全相同:同一把密钥(绑定时共享的)、同一个时间(只要双方钟都准,见手机时间不准为什么影响出码)。确定性算法保证输出必然一致——所以「校验」不需要任何网络往返,平台收到你填的码后,本地按同样公式算一遍,对上了就放行。
三个由原理直接推出的特性
- 断网可用:整个运算只发生在本地 CPU 上(为什么离线也能出码);
- 30 秒一换:窗口数变了,HMAC 输入变了,输出整个重排(一枚验证码只有 30 秒寿命);
- 钟错全错:唯一共享的外部变量是时间,它偏了,两边就「看着同一个钟」变成了各看各的。
标准的出处
这套机制是公开标准:HOTP 定义于 RFC 4226(计数器驱动),TOTP 定义于 RFC 6238(时间驱动,30 秒窗口、6 位数字是主流参数)。验证器与各平台都是标准的实现者——这也是「任何标准验证器都能扫任何平台的码」的根本原因(两款主流验证器怎么选的通用性一节)。