用 Math.random 生成密码为什么不安全?CSPRNG 与密码熵
前阵子在某个前端群看到有人贴了一段教程代码,封装了一个「随机密码生成器」,核心就是 Math.random()。评论区很快有人甩过来一句:「这玩意能被预测,别拿它生成密码。」贴代码那位不服,说随机出来的字符串长得挺像那么回事,怎么会不安全。
这事其实每年都在重复。很多人以为「看起来乱」就等于「不可预测」,但这两件事在密码学上差得很远。本文就把这个坑讲清楚:为什么 Math.random() 不能用,该用什么,以及密码强度到底怎么算。
Math.random() 为什么不能生成密码
Math.random() 在 V8 里是一个叫 xorshift128+ 的算法。它的特点:给定一个内部状态,下一步输出完全确定;只要你知道状态,整条随机数序列都能复现出来。状态从哪来?从一个种子。种子里有一项是当前时间戳。
问题就出在这。攻击者只要拿到足够多的输出样本(一般是几十次到上百次 Math.random() 的结果),就能反推内部状态,然后预测后面所有「随机」值。这种攻击不是理论,是有现成工具的(搜 zsxqier Math.random predictor 之类的项目)。说穿了,Math.random() 不是真随机,是一串可被反推的伪随机。
所以凡是用来生成密码、API Key、Token、Session Secret 的地方,碰 Math.random() 就是错的。哪怕你输出了 32 位看起来很乱的字符串,只要攻击者拿到过你的几条历史样本,剩下的全都能算出来。
该用 CSPRNG:crypto.getRandomValues()
浏览器里正确的做法是用 Web Crypto API 提供的 crypto.getRandomValues(),Node 里对应 crypto.randomBytes()。它们背后都是操作系统的 CSPRNG(密码学安全随机数生成器),Linux 上是 /dev/urandom,Windows 上是 CryptGenRandom,熵源来自硬件噪声、中断时间、磁盘抖动这些物理量。
和 Math.random() 的核心区别:CSPRNG 的输出不可预测,即便拿到任意多条历史输出,下一位仍像抛硬币。这也是密码学场景必须满足的性质。
代码写出来很短:
function randomChar(charset) {
const buf = new Uint32Array(1)
crypto.getRandomValues(buf)
return charset[buf[0] % charset.length]
}
实际工程里还得处理模 bias(取模会让分布不均),但思路就是这样:每次都向系统要新熵,而不是维护一个可被反推的内部状态。
密码强度怎么算:字符集 × 长度
用了 CSPRNG 只是保证「每一位都不可预测」。密码强不强还要看长度和字符集,这个值就是熵(entropy),单位是 bit。
公式:
熵 ≈ log2(字符集大小) × 长度
几个常见字符集的每位熵:
| 字符集 | 大小 | 每位熵 |
|---|---|---|
| 纯数字 0-9 | 10 | 3.32 bit |
| 小写字母 a-z | 26 | 4.70 bit |
| 字母数字(大小写+数字) | 62 | 5.95 bit |
| 再加上常见符号 | ~94 | 6.55 bit |
代几组数字感受一下:
- 8 位纯数字:
3.32 × 8 ≈ 27 bit,按 NIST 估算的强度下限差得远,离线暴力几小时就能跑完。 - 12 位字母数字:
5.95 × 12 ≈ 71 bit,NIST SP 800-63B 对应的最低强度档。 - 16 位字母数字:
5.95 × 16 ≈ 95 bit,离线爆破成本远在合理预算之外。 - 16 位含符号:
6.55 × 16 ≈ 105 bit,同长度但熵更高。
NIST SP 800-63B 给的建议是:用户自己设的口令至少 8 位;随机生成的口令至少 12 位达到最低强度。实际操作我的习惯是普通账号 12 位起,邮箱、支付、Git 这种 16 位打底。
注意一个反直觉点:加长度比加符号划算。同样从字母数字扩到含符号,每位熵只多了 0.6 bit;而每多一位字母数字,熵直接多 5.95 bit。所以与其纠结「要不要再叠几个特殊字符」,不如直接把长度拉到 16 位以上。
排除易混淆字符不影响安全性
密码生成器一般都有「排除 0/O、1/l/I」这种选项。这件事不影响熵的安全上限,字符集从 62 减到 58 左右,每位熵从 5.95 降到 5.86,差别可以忽略。
它真正的价值是降低人工输入的错误率。你把生成的密码粘进系统时不用费劲分辨到底是 0 还是 O,是 1 还是 l。这跟安全性无关,跟可用性有关。一旦切换到密码管理器自动填充,这个选项其实可以关掉。
实操
讲了一堆原理,想直接生成一个符合 CSPRNG 标准的强密码,可以丢进 UPTools 密码生成器。它背后调用 crypto.getRandomValues(),可以自定义长度、字符集,勾选排除易混淆字符,还能批量出多条。整个生成过程在浏览器本地,不上传任何字符。
三句话总结
Math.random() 是可被反推的伪随机,碰都不要碰它去生成密码或 Token,必须用 crypto.getRandomValues()。密码强度等于 log2(字符集) × 长度,16 位字母数字大约 95 bit 熵,远高于 NIST 最低线。加长度比加符号划算,排除 0/O、1/l/I 是为方便人输入,跟安全性基本无关。
相关文章
- 为什么在线工具比本地软件更注重隐私?本地处理如何保护你的数据 同样是处理敏感文件,为什么说浏览器本地完成的在线工具反而比某些本地软件更注重隐私?本文讲清本地处理的原理、它保护了什么、以及它的边界。
- SVG 直接内联 HTML 会触发 XSS?压缩与安全检测一次讲清 SVG 不只是图片,它是 XML,能内嵌 script 和事件属性。用 img 引入不会执行脚本,直接内联进 HTML 就可能触发 XSS。本文讲清风险边界和压缩要点。
- URL 里的中文和空格怎么传?encodeURI 和 encodeURIComponent 的区别 URL 里传中文搜索词或回跳地址,用 encodeURI 还是 encodeURIComponent 用错就会乱码或参数解析错乱。本文讲清两套 API 的区别和常见坑。