跳到主要内容
2FAAuth.Work
语言: 中文
阅读约 10 分钟 作者:2faauth.work 编辑政策

TOTP 工作原理:6 位数字验证码背后的数学原理

用通俗易懂的方式讲解基于时间的一次性密码(TOTP)的生成过程:从共享密钥到您输入的 6 位数字。

TOTP 验证码到底是什么

打开你的身份验证器应用,你会看到一个 6 位数字,正在倒计时它 30 秒的寿命。这个数字就是一个 TOTP 验证码:一种基于时间的一次性密码,在 RFC 6238 中被标准化,作为更早的基于 HMAC 的方案 HOTP 的扩展。

核心思想:这个验证码不是密码。它是你持有共享密钥的证明——这个值在你首次设置 2FA 时安装到你的设备上。由于验证码由该密钥加上当前时间推导而来,它每 30 秒变化一次,任何人在你输入之后瞥见它也无济于事。

整个过程中没有任何东西被传输。没有短信,没有与登录服务器的连接,完全没有网络。你的应用和服务器在各自的时钟上独立执行相同的计算。相同的输入,相同的数学,相同的验证码——这就是全部奥妙所在。

对比一下短信验证码:短信通过网络传输,可能被拦截或被 SIM 卡劫持者截获;TOTP 的验证码从未离开你的设备,攻击者即使窃听到服务器通信也一无所获。

常见的误解:验证码本身不能“重置密码”或“找回密钥”。它只是证明你持有密钥。一旦密钥丢失(例如手机报废且没有备份),验证码无从生成,只能通过服务方预留的恢复代码找回账号。因此首次设置 2FA 时,务必把恢复代码抄写在离线、安全的地方——这是整个体系中最容易、也最致命的疏漏。

共享密钥:一切的起点

在账户上启用双因素认证(2FA)时,服务器会生成一个随机密钥——通常是 20 字节的加密随机数据。这一串字符是后续一切的根源。

服务器只会把它交给你一次,通常以二维码的形式呈现。看似像素图案,实际上只是编码后的文本:密钥本身,以 Base32(字母 A–Z 和数字 2–7)书写,外加账户名和发行方。你的验证器应用扫描它、解码它,并将密钥存储在本地。

从那一刻起,同一密钥存在两份副本:一份在服务器上,一份在你的应用里。此后它绝不会再通过网络发送。之后生成的每个验证码都源自这个共享值——这正是保护它如此重要的原因。

那张二维码的截图就是你密钥的永久副本。任何看到它的人都能永远生成有效验证码。扫描后,立即删除。

实际操作中有几点值得注意:

  • 确认账户名无误。 二维码里包含账户名和发行方,扫描后核对应用中显示的条目,避免多个密钥混在一起分不清归属。
  • 保存恢复途径。 多数服务在设置 2FA 时会提供一次性恢复代码。密钥只存在你的手机里,手机丢失或换机时,恢复代码是唯一的出路。打印或存入密码管理器,不要和截图放在同一处。
  • 不要用不可信的相机应用或在线识别工具解码二维码。 任何解码过程都等于把密钥交给对方。只用验证器应用本身扫描。

启用完成后,最好立即用生成的验证码登录一次,确认两端密钥一致。如果服务器始终提示验证码错误,常见原因是时间不同步或扫描了错误账户的二维码,此时应重新添加,而不是反复重试。

时间步:一个移动的计数器

TOTP 从不直接使用原始时钟。相反,你的应用和服务器都会取当前的 Unix 时间戳(自 1970 年起的秒数)并除以 30,舍弃余数:

counter = floor(unix_time / 30)

结果是一个普通整数,每 30 秒递增 1。在撰写本文时,这个计数器已经是某个巨大的数字——但应用只关心双方计算出相同的值。相同的计数器加上相同的密钥就意味着相同的验证码。

举例说明:时间戳 1,700,000,045 除以 30 得到约 56,666,668.17,向下取整后计数器为 56,666,668。此后 30 秒内,无论你何时打开应用,验证码都不变——这正是它可以在一整个时间步内手动输入的原因。

为什么时钟偏移会破坏验证码

整个方案的关键在于一致。如果你手机的时钟偏差 40 秒,你计算出的是计数器 N,而服务器已经到了 N+1——验证码不同,出现“验证码无效”错误,即使你输入得完全正确。

服务器知道时钟会漂移,所以大多数服务器会接受上一个或下一个时间步的验证码作为宽限期。也就是说,偏移在约 ±30 秒内通常无感;偏移 40 秒时,验证码恰好落在窗口边缘,表现为“时好时坏”——这次能用、下次不行,是典型的时钟漂移症状。超出此范围,请重新同步设备时钟(设置为自动)。

另一个常见误区:手机时间本身准确,但设置了非自动时区导致夏令时处理错误,同样会使时间戳偏移。TOTP 只依赖 Unix 时间戳,与时区无关,所以“自动设置时间”比手动调时区更可靠。

HMAC:将密钥与计数器混合

每个 TOTP 验证码的核心都是一个函数:HMAC-SHA1。HMAC 的全称是 Hash-based Message Authentication Code(基于哈希的消息认证码),它在这里的任务很简单——把两个输入混合成一个不可预测的输出。

这两个输入是:

  • 共享密钥——密钥本身,只有你的手机和服务器知道
  • 时间计数器——每 30 秒变化一次的数字

可以把 HMAC 想象成带密钥的哈希。普通哈希每次都以相同方式打乱数据,而 HMAC 的打乱方式因密钥而异。输入相同的密钥和计数器 48291,总会得到相同的 20 字节结果。计数器加一,输出就会变成看似完全无关的东西。

这种“雪崩效应”非常彻底:哪怕密钥只改动一个比特,20 字节输出也会面目全非。因此两个不同用户即使使用相似甚至重复的密钥,各自的验证码也毫无关联。

至关重要的是,这个过程只能单向进行。即使看到数百万个输出,攻击者也无法从中获得关于密钥的任何有用信息——正是这一特性让整个方案得以安全。

另外值得知道:SHA1 在这里并不代表算法过时。HMAC-SHA1 迄今没有被实际攻破的记录,Google Authenticator 等主流应用也默认使用它;SHA256 只是部分服务的可选选项,安全性上并非必需。

截断:从 20 字节到 6 位数字

HMAC-SHA-1 产生 20 字节——远超显示所能合理呈现的长度。TOTP 通过动态截断对其进行压缩:

  1. 取 HMAC 输出的最后一个半字节(最低 4 位)。得到一个 0 到 15 之间的数字。
  2. 将其用作偏移量:从输出中该位置开始取 4 字节。
  3. 掩去这 4 字节的最高位,留下一个 31 位整数。

掩去最高位的原因很实际:某些编程语言的有符号整数类型会把这一位当作符号位,不处理就可能出现负数,破坏后续的取模运算。

该整数仍然太大,无法直接显示,因此对其取模 1,000,000。余数——介于 0 到 999,999 之间——即成为验证码,并用零填充至 6 位。余数 4,207 显示为 042207。填充不是装饰:验证服务器按固定长度比较,缺少前导零会导致验证失败。

之所以称为动态,是因为偏移量来自数据本身:每个验证码采样 HMAC 输出的不同片段。若偏移量固定不变,验证码将始终取自同一位置,码空间会明显缩小。

一些服务使用 7 位或 8 位验证码,改为对 10,000,000 或 100,000,000 取模——相同的数学原理,更大的码空间,更难猜测。

为什么验证码可以放心手动输入

6 位验证码有一百万种可能的取值(000000 到 999999)——纸面上看并不算多。真正起作用的是你只有寥寥几次猜测机会。服务器会对登录尝试进行速率限制,并拒绝反复失败,因此攻击者无法遍历所有组合。常见的做法是同一账户连续输错若干次后临时锁定,或强制要求重新输入完整密码。真正的防线是速率限制,而非验证码长度。

每个验证码的寿命也很短。它与 30 秒的时间窗口绑定,且只在其前后很短时间内被接受,因此被窃取的验证码在被重用之前就已过期。攻击者若通过钓鱼页面骗取验证码,也必须在半分钟内立刻使用,否则作废。

最后,看到一个验证码并不能推知未来的验证码。验证码是使用你的密钥进行 HMAC 计算的输出。HMAC 是单向函数:给定输出,无法还原输入。瞥见今天验证码的攻击者无法推导出密钥,也无法预测明天的验证码——因为他们不知道底层计数器的配对值。

这也解释了为什么验证码可以直接手动输入:即使有人在你身后瞥到一个已用过的验证码,它也已失效,且无法用来推出下一个。

常见问题与边缘情况

验证码为何失效

最常见的原因是时钟漂移。手机时钟与服务器时钟必须保持在允许的时间窗口内,因此手机时间不准会产生错误的验证码。解决方法:启用自动时间同步。具体来说,Android 在“系统 > 日期和时间”里打开“使用网络提供的时间”;iPhone 则打开“自动设置”。如果你手动改过时区或时间,验证码会立刻失效,改回自动同步即可恢复。

密钥错误也会静默失败——这通常发生在二维码被扫入了另一个账户的设置流程。典型的错误场景:你在为邮箱账户开启 2FA,却用同一个验证器 App 里已有的旧条目核对新验证码。表面上一切正常,实际服务器在等一个从未绑定过的密钥。排查方法:删掉验证器里疑似错误的条目,重新扫码后再核对。另外,有些验证器应用里多个条目显示相同的账户名,容易看错行——给条目加上可辨认的备注能避免这种失误。

最后,二维码可能过期:许多服务会在短时间后或你离开该页面后使注册码失效,因此旧的截图无法使用。正确做法是在同一个页面内完成扫码并立即用新验证码确认。

验证码可以重复使用吗?

技术上可以——但大多数服务器会拒绝在当前时间步内已被接受过的验证码。这能防止攻击者瞥见验证码后几秒内重放。注意一个副作用:如果你自己连续输错后,在同一个时间步内又输出了正确验证码,但刚才恰好在某次尝试中碰巧输对这个码,可能仍被判定为“已使用”,等约 30 秒再试即可。

如果丢失设备

共享密钥只存在于你的设备上,因此丢失设备意味着失去访问权限——除非你在设置 2FA 时保存了恢复码。请离线保存,例如打印后放入抽屉,而不是截图存在可能同步到云端的相册里。每个恢复码通常只能使用一次,用掉一个就划掉一个,并记录剩余数量。换手机时务必先在旧设备上导出或转移验证器数据,再清空旧设备;顺序颠倒就会丢失全部密钥。

完整流程:串联起来

一切归结为一条流水线:共享密钥 → 时间计数器 → HMAC → 截断 → 6 位数字。

一个完整的 30 秒周期,从头到尾:

  • 设置阶段,你扫描二维码。二维码内容就是一个密钥字符串加账号名。你的应用存储共享密钥,通常还顺手存下服务器发来的账号名和发行方,方便你在列表里分清哪个码属于哪个网站。
  • 在 12:04:30,应用计算当前时间步(如果从 Unix 纪元按 30 秒分块计数,大约是 24,000)。
  • HMAC-SHA1 将密钥与该计数器混合,产生 20 个字节。
  • 截断步骤从中选取 4 个字节,转换为数字,并保留最后 6 位:比如说,482913。
  • 你把它输入。持有相同密钥的服务器运行完全相同的计算(为时钟偏差允许一到两个时间步的漂移),两码匹配。

注意两侧唯一的输入差异:应用用的是“现在”,服务器还要检查“上一个”和“下一个”时间步。这就是为什么输刚过期的那一个码也常常能通过,也是为什么同一码在 30 秒内不会变。

两个习惯保护整条链路:把二维码当作密码对待——截图别存进会自动上传的相册,扫码完成后尽快关闭页面;并保持设备时钟为自动设置,手动改过时间是最常见的“码总是不对”的原因。做到这两点,剩下的数学运算自会搞定。

分享本指南

阅读约 9 分钟

2FA安全清单:12步锁定你的账户

一份实用的安全清单,涵盖2FA设置、密码卫生和恢复选项。今天就能执行的12个具体步骤,强化你的账户,防止被接管。

阅读更多
阅读约 10 分钟

2FA 简史:从硬件令牌到通行密钥

双因素认证如何从外挂式硬件令牌与短信验证码,演变为如今可抵御网络钓鱼的通行密钥——以及每一步演变的原因。

阅读更多