密码熵:如何衡量和最大化你的密码强度
你可能听过”强密码”要包含大写字母、数字和特殊字符。但 P@$$w0rd! 满足所有这些规则,却能在不到一秒内被破解。
密码强度的真正衡量标准不是你用了什么字符,而是熵——一个来自信息论的概念,用来量化密码的不可预测性。
本文将详解密码熵的工作原理、计算方法,以及如何生成真正难以破解的密码。
什么是密码熵?
密码熵衡量的是密码的不可预测程度,以「比特」(bit)为单位。每增加一个比特的熵,攻击者需要尝试的次数就翻一倍。
可以用骰子来类比:一个 6 面骰子每次投掷有约 2.6 比特的熵——只有 6 种可能。一个 20 面骰子有约 4.3 比特——面越多,不确定性越大。
密码的原理相同:可选字符越多(骰子面数越大)、密码越长(投掷次数越多),熵就越高。
这就是为什么熵比复杂度规则更科学。
一个看起来很复杂的密码(Tr0ub4dor&3)可能熵很低,因为它遵循了可预测的模式。而一个看起来简单的密码短语(correct horse battery staple)反而有很高的熵,因为它从一个巨大的词池中随机选取。
计算公式:如何计算密码熵
公式很简单:
E = L × log₂(R)
其中:
- E = 熵(比特)
- L = 密码长度(字符数)
- R = 字符池大小(每个位置可能的字符数)
字符池大小对照表
| 字符类型 | 池大小 (R) | 每字符熵(比特) |
|---|---|---|
| 纯小写字母 (a-z) | 26 | 4.70 |
| 小写 + 数字 | 36 | 5.17 |
| 大小写 + 数字 | 62 | 5.95 |
| 全部可打印 ASCII | 94 | 6.55 |
| Diceware 词表 | 7,776 | 每词 12.92 |
用代码计算
// 用 JavaScript 计算密码熵
const entropy = (length, poolSize) =>
length * Math.log2(poolSize);
entropy(8, 26); // → 37.60 比特(纯小写)
entropy(12, 62); // → 71.45 比特(大小写+数字)
entropy(16, 94); // → 104.87 比特(全字符集)
import math
def entropy(length: int, pool_size: int) -> float:
return length * math.log2(pool_size)
entropy(8, 26) # → 37.60 比特
entropy(12, 62) # → 71.45 比特
entropy(16, 94) # → 104.87 比特
注意:这个公式假设每个字符是均匀随机选择的。如果是人为选择的密码(使用常见单词或模式),实际熵远低于理论值。
多少熵才够安全?
答案取决于你保护的是什么,以及攻击者的猜测速度。
现代 GPU 对 MD5 等快速算法每秒可测试超过 10¹²(一万亿)个哈希。以下是不同熵值的实际意义:
| 熵值(比特) | 强度 | 破解时间(10¹² 次/秒) | 建议用途 |
|---|---|---|---|
| < 40 | 弱 | 不到 1 秒 | 不建议使用 |
| 40–59 | 一般 | 秒到小时 | 临时账号 |
| 60–79 | 强 | 天到数百年 | 普通账号 |
| 80–99 | 很强 | 千年以上 | 邮箱、银行 |
| 100+ | 极强 | 超过宇宙热寂时间 | 加密密钥、主密码 |
使用全部可打印 ASCII 字符的 16 位密码有约 105 比特的熵——已经达到「极强」级别。你可以用我们的随机密码生成器即时生成,工具会实时显示每个密码的熵值分析。
NIST 新指南(2024 更新)
美国国家标准与技术研究院(NIST)SP 800-63B 在 2024 年做了重大更新:
- 取消了强制复杂度规则(不再要求必须包含特殊字符)
- 取消了强制定期更换密码
- 提高最低密码长度至 15 个字符(原为 8 个)
- 强调筛查已泄露的密码
- 鼓励长度和随机性优先于复杂度
这些变化印证了熵的数学原理一直在告诉我们的:长度和随机性比字符种类更重要。
为什么长度比复杂度更重要
来看一下数学。考虑两种提升 12 位密码熵的方式:
方案 A —— 保持 12 位,从大小写+数字(62)换成全 ASCII(94):
- 12 × log₂(94) - 12 × log₂(62) = 78.66 - 71.45 = +7.21 比特
方案 B —— 保持大小写+数字(62),多加一个字符(12 → 13):
- 13 × log₂(62) - 12 × log₂(62) = 77.40 - 71.45 = +5.95 比特
增加一个字符带来的熵提升,几乎和切换到更大字符集一样多。加两个字符就超过了。
再看 P@$$w0rd!(9 个字符)。它虽然使用了全 ASCII 字符集,但太短了。更糟的是,它遵循了可预测的「leet speak」模式,字典攻击早已覆盖,所以实际熵远低于理论值 59 比特。
结论:对于真正随机的密码,增加长度比增加字符种类更高效。但真正的敌人是可预测性,不是长度不够。
密码短语 vs 随机密码
| 维度 | 随机密码 | 密码短语(Diceware) |
|---|---|---|
| 示例 | kX#9mP$2vL!nQ7wR | correct horse battery staple |
| 每单位熵 | 6.55 比特/字符 | 12.92 比特/词 |
| 达到约 78 比特需要 | 12 个字符 | 6 个词 |
| 可记忆性 | 差 | 好 |
| 手机输入 | 困难 | 方便 |
| 最适合 | 密码管理器中的密码 | 主密码、需要记忆的登录 |
Diceware 的工作原理
Diceware 使用一个包含 7,776 个词的词表(6⁵ = 7,776)。每个词通过掷五次骰子选出,提供恰好 12.92 比特的熵。
四个词约 51 比特;六个词约 77 比特。
该选哪个?
- 存在密码管理器里的密码:使用 16 位以上、全字符集的随机密码。你不需要手动输入,所以记忆性不重要。我们的随机密码生成器可以一次批量生成最多 50 个。
- 主密码:使用 5-6 个词的 Diceware 密码短语。它足够好记,又能提供 64-77 比特的熵。
- API 密钥和令牌:使用
openssl rand或crypto.randomBytes()获取最大熵值,不需要考虑人类记忆。
实战:开发者工具与代码
以下是开发者生成高熵密钥的常用方式:
浏览器(Web Crypto API)
// 密码学安全的密码生成
function generatePassword(length, charset) {
const array = new Uint32Array(length);
crypto.getRandomValues(array);
return Array.from(array, v => charset[v % charset.length]).join('');
}
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*';
generatePassword(16, chars);
// → 'kX#9mP$2vL!nQ7wR'(每次随机)
Node.js
const crypto = require('crypto');
const token = crypto.randomBytes(32).toString('base64url');
// → 'Ql2Hj8xK9mNp3rVw5tYz7uBa0cEf4gIk'(43 字符,256 比特)
Python
import secrets
token = secrets.token_urlsafe(32) # 256 比特的熵
password = secrets.token_hex(16) # 128 比特,十六进制格式
命令行
# 192 比特的熵,base64 编码
openssl rand -base64 24
# 256 比特,十六进制编码
openssl rand -hex 32
各方法熵值对比
| 方法 | 输出长度 | 熵(比特) |
|---|---|---|
| UUID v4 | 36 字符 | 122 |
openssl rand -base64 24 | 32 字符 | 192 |
| 16 位全 ASCII | 16 字符 | 105 |
| 6 词 Diceware | 约 30 字符 | 78 |
| 4 词 Diceware | 约 20 字符 | 52 |
永远不要用
Math.random()做任何安全相关的事。它使用的是非密码学安全的伪随机数生成器——如果攻击者知道种子,输出就是可预测的。浏览器中请用crypto.getRandomValues(),Node.js 中请用crypto.randomBytes()。
密码存储:为什么仅有熵还不够
即使是 128 比特的密码,如果服务端以 MD5 明文哈希存储,也形同虚设。数据库泄露后,攻击者在单张 GPU 上每秒就能测试数十亿个 MD5 哈希。
这就是慢哈希算法的用武之地。它们的设计目标就是让每次猜测都变得昂贵:
| 算法 | GPU 上的速度 | 等效减速 |
|---|---|---|
| MD5 | 约 100 亿次/秒 | 基准线(不要使用) |
| SHA-256 | 约 50 亿次/秒 | 约 2 倍 |
| bcrypt (cost=12) | 约 5 次/秒 | 约 20 亿倍 |
| argon2id | 约 2 次/秒 | 约 50 亿倍 |
bcrypt 的 cost 参数设计得很优雅:每增加 1,所需计算量翻倍。cost=12 意味着 2¹² = 4,096 轮哈希运算,相当于在密码本身的熵之上额外增加了 12 比特的「存储熵」。
双重保护模型:高熵密码防御离线暴力破解,慢哈希防御数据库泄露。两者缺一不可。
想深入了解哈希算法的差异,请阅读 MD5 与 SHA-256 对比,也可以试试 MD5 哈希生成器来直观感受不同算法的输出。
常见密码迷思破除
”每 90 天换一次密码”
NIST 2024 版指南明确建议不要强制定期更换密码。频繁更换会导致用户选择更弱、更可预测的密码——在末尾加数字、在几个密码间轮换。除非有证据表明密码已泄露,否则不需要更换。
“a→@、e→3 能增强安全性”
Leet speak 替换是字典攻击最先尝试的模式之一。在 password 中把 a 替换成 @ 几乎不增加熵,因为攻击者早已预料到这种替换。
真正的随机性——而非巧妙的替换——才是提升熵的关键。
“8 位加特殊字符就够了”
即使用了全部 94 个 ASCII 字符,8 位密码也只有 52 比特的熵。以每秒 10¹² 次的猜测速度,大约 75 分钟就能破解。
至少使用 12 位,重要账号建议 16 位以上。
“看起来越复杂就越安全”
视觉复杂度和熵是两码事。Tr0ub4dor&3 看起来很复杂,但遵循了可预测的「基础词+替换」模式。mfYq8kL2nR 看起来更简单,但因为是真正随机的,熵反而更高。
更多安全策略请参阅Web 安全最佳实践。
常见问题
多少比特的熵才算安全?
对于大多数在线账号,60-80 比特就能提供强保护。对于高价值目标(如主密码或加密密钥),建议 100 比特以上。每多一个比特,攻击者需要的时间就翻倍。
添加特殊字符一定能增加熵吗?
只有在字符是从完整池中随机选取时才能。可预测的替换(如 @ 代替 a、末尾加 !)几乎不增加熵,因为攻击者的字典早已包含这些模式。
4 个词的 Diceware 密码短语有多少熵?
使用标准的 7,776 词 Diceware 词表,每个词贡献 12.92 比特。四个词约 51.7 比特——对低安全需求足够。重要账号建议使用 5-6 个词(64-78 比特)。
Math.random() 能用来生成密码吗?
不能。Math.random() 是一个非密码学安全的伪随机数生成器。请在浏览器中使用 crypto.getRandomValues(),在 Node.js 中使用 crypto.randomBytes() 来生成安全随机数。
bcrypt 的 cost 参数如何影响安全性?
bcrypt 的 cost 每增加 1,哈希计算量翻倍(暴力破解也相应翻倍)。cost=12 意味着 2¹² = 4,096 次迭代,相当于在密码本身的熵之上增加了 12 比特的破解难度。
NIST 2024 年密码指南有什么变化?
NIST SP 800-63B 取消了强制复杂度要求(不再强制特殊字符、大小写混合)和定期密码轮换。新指南倾向于更长的密码(建议 15 个字符以上)、筛查已泄露密码库,以及允许所有可打印字符(包括空格)。
要点总结
- 熵 = L × log₂(R) —— 每多一个比特,破解所需猜测次数翻倍
- 长度 > 复杂度 —— 增加一个字符比扩大字符集更有效
- 使用密码学安全 API ——
crypto.getRandomValues()或crypto.randomBytes(),绝不用Math.random() - 密码管理器 + 随机生成是大多数人的最佳实践
- 服务端也很重要 —— 用 bcrypt 或 argon2,绝不要用 MD5 存储密码
想生成一个高熵密码?试试我们的随机密码生成器——它会实时显示每个密码的熵值分析。