跳转到主要内容
技术

TOTP 的工作原理 - 基于时间同步生成的 6 位验证码

什么是 TOTP - 每 30 秒变化一次的一次性验证码

基于时间的一次性密码(TOTP)是 Google Authenticator、Authy、Microsoft Authenticator 和 1Password 背后的算法。服务器和用户设备共享一个密钥,双方通过将该密钥与当前时间结合来计算一个 6 位数字。这个数字每 30 秒更换一次,成功登录需要用户输入当前窗口内有效的数字。RFC 6238 于 2011 年对此进行了标准化,确保了厂商无关的互操作性。

TOTP 最大的实用优势是,一旦密钥共享完成,它无需任何网络连接即可工作。服务器不需要向设备发送任何内容,设备也不需要查询服务器。这使得 TOTP 在大规模运营时比基于短信的一次性密码更经济,同时还避开了困扰短信认证的 SIM 卡交换攻击。

HOTP - 基于计数器的前身

TOTP 建立在基于 HMAC 的一次性密码(HOTP)之上,后者定义于 RFC 4226。HOTP 使用计数器和密钥,通过 HMAC-SHA-1 对它们进行哈希运算,并从结果中提取一个 6 位数字。每次成功登录后,双方的计数器都会递增,因此下一个验证码是不同的。HOTP 无需时间源,这使其适用于没有时钟的离线硬件令牌。

HOTP 的缺点是计数器失步。如果用户多次打开认证器应用而未使用生成的验证码,设备的计数器会前进而服务器端保持不变。因此服务器必须向前查看若干个值以找到匹配项,这增加了运维复杂度。TOTP 用当前时间除以 30 秒的时间窗口来替代计数器,优雅地解决了这个问题。

TOTP 算法 - 30 秒时间步长

TOTP 将计数器计算为当前 Unix 时间戳除以 30(整数除法)。例如,在 Unix 时间 1716000000(2024 年 5 月 18 日 UTC 14:40)时,计数器为 57200000。该计数器与共享密钥一起输入 HOTP 算法,生成一个 6 位数字。30 秒后,计数器变为 57200001,生成一个完全不同的数字。

RFC 6238 规定的 30 秒步长在可用性和安全性之间取得平衡。太短用户来不及输入验证码;太长则暴力破解有更大的成功窗口。服务器通常还会接受前一个和下一个 30 秒窗口,总共给予大约 90 秒的容差。不同服务对此略有调整:Steam Guard 使用 30 秒步长但有自定义调整,而某些企业令牌使用 60 秒。

二维码中的密钥 - otpauth URI

启用双因素认证时,屏幕上的二维码编码了一个包含共享密钥的 otpauth:// URI。一个典型的 URI 形如 otpauth://totp/Example:alice@example.com?secret=JBSWY3DPEHPK3PXP&issuer=Example。密钥使用 Base32 编码(使用 Base32 是因为它避免了在显示字体中容易混淆的字符)。认证器应用解析此 URI 并将密钥本地存储。

任何拍到二维码照片的人都可以克隆 OTP 生成器,因此设置界面应仅显示一次,且绝不应在屏幕共享会话期间显示。大多数服务会在 TOTP 设置时配备一组一次性备份码。每个备份码是一个单次使用的长随机字符串,可以绕过 TOTP,在手机丢失时使用。将备份码存储在另一个密码管理器中或打印后放入保险箱是标准做法。

时钟漂移 - 最常见的失败原因

当 TOTP 认证失败时,原因通常是服务器和设备对当前时间存在分歧。服务器通常通过 NTP 同步到 UTC,但手动调整过时间的手机,或刚从国际旅行返回设置混乱的手机,可能漂移一分钟甚至更多。Google Authenticator 包含一个“时间校正”功能,从 Google 获取正确时间并相应调整内部计算的偏移。

服务端时钟漂移同样危险。没有正确 NTP 同步的容器可能漂移数十秒,尤其是在嵌套虚拟化环境中。行业案例研究反复描述过这样的故障:由于认证服务器的时钟落后了几分钟,所有用户都被锁定在系统之外。对于实现 TOTP 的系统,定期 NTP 同步和时间同步失败告警是不可妥协的运维要求。

实现要点 - SHA-1 到 SHA-256、重放保护

TOTP 默认使用 HMAC-SHA-1,RFC 6238 认为这对 OTP 用例来说已经足够。更新的实现可以通过 otpauth URI 中的 algorithm 参数选择 SHA-256 或 SHA-512。旧版 Google Authenticator 不支持 SHA-256,因此向后兼容性是大多数服务仍然使用 SHA-1 的主要原因。对于仅面向现代认证器应用的全新实现,SHA-256 是合理的升级选择。

服务器还必须防止重放攻击。如果两次登录尝试在同一个 30 秒窗口内到达,只有第一次应该成功;否则,捕获了验证码的攻击者可以重复使用它。存储每个用户最近一次成功使用的计数器值,并拒绝任何等于或小于该值的后续请求,是经典的防御方式。默认的 6 位长度可以通过 digits 参数增加到 7 或 8 位以获得更高的熵,代价是输入便利性稍有下降。

XB!LINE

这篇文章对您有帮助吗?

相关文章