banner
约 1,200 字
4 分钟

TOTP 动态验证码的算法原理

摘要

TOTP 通过共享密钥与当前时间步数进行 HMAC 运算,再经过动态截断和取模生成短时有效的动态验证码。服务器用同一密钥和时间窗口进行验证,整个过程中共享密钥不会在网络中传输。

TOTP 是“基于时间的一次性密码”的缩写。它能在本地验证器软件里不断生成 6 位或 8 位数字,并不是因为平台把验证码发送给了你,而是因为你和平台各自保存着同一把共享密钥,并在同一时间用同一套算法进行计算。

TOTP 的算法核心可以概括为:

纯文本
TOTP = Truncate(HMAC-SHA-1(K, T))

其中,K 是共享密钥,T 是随时间递增的时间步数,Truncate 表示从哈希结果中截取并转换成最终验证码。TOTP 本质上可以看作 HOTP 的时间变体,它用时间步数替代了 HOTP 中的计数器。

第一步:把当前时间换算成时间步数 T#

TOTP 不直接使用秒级时间戳,而是把时间切分成固定长度的时间窗口。默认情况下,每个窗口是 30 秒,也可以设置为 60 秒。计算公式为:

纯文本
T = floor((当前 Unix 时间戳 - T0) / X)

其中,X 是时间步长,默认是 30 秒;T0 是计时起点,默认是 0,也就是 1970 年 1 月 1 日;floor 表示向下取整。

举例来说,假设当前 Unix 时间戳是 1734500000,时间步长 X 为 30 秒,那么:

纯文本
T = floor(1734500000 / 30) ≈ 57816666

这个 T 就是当前 30 秒窗口的编号。每过 30 秒,T 就会增加 1。在实际的 HMAC 计算中,T 通常会被编码为 8 字节的大端整数。

第二步:用密钥 K 和时间步数 T 做 HMAC-SHA-1 运算#

把共享密钥 K 和时间步数 T 一起送入 HMAC-SHA-1 函数:

纯文本
HS = HMAC-SHA-1(K, T)

这一步会输出一个固定长度为 20 字节的哈希值 HS。这个结果看起来是一串没有规律的二进制数据,不能直接作为验证码使用。

第三步:动态截断,从哈希值中取出 4 个字节#

20 字节的哈希结果太长,用户无法直接输入,因此需要从中动态截取一部分。TOTP 使用一种叫做动态截断的方法,具体挑选哪 4 个字节,由哈希值本身决定。

首先,取 HS 的最后一个字节,将它与 0x0F 做按位与运算,得到一个 0 到 15 之间的整数 Offset:

纯文本
Offset = HS[19] & 0x0F

这个 Offset 就是截取位置的偏移量。

然后,从 HS 的第 Offset 个字节开始,连续取出 4 个字节,得到一个 32 位二进制串 P:

纯文本
P = HS[Offset : Offset + 4]

接着,将 P 的最高位,也就是符号位,屏蔽为 0,得到一个 31 位二进制串 Sbits:

纯文本
Sbits = P & 0x7FFFFFFF

这样做是为了避免不同系统在处理有符号数时产生歧义。

第四步:取模,得到最终验证码#

将上一步得到的 31 位二进制串 Sbits 转换成一个十进制整数 Snum。最后,对 Snum 进行取模运算,取余数,就得到最终的验证码:

纯文本
验证码 = Snum mod 10^Digit

其中,Digit 是验证码位数,通常是 6 或 8。如果 Digit 等于 6,就是:

纯文本
验证码 = Snum mod 1000000

结果会补零到 6 位,例如 012345。

几个关键补充#

第一,TOTP 并非绝对只能使用 SHA-1。虽然 SHA-1 是默认标准,但 TOTP 协议也允许实现方使用 SHA-256 或 SHA-512 作为底层哈希函数,以提升强度。

第二,服务器端通常会做时间容错。由于网络延迟或设备时钟轻微漂移,服务器验证时不会只比对当前这一个时间窗口。它通常会同时计算当前时间步、前一个时间步和后一个时间步的验证码,只要用户输入匹配其中任意一个,就通过验证。这为几秒到几十秒的时钟偏差留出了容错空间。

第三,设备时间必须准确。验证器软件完全依赖设备自身的系统时间来参与计算,它本身不会联网获取时间。因此,当手机时间不准时,TOTP 验证码就可能失效。

总结来说,TOTP 的核心就是共享密钥与时间步数共同参与 HMAC 运算,再经过动态截断和取模,最终得到随时间变化的动态验证码。整个过程中,共享密钥本身不会在网络中传输,验证码也会随时间窗口不断变化。

END