先说结论
这个问题背后的疑心恰恰是对的,留着它。造密码的页面,是全网唯一一种收不到任何敏感东西、却能留下全部敏感东西的页面:它的产出本身就是秘密。一个把产出发出去的生成器,不是工具不行,是密码收集器。问题从来不是这个生成页面看着可不可信,而是它想留的话,留不留得下。而这件事,难得地,查得了。
三样东西定胜负:随机数从哪儿来,结果能不能离开页面,结果有没有哪一点能被预测。三样都有老实的答案,访客自己就能验证。一个下载来的应用,在谁也看不进去的窗口里生成,可没有这待遇。
浏览器的随机数从哪儿来
浏览器里正经的生成器,打的都是同一口井:crypto.getRandomValues,浏览器的密码学随机数发生器,由操作系统用硬件噪声播种,而且不停地补种。浏览器取 TLS 密钥用的也是这口井,你和银行之间的加密就跑在那上面。桌面程序能摸到比网页更好的随机数?这话不成立,两边最后都落到同一口系统的井里。
页面不该用的是 Math.random(),掷骰子用的家常函数。浏览器用一个很快的生成器来实现它,而那个生成器的内部状态,看上手边几个连续输出就能重建。也就是说,架在它上面的密码看着随机,谁见过其中一个,就都算得出其余。这不是纸上谈兵,对市面上的生成器演示过不止一次。而且从外面看不出来,这正是代码能读的生成器最硬的一条理由:两个函数的差别,在源码里就一个词。
再往上还有一层细致。把随机的 32 位字用求余数变成“26 以内的数”,会朝前几个字母有一丝丝偏;讲究的生成器不取余数,重抽。这里的生成器就是这么做的。它避开的偏差大约是一亿六千五百万分之一,用起来看不见,但恰恰是这种细节,把为这件事造的工具和论坛上抄来的代码片段分开。
一个坏的生成页面能干什么
把砸锅的方式一条条点名,因为每一条都查得了。
- 把密码发出去。页面在本地生成,然后把造好的发走。随点击一起,捎在统计里,或者攒着以后发。这一条是一票否决,而且看得见:它必须是一次网络请求,请求盯得住。
- 在服务器上生成。密码不是从网络出去,是从网络来的。那运营者比你先见到它,它怎么造的你也无从知道。同一个检查,反着做。
- 生成得弱。
Math.random,拿时间戳当种子,几百个词的词表冒充强度。这一条网络面板抓不住,只有读得了的源码抓得住,或者一条按真实设置数出来的老实强度提示。 - 留历史。热心地替你记着最近二十个密码。存在比标签页活得久的地方,摆在一台可能是共用的机器上。
这个站的密码与口令短语生成器,从造法上就把四条都堵死了:只用 crypto.getRandomValues,生成在页面里完成,任何形式的存储都没有,没有历史,还有一行强度提示,把你这套设置下一共有多少种可能的结果数得清清楚楚。口令短语用的词表是 EFF 的 Diceware 词表,一字未动,随工具自己的文件夹一起带着。
怎么检查任何一个生成器,包括这个
完整的方法写在上传那一页。要挑一个页面先试,就试这个:
- 先拔网线。把页面打开,断网,然后再生成。没有连接时造出来的密码,不可能是外面取来的,也不可能在出生那一刻被送走。这个页面断网照常工作,这就是它的意义。
- 生成时盯着网络面板。按下按钮,读那份清单:什么都不该出去。然后复制密码,再看一眼。复制那一下,正是不老实的页面会挑的时机。
- 找收集器才需要的东西。账号,同步,“最近生成”的清单。有记性的生成器,手里就有副本。
结尾配得上一句老实话。检查告诉你的,是你盯着看时页面做了什么;公开发布、原样可读的代码,这个站上的一切都是如此,告诉你的是它一般会做什么。剩下的是机器本身:没有哪个网页能替你把密码挡住被攻破的浏览器或者记键盘的恶意软件,生成器也不例外。检查替你换来的东西更小,也更实在:一个任何服务器都从没见过的密码,出自一段允许你亲自读一遍的算术。