全部文章

TOTP 验证码是什么?2FA 动态验证码的工作原理

TOTP(基于时间的一次性密码)是验证器 App 用的那套机制。它只做一件事:把一把固定的密钥和当前时间放在一起,算出一个 6 位数字。时间在走,所以数字一直在变。

本文另有 English

TOTP 验证码是什么?2FA 动态验证码的工作原理

TOTP 是什么

TOTP 是一套算验证码的规则,写在 RFC 6238 这份标准里。Google Authenticator、Microsoft Authenticator、Authy、1Password 里的验证码功能,用的都是它,所以它们之间可以互换。

它的输入只有两样:

  • 一把密钥(开启两步验证时平台给你的那串 Base32 字符)
  • 当前时间

输出是一个 6 位数字。密钥不变,时间在变,所以数字每 30 秒换一个。

TOTP 和 2FA 是什么关系

2FA 说的是「除了密码,再要一个证据」这件事本身。至于第二个证据用什么,有很多选择:短信、邮箱、硬件密钥、验证器 App。

TOTP 是其中一种实现。它之所以流行,是因为它不需要平台承担任何发送成本,也不依赖手机号,而且断网可用。

所以「2FA 验证码」这个说法在日常语境里通常指的就是 TOTP 验证码。

核心只有一步:密钥 + 时间 → 验证码

把整套机制压缩成一句话:

TOTP 的全部逻辑
密钥(不变) + 当前时间(一直在走) → 当前验证码

具体是这样运转的:

  1. 取当前时间的 Unix 时间戳,也就是从 1970 年 1 月 1 日到现在的秒数。
  2. 除以 30 并取整,得到一个「时间片编号」。同一个 30 秒窗口内,这个编号不变。
  3. 用密钥对这个编号做一次 HMAC 运算,得到一串固定长度的字节。
  4. 从这串字节里截取一段,转成数字,取末尾 6 位。

关键在第二步。时间不是按秒进入计算的,而是先切成 30 秒一片。这样在同一片时间里,任何时刻算出来的结果都一样——你和平台不需要时间精确到毫秒一致,只要落在同一片里就行。

第三步的 HMAC 是一种单向运算:给定密钥和输入,结果唯一且可重复;但反过来,拿着一堆结果推不出密钥。这就是为什么把验证码给别人看,对方也无法反推出你的密钥。

为什么网站和验证器能算出一样的验证码

因为双方手上的两个输入是同一份。

密钥在开启两步验证的那一刻就交接完毕,平台存着一份,你的验证器存着一份。时间则是全世界共用的同一个 Unix 时间戳。两个输入相同,同一套算法自然得出相同的 6 位数字。

平台核对时做的事很简单:用它存的密钥算一次当前验证码,和你输进来的比一比。

正因为它靠的是双方时间一致,时间就成了整套机制里最脆弱的一环。设备时间慢了一分钟,算出来的就是一分钟前那一片的验证码,平台自然不认。这也是 2FA 验证码报错最常见的原因。

考虑到网络延迟和手动输入的耗时,平台通常会同时接受前一片和后一片的验证码,相当于给了大约正负 30 秒的容错。少数平台放得更宽,也有平台严格只认当前这一片。

为什么每 30 秒更新

30 秒是标准里的默认值,也是两种压力之间的折中。

时间片越短,验证码泄露后可用的时间越短,但用户越容易在输入过程中赶上刷新,体验变差。时间片越长,输入从容,但一个被人看到的验证码可以用更久。

30 秒之所以成为事实标准,主要是因为绝大多数验证器都按它实现,平台按它校验。有些平台会用 60 秒,otpauth 链接里的 period 参数就是用来声明这件事的。

为什么是 6 位

算法在截取那一步会得到一个很大的数字,取末尾几位就是验证码。取几位是可配置的,标准默认 6 位。

6 位有一百万种组合。配合 30 秒有效期和平台的失败次数限制,猜中的概率低到没有实际意义。再加位数会提高安全性,但输入负担也上去了——8 位的实现存在,但少见,通常出现在对安全要求更高的企业系统里。

otpauth 链接里的 digits 参数负责声明位数。

Base32 在这里做什么

算法需要的是密钥的原始字节,而人需要的是能抄写、能复制的文本。Base32 负责在两者之间转换。

它的字符表是 A–Z 加 2–7,刻意去掉了 0、1、8、9 和 O、I、L——这些字符手抄时容易互相认错。所以密钥里出现小写字母不用担心(不区分大小写),但出现 0、1、8、9 基本可以断定抄错了。

工具拿到密钥后的第一步就是把 Base32 文本还原成字节,然后才进入 HMAC 运算。这一步失败,就是你看到「密钥格式不对」的时候。密钥本身长什么样、在哪里找,见2FA 密钥是什么。

二维码里到底存了什么

二维码不是加密,只是把一段文字画成图案。开启两步验证时那张码里存的是这样一行:

扫码得到的实际内容
otpauth://totp/GitHub:zhang@example.com?secret=JBSWY3DPEHPK3PXP&issuer=GitHub

密钥就明文写在 secret= 后面。验证器扫码,做的是读出这行文本、取出密钥、把账号名存进列表——和你手动输入密钥完全等价。

如果链接里还带着 algorithm=SHA256、digits=8 或 period=60,说明这个平台没用默认参数。这种情况下只抄 secret 的值会算出对不上的码,得把整条链接一起用。

顺带一个安全提醒:这张二维码的截图和密钥本身一样敏感,任何人扫到它就等于拿到了密钥。

TOTP 不是短信验证码

两者都是 6 位数字,来源完全不同。

同样 6 位数字,产生方式与风险面不一样。
TOTP 验证码短信验证码
谁生成你的设备本地算平台生成后下发
传输过程没有传输经过运营商网络
断网能用能不能
主要风险密钥泄露短信被拦截、SIM 卡被转移
有效期通常 30 秒通常几分钟

短信验证码的弱点在于「要送达」,路上就有被截走的可能。TOTP 没有这段路,代价是密钥必须自己保管好。

在浏览器里生成 TOTP 验证码

理解了原理,操作就很直接:有密钥就能出码,不必先装 App。账号、密码和密钥各自填在哪,见2FA 密钥使用教程。

把密钥粘贴进 2FA 验证码生成器,页面用浏览器自带的 Web Crypto 做上面那套 HMAC 运算,给出当前验证码和剩余秒数。多个账号一行一个密钥,各自出码、各自倒计时。带参数的 otpauth 链接可以整条粘贴,algorithm、digits、period 会被读出来。

密钥只在浏览器里参与计算,EnvTrace 不会把它发到服务器,也不会写进存储。

如果算出来的码平台不认,先量一下设备时间——按上面的原理,时间偏差是唯一能让「密钥完全正确」和「验证码错误」同时成立的原因。排查步骤见2FA 验证码无效怎么办。

常见问题

TOTP 和 2FA 是什么关系?
2FA 是「两步验证」这件事,TOTP 是实现它的一种具体方式。短信验证码、硬件密钥、人脸都可以充当第二步,TOTP 是其中最常见的一种。
验证器不联网为什么也能出验证码?
算验证码只需要密钥和当前时间,两样都在设备本地。验证器从来不需要向服务器请求验证码。
TOTP 验证码为什么是 6 位?
6 位是标准里的默认值,也是可读性和安全性的平衡点。算法先得到一个很长的数字,再取末尾 6 位。少数平台用 8 位。
同一把密钥在两台设备上算出的码会一样吗?
只要两台设备的时间都准,结果就一样。算法输入只有密钥和时间,没有设备相关的东西参与。
TOTP 和短信验证码有什么区别?
短信验证码由平台生成后发给你,要经过运营商网络。TOTP 由你的设备本地算出,不经过任何传输,也就没有短信被拦截或 SIM 卡被转移的问题。

在浏览器里生成 TOTP 验证码

粘贴 Base32 密钥,立刻得到 6 位验证码,每 30 秒刷新。

打开 2FA 验证码生成器→
继续阅读

相关文章