3. 信封加密概念与原理

我刚开始做加解密系统的时候,经常接到一个需求:“帮我把这批数据库字段加密一下”。那时候我就直接把用户密钥拿过来,一条数据一个秘钥对?太多。一个密钥加密全部?风险太大。直到接触了信封加密——嗯,这招儿真管用。

说白了,信封加密就是“用数据加密数据,再用高级密钥去加密那个数据密钥”。它把对称加密的高效和密钥管理的灵活性结合在了一起。你想想看,如果对所有数据都用同一个主密钥加密,一旦泄露,全盘皆输;如果每条数据都独立密钥,存储和分发又成了灾难。

核心概念

信封加密里有两个角色:

  • 数据加密密钥(DEK)——真正的“干活”密钥。对称算法(比如 AES-256),大量数据都用它加密,速度快。
  • 密钥加密密钥(KEK)——专门用来加密 DEK 的“保护伞”。可以是对称密钥,也可以是非对称密钥(比如 KMS 中的主密钥)。

记住:实际加密数据的永远是对称密钥(DEK),主密钥(KEK)只用来保护 DEK,不动你的海量数据。

一个完整的信封加密流程长这样:

  1. 生成一个随机的 DEK。
  2. 用 DEK 加密实际数据,得到密文 C1。
  3. 用 KEK 加密 DEK,得到密文 C2(称为“信封”)。
  4. 把 C1 和 C2 一起存储 / 传输。
  5. 解密时:先用 KEK 解密 C2 拿到 DEK,再用 DEK 解密 C1 还原数据。

为什么叫“信封”?想象一下:你把数据装进一个信封里,信封封口用的是 DEK 这把锁;然后你用 KEK 这把更高级的锁再把那个 DEK 锁进一个更小的信封里。两个信封一里一外,外层的钥匙只有你(或 KMS)持有。

典型的密钥层次

KMS 里的主密钥(CMK)就是 KEK 的一种常见形态。我在项目中遇到过这样的层次:

层级 名称 算法 保护对象
L0 根密钥(根CA) 非对称 RSA-4096 HSM 内部
L1 客户主密钥(CMK) 对称 AES-256 / 非对称 DEK
L2 数据加密密钥(DEK) 对称 AES-256 业务数据
L3 业务数据 — 明文 / 密文

越往上,密钥越珍贵、越难泄露。L0 的根密钥甚至可能从不出 HSM 硬件。L2 的 DEK 可以频繁轮转——你甚至每写一条记录就换一个 DEK。

为什么非要用信封加密?

两个字:性能和管理。

  • 性能:公钥加密比较慢。如果拿非对称密钥直接加密几十 GB 的数据,服务器得累死。而 DEK 是对称的,速度是公钥的几千倍。你只需要用非对称加密一次 DEK 就行。
  • 管理:你可以在 KMS 里只存 KEK(比如每个应用一个主密钥),而 DEK 随密文一起存储,不需要额外保护。甚至可以把 DEK 的密文(信封)直接存在数据库的一个字段里。密钥管理范围从“海量文件”缩小到“少量主密钥”。

我做过一个文件存储系统,用户上传的数百万文件都用不同的 DEK 加密,每个 DEK 再被同一个 CMK 加密后随文件存储。CMK 只有一把,托管在 KMS 里。这样即使存储服务器被拖库,攻击者也拿不到文件内容——因为没有 KMS 权限去解密那个 CMK 下的 DEK。

完整流程示意图

下面这张图展示了信封加密的整个生命周期。注意节点颜色:蓝色节点是“密钥生成/使用”,绿色节点是“加密操作”,红色节点是“存储结果”。

信封加密流程图 生成随机 DEK 用 DEK 加密 业务数据 数据密文 C1 (体积大) 用 KEK 加密 DEK (获取信封 C2) 信封 C2 (加密的DEK) 主密钥 KEK (托管于 KMS) C1 + C2 一起存储 加密 封装

解封过程 & 避坑

解密是反过来的:读取 C1 和 C2,向 KMS 请求用 KEK 解密 C2 得到 DEK,再用 DEK 解密 C1。这里有个细节我当初踩过坑:

我曾经直接让应用服务器缓存明文 DEK 以提升性能,结果某个实例日志泄露了 DEK。后来改成了:每次只用 Decrypt API 解密信封,DEK 永不出 KMS — 除非你本地有 HSM 级别的保护。对于吞吐要求高的场景,可以考虑“DEK 缓存带过期时间 + 仅在工作内存中”,但绝不能落盘。

另外,DEK 可以定期轮转。你只需要在加密时选一个新的 DEK,然后重新用 KEK 加密就行——历史数据完全不受影响。这主意在实际生产环境里非常省心。

一个简单代码示例(伪代码)

// 加密
function envelopeEncrypt(kekId, plaintext):
    dek = generateRandomKey(32)       // AES-256
    ciphertext = aes256Encrypt(dek, plaintext)
    envelope = kmsEncrypt(kekId, dek) // 使用 KMS 主密钥
    return { ciphertext, envelope }

// 解密
function envelopeDecrypt(kekId, ciphertext, envelope):
    dek = kmsDecrypt(kekId, envelope)
    plaintext = aes256Decrypt(dek, ciphertext)
    return plaintext

实际调用 KMS 时,kmsEncrypt 和 kmsDecrypt 通常是远程 RPC。注意加解密操作不要放在客户端自身实现——把信封传给 KMS 让它帮你解,避免密钥在客户端出现。

适用范围总结

场景 推荐使用信封加密? 原因
海量静态文件加密 ✅ 强烈推荐 DEK 随文件,KEK 集中管理
数据库字段级加密 ✅ 推荐 每行一个 DEK,轮转容易
小型应用,数据量少 ✅ 可用 也可直接用主密钥加密
需要多方协作的场景 ✅ 非常适合 可以每个协作方持有不同的 KEK,但共用同一个 DEK

总结一句:信封加密就是把“密钥管理”的成本从每份数据上剥离出来,让一个(或几个)主密钥去保护成千上万的数据密钥。它既享受了对称加密的高性能,又得到了非对称加密的灵活权限控制。如果你正在做任何一个要落盘的加密系统,不妨考虑这套方案。