Skip to content
返回博客
安全

otpauth URI 详解:从 2FA 二维码截图找回验证器密钥

2FA 设置二维码里只有一行 otpauth:// 文本,密钥就在其中。逐字段看懂这行 URI,再用两款浏览器内工具识别截图、核对密钥,在新手机上恢复验证码。

7 分钟

开启两步验证时网站给你的那张二维码,扫出来只是一行文本:otpauth://totp/...。其中 secret= 后面那串 Base32 字符就是密钥。有了这一行,标准的身份验证器就能算出和原来完全相同的验证码。所以,只要还留着当初那张设置二维码的截图,而且服务端后来没有换过密钥,换手机后就能把 2FA 恢复回来。

Google 身份验证器(常被叫作「谷歌验证器」)里「转移账号」导出的二维码是另一回事,它是给 App 自己的导入功能用的,后文单独说明。

下面先把这行 URI 逐个字段拆开,再用两个浏览器内的工具走一遍恢复流程:识别截图、拿旧设备核对密钥、在新 App 里重新添加。文中示例一律使用规范里公开的测试密钥 JBSWY3DPEHPK3PXP,不涉及任何真实密钥。

otpauth URI 逐字段拆解

格式以 Google Authenticator 项目的 Key Uri Format 为准:

otpauth://TYPE/LABEL?PARAMETERS

一条常见的设置 URI 长这样:

otpauth://totp/Example:alice@example.com?secret=JBSWY3DPEHPK3PXP&issuer=Example
字段是否必填 / 默认值注意事项
type必填这里是 totp。hotp 是基于计数器的变体,用 counter 参数代替周期
label必填例如 Example:alice@example.com:账户名,前面可加颁发者和冒号(冒号可写成 %3A);两部分本身都不能含冒号
secret必填Base32 编码,只有字母 A–Z 和数字 2–7;按规范应省略 = 填充
issuer强烈建议;缺省时可取 label 前缀和 label 前缀同时出现时,两者应当一致
algorithm可选,默认 SHA1另外两个取值是 SHA256、SHA512
digits可选,默认 6只能是 6 或 8
period可选,默认 30每个验证码的有效秒数

读真实 URI 时,最容易卡住的是两点:

  • 百分号编码(percent-encoding):label 和 issuer 都经过 URL 编码,%20 是空格,%40 是 @,%3A 是冒号。规范自己的例子就是 ACME%20Co:john.doe@email.com。
  • 没写的参数取默认值:只有 secret 和 issuer 的 URI,就是 SHA-1、6 位、30 秒的普通 TOTP。手动抄参数时,这几个默认值也要一并记下。

这份规范页最后一次修改是在 2018 年。页面上当年的备注写着:Google 身份验证器忽略 algorithm 和 period,Android 版还忽略 digits。如今各 App 的实际行为,规范页没有说明。所以 URI 里只要有非默认值,在新 App 里添加后都要核对一下验证码。

密钥、时间和 HMAC 是怎样算出 6 位数的,见 TOTP 工作原理详解。

第一步:从截图里读出 URI

打开二维码识别,选择、拖入或直接粘贴截图即可。图片只在当前浏览器本地识别,不会上传。

识别结果会把内容归为「文本」,并完整显示原始内容。工具不会把 otpauth:// 当作特殊类型处理,也就不会替你拆出密钥、颁发者或算法,需要你对照上面的表格自己读:

otpauth://totp/Example:alice@example.com?secret=JBSWY3DPEHPK3PXP&issuer=Example
                                                ^^^^^^^^^^^^^^^^ 密钥

如果识别出来的文本不是以 otpauth://totp/ 开头,就不要往下做了,本文后面的步骤不适用。

第二步:拿旧设备核对密钥

在依赖这个密钥之前,先确认它是对的。旧设备还能用的话,把两边的验证码放在一起比一比:

  1. 打开 TOTP 验证码生成器,停留在「生成」标签页。
  2. 在「Base32 密钥」里只粘贴 secret= 后面的那段值。整条 URI 粘进去是不行的:这个输入框只接受 Base32,会直接提示密钥无效。
  3. URI 里写了 algorithm、digits 或 period 的,展开「高级选项」逐项对上;没写就保持默认。
  4. 把「当前验证码」和旧设备上的验证码对照,同一个时间窗口内两者应当完全一致。

对不上时,先查电脑时钟准不准,再逐项检查参数。密钥里错一个字符,所有验证码都会不同。

习惯用命令行的话,Python 库 pyotp 可以直接读整条 URI:

import pyotp

uri = "otpauth://totp/Example:alice@example.com?secret=JBSWY3DPEHPK3PXP&issuer=Example"
print(pyotp.parse_uri(uri).now())

第三步:在新设备上恢复

  1. 在 TOTP 验证码生成器里保留第二步填好的密钥和高级选项,切换到「设置」标签页。
  2. 按 label 里的内容填写「颁发者」和「账户」,新 App 里才会显示能认出来的名字。
  3. 用新手机上的身份验证器扫描屏幕上的二维码,确认它显示的验证码和「生成」标签页一致。
  4. 用新 App 里的验证码登录一次对应的服务,确认能用。

「设置」标签页还会显示重新生成的 otpauth:// URI,有些密码管理器支持直接粘贴这条 URI。二维码生成器可以把任意文本做成二维码图片,但用这条 URI 做出来的二维码,和原来那张截图一样敏感。

截图可能外泄过,就换一个密钥。 到该服务的安全设置里重新设置一次两步验证,让账户换上新密钥,再确认用旧密钥算出的验证码已经无法登录。这样一来,旧截图落到谁手里都没用了。

Google 身份验证器导出的二维码:为什么这里用不了

Google 身份验证器有自己的转移流程:在旧手机上依次点按「转移账号 → 导出账号」生成二维码,再在新手机上点按「转移账号 → 导入账号」扫描。一次导出多个账号时,旧手机可能会生成多个二维码(见 Google 帐号帮助)。

对这种二维码,Google 只说明了用「导入账号」去扫,并没有公开里面装的是什么。本文的步骤针对的是上面介绍的标准 otpauth://totp/ 设置 URI。如果你识别出导出二维码的内容,发现不是以 otpauth://totp/ 开头,本站的 TOTP 验证码生成器就用不了它,请按 Google 的说明在新手机上导入。

同一页帮助还提到:验证码保存在 Google 账号里的,在新设备的 Google 身份验证器中登录账号后会自动同步。这种情况下,可能根本用不到二维码。

保管好密钥

  • 密钥本身就是第二因素。 TOTP 验证码由密钥和当前时间算出,谁拿到这条 URI 或二维码截图,谁就能一直算出有效的验证码,直到这个密钥被换掉。
  • 设置截图要按密码的标准保存。 放进加密的密码管理器或加密磁盘都可以;会自动同步到云相册的手机相册不行。用不着的副本及时删掉。
  • 不要把 URI 粘进搜索框或聊天窗口。 你在搜索框里输入的内容会发给搜索引擎,聊天消息则会留在别人的记录里。已经发生过的,按第三步的做法换掉密钥。

常见问题

很久以前的 2FA 二维码截图还能用吗?

只要服务端还在用这个密钥,就能用。二维码里存的是固定文本,本身没有有效期。一旦你在服务端重置或重新设置了两步验证,旧密钥就失效了,截图也随之作废。

密钥里有小写字母或空格,还有效吗?

一般有效。Base32 的设计本身就不区分大小写(RFC 4648),空格往往只是为了方便阅读才加的。本站的 TOTP 验证码生成器会自动去掉空格并转成大写。A–Z 以外的字母、2–7 以外的数字都是无效字符。

URI 里为什么有 %40、%20 这样的字符?

label 和 issuer 经过了 URL 编码:%40 是 @,%20 是空格,%3A 是冒号。在新 App 里手动填账户名之前,先把它们还原。符合规范的密钥是不带填充的纯 Base32,不会出现百分号。

别人能从我的 6 位验证码反推出密钥吗?

实际上做不到。RFC 4226 的安全分析结论是:攻击者即使收集到大量有效验证码,也不比随机猜测强多少,最好的攻击方式只有暴力穷举。需要保护的是密钥,而不是验证码。

参考资料

标签: Two-Factor Authentication TOTP Security QR Code 2FA