4、密码协议中的随机数应用:密钥生成、初始化向量(IV)、nonce、盐值(salt)的随机性要求
聊到密码协议里的随机数,我脑子里第一个蹦出来的词就是「各司其职」。你想想看,密钥、IV、nonce、盐值,虽然都叫随机数,但它们在协议里扮演的角色完全不同,对随机性的要求也天差地别。
我早年做过一个物联网安全网关的项目,当时团队里有人图省事,把生成密钥的随机数直接拿去当IV用。结果呢?上线不到一周,加密数据流就被分析出了模式。嗯,从那以后我养成了一个习惯——每种随机数的用途和来源,必须在设计文档里单独列出来。
4.1 密钥生成:随机性的「硬门槛」
密钥是密码系统的命根子。它的随机性要求,说白了就是两个字:不可预测。
我个人习惯把密钥生成分成三个等级来看:
- 对称密钥(如 AES-256):需要真随机数,或者经过熵池充分混合的伪随机数。长度固定,每一位都必须独立均匀分布。
- 非对称密钥对(如 RSA、ECC):生成过程更复杂。随机数不仅用于选素数或标量,还参与密钥派生。我记得有一次调试 ECDSA 签名,发现私钥的随机性不够,导致签名值出现了可观测的偏差——这可是致命伤。
- 会话密钥:每次会话都要重新生成,不能复用。我见过有人把会话密钥缓存起来重复使用,结果被重放攻击打得措手不及。
核心原则:密钥的随机数必须来自硬件随机数发生器(TRNG)或经过 NIST SP 800-90A/B/C 认证的 DRBG。软件层面的 rand() 函数,绝对不能用。
这里我贴一段伪代码,展示一个典型的密钥生成流程:
// 伪代码:安全密钥生成流程
function generate_symmetric_key(key_length_bits):
// 1. 从 TRNG 获取原始熵
entropy = trng_read(key_length_bits)
// 2. 通过 DRBG 进行去偏和扩展
drbg_seed(entropy)
key = drbg_generate(key_length_bits)
// 3. 做一次已知答案测试(KAT)
if not drbg_kat_pass():
panic("DRBG 自检失败")
return key
4.2 初始化向量(IV):唯一性比随机性更重要
很多人一提到 IV,就以为必须「足够随机」。其实不然。IV 的核心要求是唯一性——在同一个密钥的生命周期内,IV 绝对不能重复。
为什么会这样?拿 CBC 模式举例:
- 如果 IV 重复,两个相同明文块加密后得到相同密文块,模式就暴露了。
- 如果 IV 可预测,攻击者可以构造选择明文攻击。
我建议的做法是:
- 计数器式 IV:用单调递增的计数器,配合密钥一起做哈希。简单、高效、唯一性有保证。
- 随机 IV:如果必须用随机数,长度至少是块大小(AES 是 128 位),且必须由 TRNG 生成。但要注意,随机 IV 有碰撞概率,虽然很小,但在海量会话场景下不能忽视。
避坑指南:我曾经在 GCM 模式下踩过 IV 长度不够的坑。GCM 推荐 96 位 IV,如果小于这个长度,内部会做填充处理,反而降低了安全性。所以,老老实实用 96 位吧。
4.3 Nonce:一次性的「临时工」
Nonce 这个词,字面意思就是「只用一次的数」。它在协议里通常用于防止重放攻击,或者作为握手过程中的临时标识。
Nonce 的随机性要求,取决于它的使用场景:
| 场景 | 随机性要求 | 典型长度 | 我踩过的坑 |
|---|---|---|---|
| TLS 握手 | 强随机(客户端/服务器各生成一个) | 32 字节 | 有人用时间戳当 nonce,被中间人预测 |
| 认证协议(如挑战-响应) | 不可预测即可,不要求真随机 | 16-32 字节 | nonce 重复导致认证绕过 |
| 加密模式(如 GCM nonce) | 唯一性优先,可预测但不可重复 | 12 字节 | 计数器溢出导致 nonce 复用 |
我个人习惯把 nonce 和 IV 区分开:nonce 更强调「一次性」,IV 更强调「唯一性」。虽然很多时候它们可以互换,但在协议设计层面,分开思考更清晰。
4.4 盐值(Salt):对抗预计算攻击的「调味料」
盐值主要用于密码哈希和密钥派生函数(如 PBKDF2、bcrypt、Argon2)。它的作用,说白了就是让相同的密码产生不同的哈希值,从而让彩虹表失效。
盐值的随机性要求,和密钥不太一样:
- 长度:至少 16 字节。我见过有人用 8 字节的盐值,结果碰撞概率高得吓人。
- 唯一性:每个用户、每次密码变更,都必须生成新的盐值。
- 随机性来源:必须用 TRNG。我曾经在代码审查时发现有人用用户 ID 当盐值——嗯,这跟没加盐差不多。
警告:盐值不需要保密!它可以明文存储在数据库里。但如果你把盐值和密码哈希值放在同一个表里,记得做好访问控制。我见过因为盐值泄露导致离线字典攻击加速的案例——虽然盐值本身不保密,但攻击者拿到盐值后可以提前计算哈希链。
4.5 一张图看懂四种随机数的关系
下面这张 SVG 图,是我自己梳理的四种随机数在密码协议中的定位和关系。你一看就明白了。
4.6 随机性要求的对比清单
最后,我整理了一份对比清单。你写代码或者做方案评审时,可以直接拿来对照:
| 类型 | 随机性核心要求 | 推荐来源 | 常见错误 |
|---|---|---|---|
| 密钥 | 不可预测、均匀分布 | TRNG + DRBG | 用 rand()、用固定种子 |
| IV | 唯一性(同一密钥下) | 计数器 + 哈希 / TRNG | IV 重复、IV 可预测 |
| Nonce | 一次性、不可重用 | TRNG / 单调计数器 | 时间戳当 nonce、nonce 复用 |
| 盐值 | 唯一性、足够长度 | TRNG(≥16 字节) | 盐值太短、盐值不更新 |
嗯,这一章的内容差不多就这些。你想想看,这四种随机数其实都有一个共同点——它们都依赖一个可靠的随机数源。但到了具体协议里,怎么用、用多少、用在哪,就得看你对协议的理解深度了。我个人的经验是:永远不要假设「随机数够用」,而是要在设计阶段就把每种随机数的生成、存储、生命周期都写清楚。