密码学 05:随机数、salt、IV 与 nonce 各要求什么
订单服务给每个请求发一个 ID,钱包签交易时也会看到 nonce;密码数据库给每个用户密码加 salt,加密接口还让调用方传 IV 或 nonce。于是一个诱人的错误设计出现了:所有带这类名字的字段,都用一次 random() 生成,反正“随机就安全”。这会忽略两个相反的要求:有的值必须令攻击者难以预测,有的值最关键的是不能在同一作用域内重复;有些值甚至可以是单调计数器。
本篇选择四种具体场景,不教自己造随机数发生器。先看谁产生、谁保存、是否公开,再看如果重新使用一个值会发生什么。
1 | |
秘密需要不可预测,salt 不要求保密
例如生成一把要长期保管的对称密钥,必须用适合密码学用途的系统随机接口,不能从当前秒数、进程 ID、简单线性公式推算。攻击者能够猜中或枚举密钥,就没有必要破解算法本身。Python 标准库 secrets.token_bytes(32) 是本章示例使用的生成接口;实测只输出长度 32,不把字节值写进文章或日志。真正部署时还涉及密钥保存、访问控制和轮换,36 篇再处理。
密码 salt 服务于另一个问题:两名用户用了同一个弱密码,如果没有各自的 salt,数据库泄漏后攻击者可把预先准备的猜测结果复用到许多记录。salt 应按所选密码哈希规范给各记录提供足够多样性;它可以公开保存在密码记录旁,不用当作密钥。salt 不能让弱密码突然变强,也不能替代缓慢、耗内存的密码哈希。08 篇才实际运行 Argon2id 的合成弱密码实验;本篇不拿 sha256(password + salt) 当生产方案。
RFC 9106 建议密码哈希使用 128 位 salt,并推荐针对每个密码生成唯一值;这个规范场景里的 salt 与加密器的“每次调用 nonce”虽然都可以公开,约束却不相同。两者不应简单当作一类“必须不可预测的随机数”。
AES-GCM 的 nonce:同一密钥下切勿重复加密
认证加密需要处理密钥、nonce、明文、附加上下文以及密文和标签。RFC 5116 对 AES-GCM 特别警告:如果同一把密钥在两次加密不同明文时使用相同 nonce,既可能失去机密性,也可能损害完整性。生成规则必须在实际系统生命周期内保证同一密钥下唯一,不是“重启前似乎没撞过”。一些算法对 nonce 随机性、长度或误用抵抗能力另有要求,不能把本段对 GCM 的结论一字不改贴到所有 AEAD 上;07 会用成熟库核对具体接口。
实现者常考虑用每密钥计数器来生成 nonce。它看起来不“随机”,但在正确分配不重复的前提下,可能满足选定算法的唯一性要求;并发实例应分配互不重叠的区间,持久保存已使用进度或在重新开始时安全换密钥。更换密钥后,数值 0 可以重新出现,因为约束是 (密钥, nonce) 组合;这不意味着可以对旧密钥把计数器清零。IV 是不同加密方案对初始化输入的惯用名称,不保证每种模式对 IV 的要求都和 GCM nonce 相同;06 在具体模式上再解释。
签名算法也有一个叫 nonce 或临时 k 的值。ECDSA 如果复用或泄露相关的 k,可危及私钥;RFC 6979 给出了按规范确定性生成 ECDSA 签名随机值的方法。这件事与“服务端把请求 ID 放进已消费集合”是完全不同的机制。本章没有实现签名曲线或复现私钥泄漏,12、28 与 37 再结合成熟库、规范和具体向量处理。
业务序号:必须先知道由谁使用、在哪里消费
在 HTTPS 合成订单里,客户端可带订单 ID,服务端按已认证身份核对这个 ID 是否已用并原子地记录消费。ID 单调增长并非天然不安全;如果身份没绑定、消费状态不一致或两个并发请求都在写入前通过查询,再复杂的随机 ID 也挡不住重复效果。
Ethereum 的交易 nonce 与链上账户状态和具体交易规则相关;EIP-712 类型化签名的数据字段若只靠“我有签名”,应用不能自动获得防重放,需明确域、nonce、期限和消费记录。这里借它们提醒读者先定位作用域,不声称本章已经做过真实交易实验;Bitcoin 使用的 UTXO 约束也不能简单套成“每账户同一 nonce”。
可失败的最小实验
examples/cryptography/05_values.py 使用 secrets.token_bytes 生成 32 字节秘密和 16 字节公开 salt,只在输出里显示秘密长度及 salt 值。它还有一个纯教学用的 KeyNonceLedger:相同标签/计数器组合第二次被拒绝,但重新创建账本会忘记过去的记录。账本输出只是“是否见过输入对”的布尔值,不执行 AES-GCM,也不代表已验证攻击者能否解密。
1 | |
2026-10-06 UTC,Python 3.12.3 上两条命令退出码均为 0,4 个测试通过。在一次运行中,公开 salt 是 fd2b89e6efb65c0b67b01fdeda422ea9;下一次的随机输出预期不同,不能把这一值写死成每次运行的断言。同密钥首次 nonce 0、下一个 1 都为 true,重置到 0 为 false;切换到另一密钥标签后 0 再次为 true。第四个测试重新创建空账本,旧的 nonce 0 被接受,直接暴露“只靠进程内存去重”在重启时的缺口。
若删除集合查重,复用检测测试应失败;但即使所有测试通过,也没有测量系统随机源质量、持久化、并发安全或真实认证加密。真实算法实验在 07 之后才可按原语层次登记。
两道带答案的练习
画图题。 某服务有两个进程,都拿着同一把 AES-GCM 密钥,进程内计数器都从 0 开始。请指出攻击成立所需的重复条件,以及把两个进程的计数分别设为 0 与 1 后,重启时仍会有什么风险。
可核对答案: 两个进程若对同一密钥分别用 nonce 0 加密不同明文,满足同密钥 nonce 复用条件;改为 0/1 只能暂时避开最初的冲突,进程重启或计数器回绕、并发分配还会重用旧值。需要可持久的唯一分配策略、重新建钥或满足所选算法约束的其他机制,不能只观察“这轮测试没有撞上”。
变更题。 把 05_values.py 中 ledger.reserve("synthetic-key-A", 0) 的第三次调用改成用计数 2,再运行脚本;它的断言为什么会失败?随后把第三次改回 0、只把密钥标签改成 B,第四次调用改用新的 C,结果又代表什么?
可核对答案: 计数 2 尚未使用,第三次返回 true,与测试要求的 false 冲突,所以主脚本退出非零;把密钥改为新标签 B,0 也会被接受,同样不满足“同密钥重复”的断言。另一新密钥 C 的 0 仍可被账本接纳,说明模型按 (标签, nonce) 而非按 nonce 数字全局去重;标签必须真的代表不同密钥,不能只是改了名字却复用相同的密钥字节。
资料与导航
- RFC 9106,Argon2:salt 与密码哈希;RFC 5116,§5.1.1:GCM nonce 重用后果。
- RFC 6979,确定性 ECDSA:签名内部
k的不同要求;此处仅研究了规范,没有运行签名。 - 04 HMAC 为什么需要秘密 · 01 窃听、篡改、冒充与重放;06 完成前不提前链接。






