UPTools UPTools
安全 主笔:工具匠

用 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-9103.32 bit
小写字母 a-z264.70 bit
字母数字(大小写+数字)625.95 bit
再加上常见符号~946.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 是为方便人输入,跟安全性基本无关。

相关文章