浏览器工具
把你的密钥从 Google Authenticator 里取出来。
Google Authenticator 会给你一张装着手机里全部账号的二维码,然后不给你任何读它的办法。把那张码截图,拖到这里,你就能拿到每个账号的 base32 密钥和 otpauth 链接。
图片绝不会离开这个标签页。读取二维码、解析它、生成验证码,全都在你的浏览器里发生;断网之后照样用这个工具,你就能证明这一点。
工具
把导出码的截图拖到这里
也可以粘贴一张、选择文件,或者把摄像头对准手机。
解码在你自己的机器上、在这个标签页里完成,所以它必须靠 JavaScript 才能进行。这里没有任何服务器可以代劳。下面的内容讲的是这个格式,以及工具拿它做了什么;如果你更愿意自己跑一遍,源代码是公开的。
开始之前
把那张导出码当成密码来对待。
这不是走形式的警告。导出二维码比大多数人以为的更危险,具体有三点,值得在生成它之前先弄清楚。
它装的是每一个密钥,不是你验证码的一张照片
这张码里含有其中每个账号的原始密钥。拿到这张图片的人,可以在任何设备上、不需要你的手机、也不会被你察觉,无限期地为这些账号生成有效验证码。
改密码并不能吊销它
换新密码、换新手机或者删掉应用,都不会让一个已经导出的密钥失效。唯一有效的办法是在每个服务上把双重验证关掉再重新开启,这会签发一个新密钥,并让旧的作废。
截图不会随那一刻消失
截图会落进你的相册,再从那里进入你所用的任何照片备份。一年后翻到它的人,拿到的访问权限和你当时一模一样。请在你信得过的设备上做这件事,事后把图片删掉,包括「最近删除」相簿里的那一份。
返回的内容
每一个字段,以及它们各自的来处。
把这件事做对的理由是:你能分清哪些是解码出来的数据,哪些只是合理的默认值。两者都会显示出来,而且标注不同。
密钥
RFC 4648 的 base32,大写、不带填充,和身份验证器应用显示的完全一致。十字节的密钥是十六个字符,二十字节的是三十二个。这就是整个凭据。其余的一切都只是关于它的元数据。
发行方与账号
也就是服务方,以及它所属的那个账号。Google 有时把发行方放在它自己的字段里,有时又把它并进账号标签,写成「发行方:账号」,所以两处都会读取,并且分开显示。
算法、位数与类型
几乎总是 SHA-1、六位、基于时间,因为服务实际部署的就是这一套。导出内容里若有哪一项没有设置,就采用惯例上的默认值,并把该字段标注为默认值,而不是数据。
otpauth 链接
一个标准的 otpauth:// URI,由上面那些字段重新拼出来,而不是解码得到的,因此它正是另一个身份验证器所期待的样子。每一个还会附带一张二维码,可以直接扫进另一个应用。
周期,如实标注
每个账号报的都是三十秒,而这个值并不是从任何地方解码出来的。导出格式里有密钥、名称、发行方、算法、位数、类型和计数器的字段,却根本没有周期这个字段。Google Authenticator 对它导出的一切都使用三十秒,所以这里报的就是三十秒,并标注为默认值,免得被误当成码里本来就有的东西。
基于计数器的账号
少数账号是基于计数器而不是基于时间的,所以它们的验证码不会轮换。计数器按导出时的值显示。这类账号要小心处理:用掉一个验证码会让服务端的计数器往前走,而落在后面的副本就不再能用。
工作原理
一共是五次解码,一次接一次。
这里面没有一步是密码学,也没有一步需要服务器。整条链短到可以完整讲清楚,而这值得讲,因为通常的解释是「装这三个程序」。
这五步都由工具自己实现,没有用第三方代码。读取二维码是其中最大的一块,而且远远大过其余几步,因为浏览器不会替你做:内置的条码检测接口在 Safari 和 Firefox 里都没有,所以解码器是照着规范写出来的。
01
读取二维码
图像先降成黑白两色,找出三个角上的定位图案,穿过它们拟合出一张网格(照片斜着拍的话,就按透视拟合),把每个模块逐一采样,再由 Reed-Solomon 纠错修好摄像头拍错的地方。出来的是一串以 otpauth-migration://offline?data= 开头的字符串。
02
解码 data 参数
对这个参数做百分号解码,得到的是标准 base64,再解出几十个字节。有一个坑值得点名:不能用 URL 查询参数解析器去读它,因为那类解析器会按表单规则解码,把加号变成空格,而标准 base64 会用到加号。
03
解析 protobuf 消息
这些字节是一条没有公开文档的 protobuf 消息,里面装着可重复的账号记录、一个版本号、分片总数、分片序号和一个分片标识。每条账号记录有七个字段:原始密钥、名称、发行方,以及分别代表算法、位数、类型和计数器的数字。
04
把密钥重新编码为 base32
密钥拿到手时是原始字节。身份验证器应用是以 base32 显示它的,所以这里把这些字节再编码一遍,得到你会手动敲进另一个应用的那串字符。
05
生成一个验证码来核对
基于时间的验证码,就是用那个密钥对当前的三十秒区间做 HMAC,再截断成六位。HMAC 由浏览器自带的加密接口完成,所以这里没有一处是自己手写的密码学。如果验证码和你手机上显示的一致,提取就是对的,而这也是唯一真正重要的检验。
隐私
这个说法很窄,而且你可以验证它。
无法核实的隐私说法就是营销话术。所以下面是准确的那一条,以及你花十秒钟就能做的验证。
你的图片和你的密钥绝不会被传输
你拖进来的图片、解码出的数据、密钥和生成的验证码都留在这个标签页里。处理它们的代码里根本没有对 fetch、XMLHttpRequest、WebSocket、EventSource 或 sendBeacon 的调用;离线版还加了一份内容安全策略,让浏览器来强制执行这一点,而不是听代码的一面之词。
值得你自己做一遍的验证
把网络关掉,从缓存重新加载这个页面,或者打开下载好的单文件版,然后照样使用这个工具。它能用。任何需要服务器的东西都做不到这一点。
这个页面本身会加载什么
说准确一点,因为更宽泛的说法并不成立:这个页面会加载一段自己托管的小型分析脚本,它只记录页面地址,别的什么都不记。它碰不到这个工具,也看不到你提取出了什么。如果你还是介意,离线版没有任何一处外部引用。
什么都不保存
没有 cookie,没有本地存储,没有会话存储,没有数据库,没有缓存。重新加载会丢掉你已经提取出的一切,这只会让你不方便一次,却是永远正确的做法。
清除实际上做了什么
清除会把密钥字节覆写成零。这值得做,但它不等于把内存擦干净:JavaScript 里的字符串没法被覆写,引擎可能还留着我们和你都拿不到的副本。真正把这件事做完的是关掉标签页。老实说,任何页面都不能声称做得比这更多。
已公开,而且小到读得完
整个东西就是一个采用 MIT 许可、没有运行时依赖的软件包,所以它背后没有供应链需要你去审。二维码解码器、protobuf 读取器、base32 和 base64 编解码器,以及一次性密码函数,全都在里面;它的测试对照的是已公开的测试向量,而不是它自己。
拿到那张码
导出码藏在哪里。
它不叫导出密钥,也不叫备份,而且不在设置里。Google 把它归在换新手机这件事下面,所以大多数人从来没找到过。
- 01在存着这些账号的手机上打开 Google Authenticator。
- 02打开菜单,然后选择「转移账号」。
- 03选择「导出账号」,在它要求时完成身份验证。
- 04勾选你要的账号并继续。一张二维码就出现了。
- 05把那张码截图,或者用这个工具的摄像头对准手机。
如果你拿到了不止一张码
列表长的时候,Google 会把它拆到几张二维码上,并告诉你现在是第几张。把它们全部捕获,然后一张接一张地加进来。它们共用一个标识,各自带着自己的序号,所以无论顺序如何都能合并,工具也会说明还缺哪些分片。来自两次不同导出的码会被拒绝,而不是合到一起,因为把不同导出中彼此重叠的账号凑在一处,会得出一份你手机上从未存在过的列表。
用截图,而不是拍照
截图一次就能解出来。拍屏幕会带来反光、摩尔纹和透视变形,这些解码器都能处理,但都不必要。如果你拍的是别人的手机,就让二维码填满画面,手机正对着镜头。
之后
拿到密钥之后该怎么办。
把它转到另一个应用里
这里每个账号都有自己的一张二维码,用的是所有身份验证器都能读的格式,可以直接扫进另一个应用。手动输入 base32 密钥也行;如果你想确认扫描过程中没出错,这样做值得。
拿一个验证码和手机对一下
在你依赖这里的任何结果之前,把生成的验证码和 Google Authenticator 在同一时刻为同一个账号显示的验证码比一比。一致,密钥就是对的。不一致,密钥就是错的,结果里其他内容也都不重要了。
像存密码那样存它
密钥应该放进密码管理器里那个账号的条目下,而不是记在便签或表格里。有些管理器在你粘贴 otpauth 链接之后会替你生成验证码。有一点值得先想清楚:同时握着密码和第二重因素的管理器是一个单点故障,这个取舍你可能愿意接受,也可能不愿意。
删掉那份捕获
包括「最近删除」相簿里的那一份,而且要在你的照片图库把它同步到别处之前。然后清除工具里的内容,关掉标签页。
细节
具体规格。
这个工具做什么、不做什么,不带营销修饰。这里的每一行都由软件包自己的测试断言过。
- 运行方式
- 完全在你的浏览器里。没有服务器,没有账号,没有要安装的东西。
- 可读取
- Google Authenticator 的导出码(otpauth-migration),以及单账号的 otpauth 二维码
- 输入方式
- 拖放、粘贴、文件选择器,或设备摄像头
- 输出
- base32 密钥、发行方、账号名称、算法、位数、类型、计数器、otpauth 链接、一张导入用的二维码,以及一个实时验证码
- 网络
- 没有。工具里任何地方都没有 fetch、XMLHttpRequest、WebSocket、EventSource 或 sendBeacon。
- 存储
- 没有。没有 cookie、本地存储、会话存储、数据库或缓存。
- 离线
- 可以。单文件版在断网的情况下也能从你自己的磁盘上运行。
- 浏览器
- Chrome、Edge、Firefox,以及 Safari 16 或更高版本。摄像头需要安全页面,所以从本地文件打开时可能用不了。
- 依赖
- 运行时没有依赖。二维码编解码器、protobuf 读取器、base32 与 base64 编解码器,以及一次性密码函数,全都在这个包里。
- 许可协议
- MIT
值得一问的问题。
- 把我的导出码放进一个网站里安全吗?
- 老实的回答是:你不该把凭据放进一个你无法核实的页面里,这也正是这个页面做成可核实的原因。什么都不传输,源代码公开,还有一个你可以下载下来、断网运行的单文件版。如果你根本不想信任任何页面,那就用那一个。
- 有任何内容会被发到你们的服务器吗?
- 不会,而且我们这边也没有任何东西能接收它。解码、解析和验证码生成都在你的浏览器里运行。打开浏览器的网络面板,提取一次,你会看到根本没有请求。
- 没有互联网连接也能用吗?
- 可以。从代码仓库的发布页下载那个单个 HTML 文件,打开它。它没有任何一处外部引用,还带着一份直接禁止连接的内容安全策略。除摄像头以外一切都能用:对于从文件地址打开的页面,有些浏览器不会授予摄像头权限;拖入或粘贴截图则始终可用。
- 如果有人拿到了我的导出码,我该怎么吊销它?
- 单独吊销一个密钥是做不到的。到受影响的服务上把双重验证关掉,再重新设置一次,这会签发一个新密钥,让旧的变得没用。改密码做不到这一点,删掉应用或者重置手机也做不到。
- Google 给了我好几张二维码,接下来怎么办?
- 把它们全部捕获,一张一张地加进来。它们共用一个分片标识,各自带着自己的序号,所以无论顺序如何都能合并,工具也会告诉你还缺哪些分片。重复添加已经有的那一张没有坏处。来自两次不同导出的码会被拒绝,而不是合并,因为合起来的会是一份你手机上从未存在过的列表。
- 为什么每个账号都显示三十秒?
- 因为导出格式里没有周期字段,而 Google Authenticator 对所有账号都用三十秒。这个值是对的,但它不是从任何地方解码出来的,所以它被标注为默认值,而不是当成数据显示。如果某个服务用的是别的间隔,Google Authenticator 的导出内容也不会告诉你。
- 我有一个账号的验证码不会轮换。
- 那是一个基于计数器的账号,它按设计就不会轮换。它的计数器按导出时的值显示。对它要当心:用掉一个验证码会让服务端的计数器往前走,所以落在后面的副本就不再被接受。
- 支持哪些算法?
- SHA-1、SHA-256 和 SHA-512,用的是浏览器自带的加密接口。MD5 出现在这个格式的算法列表里,但浏览器无法计算它,所以不会为这种账号生成验证码;密钥和其余每个参数依然会正确返回。实际中服务用的是 SHA-1 和六位数字,而且无论如何,Google Authenticator 都会忽略其他设置。
- 摄像头用不了。
- 它需要安全页面,所以在普通 http 上打不开,从文件地址打开也可能不行。一次拒绝往往会被记住,所以去浏览器的网站设置里看一下。不管摄像头怎么样,拖入或粘贴截图在任何地方都能用。
- 它打不开我的 iPhone 照片。
- iPhone 拍的照片是 HEIC,除了 Safari,没有浏览器会解码它。改用截图而不是拍照,或者把文件转成 PNG 或 JPEG。截图无论如何都是更好的输入,因为它里面没有反光,也没有透视变形。
- 我能把这些转到另一个身份验证器里吗?
- 可以。每个账号都有自己的一张标准 otpauth 格式的二维码,所有身份验证器应用都能读,你也可以改成手动输入 base32 密钥。在你从手机上删掉任何东西之前,先拿一个生成的验证码和手机对一下。
- 我见过有人用 zbarimg 加一个 Go 程序来做这件事。
- 那是同样的三次解码,只是前面多了三次安装:读取二维码、对 data 参数做 base64 解码、解析 protobuf。这里把这些都放在一个页面里完成;如果你更想写脚本,背后的软件包也是公开的。这里没有包装那些工具:二维码解码器和 protobuf 读取器都是照着各自的规范写出来的。