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,暴力破解简直不要太轻松。

⚠️ 避坑指南: 我曾经在一个物联网项目中看到有人用 MD5 存设备管理员密码。结果数据库被拖,攻击者用彩虹表几分钟就还原了所有密码。从那以后,我定了个规矩:任何新项目,口令存储至少用 bcrypt。

4.2 加盐(Salt)技术:给每个密码加点“料”

加盐,就是在用户密码后面拼接一段随机字符串,然后再做哈希。这样做的目的是:即使两个用户密码相同,哈希值也不一样。

我习惯把盐比作“调料”——同样的食材,加不同的调料,做出来的菜味道完全不同。攻击者就算有彩虹表,也得为每个盐值重新生成一份,成本高得吓人。

4.2.1 加盐的正确姿势

  • 盐必须随机:用密码学安全的随机数生成器,别用时间戳或用户ID。
  • 盐必须足够长:至少 16 字节,推荐 32 字节。
  • 盐不能重复使用:每个用户、每次密码变更,都要生成新盐。
  • 盐和哈希一起存:盐不需要保密,它只是让彩虹表失效。
💡 核心要点: 加盐不是为了隐藏盐本身,而是为了增加攻击者的计算成本。一个 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);
}
📌 经验之谈: 参数设置不是越大越好。我在智能门锁项目里,把 memoryCost 设成 128 MB,结果低端芯片跑一次要 3 秒,用户体验极差。后来调到 32 MB,时间控制在 0.5 秒以内,安全性和体验都兼顾了。

4.4 知识体系总览

下面这张图,把本章的核心逻辑串起来了。你可以看到,从原始口令到安全存储,每一步都有讲究。

口令存储安全知识体系 口令存储安全知识体系 用户口令 加盐(Salt) 密钥派生函数(KDF)选择 bcrypt scrypt Argon2 安全存储的哈希值 盐 + 参数 + 哈希一起存

4.5 实践建议与避坑清单

说了这么多,最后给几条硬核建议。这些都是我在不同项目里踩过的坑换来的。

  1. 别自己造轮子:用现成的库,比如 libsodium、bcrypt.js、Argon2 的官方实现。自己实现的哈希函数,十有八九有漏洞。
  2. 成本参数要定期评估:硬件在进步,三年前合适的参数现在可能已经不够了。我每年都会重新评估一次成本参数。
  3. 考虑设备性能:智能设备往往资源受限。在树莓派上跑 Argon2 和服务器上跑,参数要区别对待。
  4. 永远不要存明文:这个不用多说了吧?但我真的见过。
  5. 做好降级预案:如果用户设备太老,算不动 Argon2,可以降级到 scrypt,但要有明确的版本标记。
⚠️ 我曾经踩过的坑: 在一个智能门锁项目里,我用了 bcrypt 的默认参数,结果在低功耗 MCU 上每次验证要 2 秒。用户都开完门了,锁还没反应过来。后来我把成本因子从 12 降到 8,时间控制在 300 毫秒以内,安全性也还能接受。记住:安全不是绝对的,是在成本和风险之间找平衡。

嗯,口令存储这块,核心就这些。记住一个原则:用最慢的算法,配最长的盐,设最合适的成本参数。这样就算数据库被拖,攻击者也拿你的用户没办法。