1.1 对称密码工程:AEAD、Nonce 与密钥生命周期
先明确攻击者能接触哪些数据、控制哪些组件,才能判断密码学提供的保证。算法选型之前,需要知道要保护什么、密钥在哪里,以及密钥泄露会影响多少数据。
先分清安全目标
| 目标 | 要阻止什么 | 常见原语 |
|---|---|---|
| 机密性 | 未授权者读到明文 | 加密 |
| 完整性 | 数据被悄悄修改 | MAC、AEAD、数字签名 |
| 身份认证 | 消息或端点冒充 | MAC、签名、认证协议 |
| 不可否认性证据 | 为特定签名的来源提供可供第三方核验的证据 | 数字签名、身份绑定与审计记录 |
签名证据仍依赖私钥保管、身份绑定和事件时间;验签通过并不自动证明实际操作者是谁。
加密本身未必提供完整性。若攻击者能修改密文,而应用在认证前就处理解密结果,可能出现 padding oracle、格式探测或业务字段篡改。现代应用应优先使用经过标准化的 AEAD,而不是自行组合“加密 + 哈希”。
AEAD 同时保护密文与上下文
Authenticated Encryption with Associated Data 接收:
ciphertext, tag = Seal(key, nonce, plaintext, aad)
plaintext = Open(key, nonce, ciphertext, aad, tag)plaintext 被加密;aad 不加密,但参与认证。例如消息版本、租户 ID、记录主键和协议类型可以放入 AAD,使密文不能在不同的预期上下文中继续通过校验。解密方必须从可信的请求或资源定位确定预期 AAD;若把攻击者一起搬来的租户字段也原样接受,这份绑定就没有检查到目标租户。AEAD 本身也不拒绝原封不动的重放,仍需协议维护序号、期限或去重状态。
常用选择包括 AES-GCM 与 ChaCha20-Poly1305。不要根据“哪个名字听起来更强”自行选择;优先使用平台高层 API、协议规定的套件和受维护库。算法安全还取决于 nonce、密钥、错误处理和实现侧信道。
Nonce 不需要保密,但必须满足算法约束
Nonce 的核心要求通常是:同一密钥下不能重复。以 GCM 为例,nonce 重用可能泄露明文关系并破坏认证,后果远比“随机性稍差”严重。
key K + nonce N1 → message A
key K + nonce N1 → message B ← 危险:同一 key/nonce pair 重用生成策略可以是足够大的随机 nonce,也可以是每密钥单调计数器;选择取决于协议。计数器需要跨崩溃持久化并防止多节点重复,随机方案需要计算碰撞预算。不能在不知道算法要求时用时间戳、短随机数或业务 ID 代替。
解密 API 必须先验证 tag,再向调用方释放明文。认证失败只返回统一错误,不能把“格式错、padding 错、tag 错”暴露成可探测分支。
运行一次篡改与上下文测试
下面需要 Python 的 cryptography 包,使用其 AESGCM 接口。返回值已包含 16 字节 tag,毋须再单独拼一个哈希。示例每次新建密钥,只加密一条消息,验证密文或 AAD 改动会失败;它没有实现生产密钥存储或跨进程 nonce 分配。
import os
from cryptography.exceptions import InvalidTag
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
key = AESGCM.generate_key(bit_length=256)
aead = AESGCM(key)
nonce = os.urandom(12)
aad = b'{"tenant":"acme","record":42,"version":1}'
plaintext = b"invoice total: 125"
ciphertext = aead.encrypt(nonce, plaintext, aad)
assert aead.decrypt(nonce, ciphertext, aad) == plaintext
assert len(ciphertext) == len(plaintext) + 16
altered = ciphertext[:-1] + bytes([ciphertext[-1] ^ 1])
for candidate, context in (
(altered, aad),
(ciphertext, b'{"tenant":"other","record":42,"version":1}'),
):
try:
aead.decrypt(nonce, candidate, context)
except InvalidTag:
pass
else:
raise AssertionError("unauthenticated data was accepted")把密钥访问与数据访问分开
把加密密钥写在源码、镜像、同一数据库字段或普通环境配置里,相当于把钥匙贴在保险箱上。生产系统常用 envelope encryption:
KMS/HSM 中的 KEK
│ 包装
▼
每批或每对象的随机 DEK ──AEAD──> 业务数据Data Encryption Key(DEK)负责高频数据加密;Key Encryption Key(KEK)留在 KMS/HSM 中,只包装 DEK。这样可以限制主密钥暴露,并在重新包装 DEK 时避免重加密全部大对象。KMS 审计记录的是向它发起的密钥操作;应用缓存 DEK 后的每次本地解密不会自动成为一条 KMS 事件,仍需应用审计。
更换 KEK 不代表现有 DEK 已自动重新包装,需按 KMS 的接口完成迁移。若旧 KEK 或 DEK 已泄露,重新包装也不能收回攻击者已获得的密钥和旧密文;事件响应可能需要新 DEK 与数据重加密。
密文记录必须自描述但不泄密,通常保存:算法套件、格式版本、key ID、nonce、ciphertext 和 tag。不要只存一个裸字节串,几年后没人知道用哪个密钥和参数解开。
轮换不等于立刻删除旧密钥
密钥生命周期至少包含生成、分发、激活、使用、轮换、撤销、归档和销毁。轮换时常采用:
- 新写入使用新 key version;
- 读取按记录中的 key ID 选择旧密钥;
- 后台逐步重加密或只在读取时迁移;
- 确认所有备份和延迟副本的恢复需求后,再停用旧密钥;
- 最后按保留政策销毁。
若删除旧密钥早于备份到期,备份仍存在却无法恢复。若旧密钥永不退役,轮换又没有降低长期风险。
随机数必须来自 CSPRNG
密钥、salt、session ID、恢复码,以及选用随机策略的 nonce,应使用操作系统或密码库提供的密码学安全随机源。按协议分配的唯一计数器 nonce 不要求随机,不能把“需要唯一”混成“必须不可预测”。普通伪随机数、时间戳、进程 ID 和自制“混淆算法”都不适合生成秘密。
不要自己实现 AES、ChaCha、GCM 或随机数生成器。即使数学公式正确,常数时间、错误返回、内存清理、nonce 管理和硬件加速路径也可能引入漏洞。
设计检查
- 威胁模型要求的是机密性、完整性、认证,还是三者都有?
- 是否使用高层 AEAD API,而非 ECB/CBC 或自制组合?
- 同一密钥下 nonce 的唯一性如何证明并监控?
- AAD 是否绑定租户、版本和对象身份,防止密文搬运?
- 密钥是否与密文隔离,并受最小权限与审计保护?
- 轮换、备份恢复和旧密钥销毁是否一起演练?
- 认证失败是否在处理任何明文前终止?
下一课处理经常被混用的哈希、MAC、KDF 与密码存储。