一句话概括
TOTP 是一种算法,它通过一个共享密钥和当前时间,生成一个不断变化的临时数字验证码,用作登录时的第二重验证因素。
它解决了什么问题
说实话:密码这玩意儿就是个必要之恶。我们记不住、到处重复用,而攻击者又非常擅长通过钓鱼、数据库泄露和老掉牙的猜密码方式来窃取它们。只依赖密码,就像把前门锁了,却把钥匙放在门垫底下。
这时候,多因素身份验证(MFA)和它最常见的形式——双因素身份验证(2FA)就派上用场了。核心思想是,证明你的身份不能只靠你知道的东西(密码),还需要你拥有的东西(比如你的手机)或你本身是什么(比如你的指纹)。
早期的 2FA 通常是通过短信接收验证码。这比没有强,但很容易受到“SIM 卡交换”(SIM swapping)攻击:骗子说服你的手机运营商把你的号码转移到他们的手机上。突然之间,你的 2FA 验证码就全发给他们了。这可不太妙。
于是,基于 App 的身份验证器登场了。它不再依赖不怎么安全的手机网络,而是在你设备上的一个 App 里本地生成验证码。但问题来了,你的 Google Authenticator App 怎么知道如何为你的 GitHub 账户生成验证码?你的 Microsoft Authenticator 又怎么知道如何跟你的 Dropbox 对话?
答案是,它们都说着同一种开放标准的语言:TOTP,即基于时间的一次性密码(Time-Based One-Time Password)。TOTP 是个无名英雄,它将你的第二重验证因素从任何单一公司中解耦出来。它是一个开放、可互操作的标准,允许任何兼容的 App 与任何兼容的服务一起工作。它是早期一个叫 HOTP(基于一个简单计数器)的标准的演进版,但通过使用时间作为“动态”部分,TOTP 的用户体验好上了一大截。
它的底层工作原理
TOTP 看起来跟魔法似的。你只需扫描一次二维码,之后你的手机和一个远在天边的服务器就能在每 30 秒钟,不需再次沟通,就同步算出一个相同的六位数密码。这不是魔法,只是一些非常酷炫的密码学技巧。
让我们来拆解一下这个“配方”。
共享密钥:通往王国的钥匙
一切都始于一个密钥。当你在某个服务上启用 2FA 时,服务器会生成一个随机的秘密密钥(比如,160 位的纯随机数据)。这个密钥是唯一需要你和服务器双方都知道的东西。整个系统的安全性就建立在对这个密钥的保密上。
当然,一串 160 位的随机比特对人类来说不太友好。所以为了我们方便,它被编码成一种叫 Base32 的格式。跟它的兄弟 Base64 不同,Base32 使用的字符集更小,不区分大小写,并且避免了像 1 和 l,或 0 和 O 这样容易混淆的字符。这使得在无法扫描二维码的情况下,手动输入成为可能(虽然还是很痛苦)。
一个典型的 Base32 密钥看起来像这样:JBSWY3DPEHPK3PXPJBSWY3DPEHPK3PXP
将这个密钥导入你的身份验证器 App 最常见的方式,是通过一个二维码,这个二维码编码了一个特殊的 URI:
otpauth://totp/FlowingDev:user@example.com?secret=JBSWY3DPEHPK3PXP&issuer=FlowingDev
你的 App 扫描这个二维码,保存签发者(FlowingDev)、账户名(user@example.com),以及最重要的 secret(密钥)。秘密的握手就此完成。
动态因子:时间
TOTP 中的“T”代表基于时间(Time-Based)。这是让验证码变化的关键因素。但系统并不会使用精确到毫秒的时间——那根本无法同步。取而代之的是,它使用一个离散的时间步长(time step)。
公式很简单:
TimeStep = floor(CurrentUnixTime / 30)
- CurrentUnixTime: 从 1970 年 1 月 1 日(Unix 纪元)至今所经过的秒数。
- / 30: 时间步长几乎普遍都是 30 秒。
这个计算会得出一个整数,这个整数每 30 秒才变化一次。在上午 10:00:00,TimeStep 的值是某个 T。到了上午 10:00:29,它仍然是 T。但一到上午 10:00:30,它就增加到 T+1。这个 TimeStep 就是我们要进行验证的“消息”。
加密魔法:HMAC-SHA1
现在,我们把静态的密钥和动态的时间步长结合起来。这通过一种叫做 HMAC(基于 Hash 的消息认证码)的密码学原语来完成。
HMAC_Result = HMAC-SHA1(SecretKey, TimeStep)
你可以把 HMAC 理解为一种创建安全签名的方法。它接收我们的密钥和我们想要签名的数据(即 TimeStep),然后用一个 hash 函数(在这里是老而弥坚的 SHA-1)把它们搅和在一起。
输出结果是一个 20 字节(160 位)的 hash 值。这是一个很长的十六进制字符串,对于特定的密钥和特定的时间步长来说,它是完全唯一的。只要密钥改变一个 bit,或者等待 30 秒让时间步长改变,你就会得到一个截然不同的 hash 值。
尽管 SHA-1 在其他用途(如证书签名)上存在已知的弱点,但在 HMAC 结构中使用时,它仍被认为是完全安全的。为了以防万一,许多现代实现正在转向使用 HMAC-SHA256 或 HMAC-SHA512。
截断处理:生成六位数字
一个 20 字节的十六进制字符串可不是一个用户友好的六位数验证码。最后一步就是“动态截断”这个 hash,把它变成我们能用的数字。
- 找到偏移量(Offset): 查看 20 字节 HMAC 结果的最后 4 个 bit(半个字节,nibble)。这会给我们一个 0 到 15 之间的数字。这就是我们的
offset。 - 抓取 4 个字节: 从 20 字节 hash 的那个
offset位置开始,抓取接下来的 4 个字节(32 bit)。 - 清除最高位: 对这个 32 bit 的数据块和一个
0x7FFFFFFF进行按位与(bitwise AND)操作。这实际上是去掉了第一个 bit,解决了潜在的有符号整数与无符号整数的问题,并确保结果总是正数。 - 取模大法好! 现在你有一个很大的 31 位整数。为了把它缩减到 6 位数,你用它对 1,000,000 (
10^6) 取模。 - 前导补零: 结果就是你的验证码!如果数字是
4521,你就在前面补上零,变成004521。
搞定!你的手机和服务器都执行着完全相同的计算。当你输入验证码时,服务器会比较它自己算出的结果和你的结果。为了考虑到你的手机和服务器之间微小的时钟漂移,服务器通常会检查当前时间步长、前一个时间步长和后一个时间步长的验证码。只要有一个匹配,你就登录成功了。
真实世界的案例
一次未遂的 GitHub 账户劫持
一位开发者正在专心写代码,突然收到一封看起来和 GitHub 官方邮件一模一样的邮件,警告他有安全问题。他一时分心,点了链接,进入了一个像素级复刻的 GitHub 登录页面,并输入了用户名和密码。攻击者现在拿到了他的凭据。钓鱼者立刻尝试登录真实的 GitHub。他输入了偷来的密码……然后看到了一个提示:“请输入您的两步验证码。” 攻击者彻底傻眼了。他没有开发者的手机,也无法生成 TOTP 验证码。与此同时,开发者收到了 GitHub 发来的关于新设备登录的真实邮件,意识到自己犯了错,并立刻修改了密码。
教训: TOTP 就像一道关键的防火墙。它把一次潜在的灾难性密码泄露变成了一次无害的、失败的登录尝试。
离线紧急访问
一位网络工程师正在飞往某会议的航班上。起飞三十分钟后,他需要检查一个关键的生产系统。飞机上没有 Wi-Fi,手机也完全没有信号。基于短信或推送的 2FA 在这里将毫无用处。但他的后台是用 TOTP 保护的。他打开笔记本电脑,进入登录页面,在手机上打开身份验证器 App,然后输入了六位数的验证码。这个 App 不需要任何网络连接;它的计算是自给自足的,只依赖于共享密钥和手机的内部时钟。他成功登录并解决了问题,此时空乘人员甚至还没开始送饮料。
教训: TOTP 完全离线工作的能力使其在关键访问场景下极其健壮和可靠,无论你身在何处。
跨平台迁移的头痛……解决了
一位长期的安卓用户决定换用 iPhone。他打开 Google Authenticator,惊恐地发现没有一个简单的“同步到我的新手机”按钮。他设置了超过 30 个账户,一想到要为每一个账户都走一遍重置 2FA 的流程就头皮发麻。但他突然想起了自己的个人安全策略:对于每个新服务,他都把初始的二维码截图保存在了加密的密码管理器中。他在新 iPhone 上安装了一个新的身份验证器 App,打开密码管理器,一个接一个地重新扫描了保存的二维码。十分钟内,所有 30 个账户都迁移完毕并正常工作。
教训: TOTP 密钥(以 Base32 字符串或二维码形式存在)才是真正的钥匙。通过安全地备份你的密钥,不依赖于任何单一的身份验证器 App,你才能获得真正的所有权和可移植性。
常见的错误和陷阱
- 随便对待密钥。 你在设置时拿到的二维码或 Base32 字符串就是密钥本身。如果攻击者拿到了它,他们就能永久生成有效的验证码。不要把它截图然后扔在桌面上,不要用邮件发给自己,也不要把它存在一个纯文本文件里。要像对待主密码一样对待它。
- 忘记保存恢复代码。 当你启用 2FA 时,几乎所有服务都会提供一组一次性使用的恢复代码。把它们打印出来。把它们保存在一个安全的地方(一个真正的保险箱,或者至少是你的密码管理器里)。如果你丢了手机,这些代码是你找回账户的唯一途径,否则就要经历痛苦的客服流程。
- 以为你的验证器 App 已经备份了。 许多简单的身份验证器 App(比如最初的 Google Authenticator)只在当前设备上存储密钥。如果手机丢失或被清空,密钥就永远消失了。使用提供安全、加密云同步的 App(如 Authy 或 1Password),或者养成手动备份密钥的好习惯。
- 忽略时钟漂移。 TOTP 是基于时间的。如果你手机的时钟与真实时间相差超过一两分钟,它生成的验证码就会被拒绝。永远把你的手机和电脑设置为“自动设置时间”。当有效的验证码突然不好用时,这几乎总是罪魁祸首。
为什么你应该关注它
如果你是一名开发者,正在构建任何有用户账户的系统,你就应该实施 MFA,而 TOTP 是基于 App 的 2FA 中最好、最开放的标准选项。它比短信更安全,比专有的推送通知系统更开放。了解如何生成密钥、创建 otpauth:// URI 以及验证传入的 token,是构建安全应用程序的一项基本技能。
即使你从未亲自实现过 TOTP 系统,你也是 AWS、Cloudflare、GitHub 和 npm 等平台上它们的使用者。了解这背后的魔法如何运作,能让你更安全地使用它。它能帮助你理解保护初始密钥、保存恢复代码以及选择适合你需求的身份验证器 App 的重要性。它揭开了那个保护你的数据免受坏人侵害的、小小的六位数密码的神秘面纱。
深入探索
- RFC 6238: Time-Based One-Time Password Algorithm — TOTP 的官方规范。
- RFC 4226: HMAC-Based One-Time Password Algorithm — HOTP 的规范,TOTP 是建立在它的基础之上的。
- RFC 4648: The Base16, Base32, and Base64 Data Encodings — 定义了用于共享密钥的 Base32 编码的规范。
- Key URI Format · google/google-authenticator Wiki — 用于二维码中的
otpauth://URI 的事实标准。 - Time-based One-time Password - Wikipedia — 对该算法的高层次和历史性概述。