4. 口令存储安全:哈希算法、加盐与密钥派生函数
口令存储,是智能设备安全里最容易翻车的地方之一。我见过太多团队把用户密码直接存明文,或者简单做个 MD5 就以为万事大吉。说实话,每次看到这种设计,我都替他们捏把汗。
这一章,我们来聊聊口令存储的核心技术。说白了,就是怎么把用户的口令变成一串谁也看不懂的密文,而且就算数据库被拖库,攻击者也拿它没办法。
4.1 哈希算法:不只是“加密”那么简单
很多人把哈希和加密混为一谈。其实区别很大:加密是可逆的,哈希是不可逆的。哈希算法就像一台绞肉机——你把整块肉放进去,出来的是肉馅,但再也变不回整块肉了。
口令存储用的就是这种“单向”特性。服务器只存哈希值,不存明文。用户登录时,把输入的密码再做一次哈希,比对两个哈希值是否一致。
4.1.1 常见哈希算法对比
| 算法 | 输出长度 | 安全性 | 适用场景 |
|---|---|---|---|
| MD5 | 128 bit | 已破解,碰撞攻击可行 | 不推荐用于口令存储 |
| SHA-1 | 160 bit | 已破解,存在理论碰撞 | 不推荐用于口令存储 |
| SHA-256 | 256 bit | 目前安全,但计算速度太快 | 可用于校验,不适合直接存口令 |
| bcrypt | 可变 | 安全,内置加盐,可调成本 | 推荐用于口令存储 |
| scrypt | 可变 | 安全,抗硬件加速 | 推荐用于口令存储 |
| Argon2 | 可变 | 最安全,抗 GPU/ASIC | 强烈推荐用于口令存储 |
看到这个表,你可能会问:为什么 SHA-256 安全却不推荐?原因很简单——它太快了。现代 GPU 每秒能算几十亿次 SHA-256,暴力破解简直不要太轻松。
4.2 加盐(Salt)技术:给每个密码加点“料”
加盐,就是在用户密码后面拼接一段随机字符串,然后再做哈希。这样做的目的是:即使两个用户密码相同,哈希值也不一样。
我习惯把盐比作“调料”——同样的食材,加不同的调料,做出来的菜味道完全不同。攻击者就算有彩虹表,也得为每个盐值重新生成一份,成本高得吓人。
4.2.1 加盐的正确姿势
- 盐必须随机:用密码学安全的随机数生成器,别用时间戳或用户ID。
- 盐必须足够长:至少 16 字节,推荐 32 字节。
- 盐不能重复使用:每个用户、每次密码变更,都要生成新盐。
- 盐和哈希一起存:盐不需要保密,它只是让彩虹表失效。
4.3 密钥派生函数(KDF):专业的事交给专业的工具
普通的哈希算法是为数据完整性设计的,不是为口令存储设计的。密钥派生函数(KDF)才是专门干这个的。KDF 的核心思想是:慢,而且要能调慢。
你想想看,用户登录时等 0.5 秒完全能接受,但攻击者想暴力破解,每猜一个密码就要等 0.5 秒,一天下来也猜不了多少。这就是 KDF 的威力——用时间换安全。
4.3.1 主流 KDF 对比
| KDF | 内存消耗 | CPU 消耗 | 抗 GPU | 抗 ASIC |
|---|---|---|---|---|
| bcrypt | 低 | 中 | 一般 | 弱 |
| scrypt | 高 | 高 | 强 | 强 |
| Argon2id | 可调 | 可调 | 极强 | 极强 |
我个人最推荐 Argon2id。它是 2015 年密码学哈希竞赛的冠军,设计上同时考虑了 CPU 和内存的消耗,能有效抵御 GPU 和 ASIC 攻击。
4.3.2 代码示例:用 Argon2 存储口令
// 伪代码示例 - 注册时存储口令
function hashPassword(password) {
// 生成 32 字节随机盐
salt = randomBytes(32);
// Argon2id 参数配置
hash = argon2id(password, salt, {
timeCost: 3, // 迭代次数
memoryCost: 65536, // 64 MB 内存
parallelism: 4 // 并行线程数
});
// 存储格式:算法+参数+盐+哈希
store("$argon2id$v=19$m=65536,t=3,p=4$" +
base64(salt) + "$" + base64(hash));
}
// 验证时
function verifyPassword(password, storedHash) {
// 解析存储的哈希,提取参数、盐和哈希值
return argon2id.verify(storedHash, password);
}
4.4 知识体系总览
下面这张图,把本章的核心逻辑串起来了。你可以看到,从原始口令到安全存储,每一步都有讲究。
4.5 实践建议与避坑清单
说了这么多,最后给几条硬核建议。这些都是我在不同项目里踩过的坑换来的。
- 别自己造轮子:用现成的库,比如 libsodium、bcrypt.js、Argon2 的官方实现。自己实现的哈希函数,十有八九有漏洞。
- 成本参数要定期评估:硬件在进步,三年前合适的参数现在可能已经不够了。我每年都会重新评估一次成本参数。
- 考虑设备性能:智能设备往往资源受限。在树莓派上跑 Argon2 和服务器上跑,参数要区别对待。
- 永远不要存明文:这个不用多说了吧?但我真的见过。
- 做好降级预案:如果用户设备太老,算不动 Argon2,可以降级到 scrypt,但要有明确的版本标记。
嗯,口令存储这块,核心就这些。记住一个原则:用最慢的算法,配最长的盐,设最合适的成本参数。这样就算数据库被拖,攻击者也拿你的用户没办法。