密码多长才安全:熵怎么算,长度和符号哪个更值

更新于 8 分钟阅读

很多网站的密码强度条在你输入一个感叹号之后就变绿,「必须包含符号」几乎成了常识。可是这条规则背后的算术很少有人摆出来。其实只有一个公式,自己就能算,算完会发现:在大多数情况下,长度比字符种类更管用。

熵就是「可能性的个数」

假设密码里的每个字符都是从一个包含 N 个字符的字符池里均匀随机抽出来的,那么长度为 length 的密码一共有 N^length 种可能,熵(以比特计)是:

熵 = 长度 × log2(N)

每多 1 比特,需要猜的空间就翻一倍。常见字符池每个字符贡献的比特数如下:

字符池大小每字符比特数
纯数字103.32
小写字母264.70
小写字母 + 数字365.17
大小写字母525.70
字母 + 数字625.95
字母 + 数字 + 本工具提供的 28 个符号906.49

第一行和最后一行相差不到两倍。也就是说,不管怎么挑字符池,每个字符最多只能让你多得一倍左右;而长度是乘上去的:20 个字符取自 90 字符池,约 129.8 比特。

加符号每位多半个比特,多加一位却多近六个比特

字符池从 62 变成 90,每个字符从 5.95 比特涨到 6.49 比特,只多 0.54。16 位的密码这样一共多约 8.6 比特。而在 62 字符池里多加一位,就多 5.95 比特。把同样的「输入成本」拿来对比:

选择熵
16 位,字母加数字95.3 比特
15 位,四类字符全用97.4 比特
12 位,四类字符全用77.9 比特
14 位,字母加数字83.4 比特

所以符号有一点用,主要价值在于网站限制了最大长度的时候。没有长度限制时,多加两位更省事。另外符号还有公式里看不到的代价:有些网站会拒绝其中某几个;$、\ 这类字符放进命令行、网址或配置文件里还可能被转义或吃掉。只有字母和数字的长密码就没有这些麻烦。

只用小写字母的情况值得单独说一句:要超过 80 比特需要 18 位(17 位只有 79.9),而 90 字符池 13 位就够了。

字符池指的是「能抽什么」,不是「抽出来是什么」

字符池取决于生成器可以从哪些字符里选,而不是成品密码里碰巧出现了哪些。如果符号开着、而某条密码里恰好一个符号也没有,它的字符池依旧是 90。本站的密码生成器按「排除之后、去重之后」的字符池计算 长度 × log2(字符池大小),所以在附加字符框里输入池里已有的字符,不会增加熵。

「排除易混字符」会去掉 I、l、1、O、0 和 | 这六个,完整字符池从 90 缩到 84,20 位密码从 129.8 比特降到约 127.8 比特。如果你要念给别人听,或者要照着纸抄,这点损失很划算。

为什么不能用 Math.random

Math.random() 不承诺不可预测。语言规范只要求它的结果看起来均匀,并没有说见过几个输出的人猜不出下一个。密码生成需要的恰恰是后一个性质。浏览器为此提供了 crypto.getRandomValues,这是由操作系统提供种子的密码学安全随机数生成器。本站的工具只用它;如果浏览器没有这个函数,工具会直接报错,不会悄悄改用 Math.random。

光有好的随机源还不够,因为把随机字节变成字符下标的常见写法有偏差:

const index = byte % pool.length; // 有偏差

一个字节有 256 种取值。字符池为 62 时,256 = 4 × 62 + 8,所以下标 0 到 7 对应 5 个字节取值,其余 54 个下标只对应 4 个。前 8 个字符出现的概率因此高出 25%。解决办法是拒绝采样:只接受小于「池大小的最大整数倍」的字节(62 时是 248,90 时是 180),否则重抽。重抽并不罕见:90 字符池下约有三成字节(256 个里的 76 个)会被丢弃。代价很小,换来的是每个字符机会均等——熵公式依赖的正是这个前提。

「每类至少一个」会让数字变得略高

不少网站要求密码里必须有数字和符号。生成器的「每类至少一个」选项会为这些字符预留随机位置,其余位置再从整个字符池里抽。这样得到的就不再是完全均匀的抽样,因为「完全没有数字」的密码永远不会出现,所以显示的比特数是轻微高估。长密码里这点损失很小,因为不加约束时大多数结果本来就包含各类字符;短密码又要求四类字符时损失会大一些。如果网站没有这个要求,关掉这个选项,数字就是精确值。

工具对此还有一道保护:长度小于所要求的字符类数量时,它会报错,而不是悄悄丢掉某一类。

熵回答不了的三件事

熵描述的是密码「怎么产生」,而且公式只对均匀随机选择成立。由此有三个边界。

  • 人想出来的密码不适用。 Tr0ub4dor&3 看上去像 11 位、90 字符池,但它是由一个词典单词加上套路化替换变来的,真实熵远低于 11 × 6.49。把单词里的字母换成数字,只能挡住不知道这套习惯的攻击者。
  • 重复使用比熵更致命。 一个 130 比特的密码用在两个网站,安全性等于两者里存储更差的那个。其中一个泄露,另一个也就跟着失守,和比特数无关。工具里也提醒了:不要把生成的密码用在已经有密码的账号上,交给密码管理器,一个账号一个密码。
  • 设备失守时全部落空。 键盘记录器或恶意扩展会在你输入或复制密码时直接读到它。熵防的是被猜出来,防不了被偷走。

生成器里的强度档位(45 比特以下为弱,到 63 为一般,到 79 为良好,80 及以上为很强)只是一把方便的尺子,不是经过测量的威胁模型。实际需要多少比特,取决于攻击者面对的是限速的登录页面,还是自己拿到手的哈希并用自己的硬件离线尝试,而这里的公式并不知道是哪一种。

心算:每位约 6 比特

不想拿计算器的话,记住一个近似就够了:字母加数字池里每位约 6 比特,完整字符池每位约 6.5 比特。想要 80 比特,大约 13 到 14 位;想要 100 比特,大约 17 位(17 × 5.95 = 101.2);想要 128 比特,字母数字池需要 22 位(21 位只有 125),完整字符池需要 20 位(19 位为 123.3)。

这些数字有个共同的前提:每一位都是抽签抽出来的。只要你自己动手改过几个字符,比如「把 a 换成 @」「末尾补个 1」,上面的乘法就不再成立,因为这些改动是可预测的。想验证一条密码到底有多强,最老实的办法是回头看它是怎么生成的,而不是看它长什么样。

在线猜测和离线破解,需要的比特数不一样

同一个密码面对不同的攻击场景,需要的强度并不相同。如果攻击者只能在登录页面上一次次试,网站通常会限速、锁定账号或者要求验证码,能尝试的次数非常有限。如果攻击者拿到了网站泄露的密码哈希,就能在自己的机器上不受限制地离线尝试,这时候能撑多久,取决于网站用什么方式存储密码,而这一点你从外面看不到。也就是说,你无法知道自己的密码会落在哪种场景里,所以长度应该按较坏的那种情况来选,而不是按登录页面的限速来选。

几个常见的误区

  • 「把 a 换成 @、o 换成 0」不算增加熵。 攻击者的字典早就包含这些替换,它们只让密码更难记,没有让它更难猜。
  • 定期强制换密码不会提高随机密码的强度。 一个随机生成的长密码,只有在你怀疑它泄露时才需要更换;换成「原密码末尾加个数字」的版本,则是用可预测的改动抵消了随机性。
  • 不要自己「挑」随机字符。 人在凭感觉挑「看起来随机」的字符时,会偏爱某些位置和某些字符。这就是要用生成器的原因:它的每一位都是抽签,而不是挑选。
  • 主密码是最后一道关。 密码管理器里的每个密码可以是 20 位的随机串,但管理器本身的主密码需要你记住。这种情况下,用下一节提到的随机词组更合适,它是整个体系里唯一需要靠记忆的一条。

到底选多长

只靠密码保护的账号,用完整字符池取 16 位约 104 比特,足够稳妥。网站最多只让你用 12 位,四类字符全开也有约 78 比特。有密码管理器帮你填写时没有理由选短的;工具把长度上限定在 64,是因为有些网站会悄悄截断更长的输入,被截断的密码你以后可能怎么也输不对。

如果需要背下来,随机抽取单词的做法遵循同样的算术:从 7776 个单词的词表里每抽一个词是 12.9 比特,六个词约 77.5 比特。本站的生成器只生成字符密码,要生成词组需要掷骰子或另找工具。

打开工具: 强密码生成器

返回指南列表

更多指南