bigywater.tech
Java · 264

密钥轮换:Java 服务如何平滑切换加密材料

密钥总要换。轮换时旧数据怎么办?什么时候才能删旧密钥?主密钥轮换的成本怎么降?并发读写时怎么避免读到一半的状态?这篇用真实的 AES-GCM 实现密文格式、信封加密与并发轮换,逐项量出结果。

71 vs 215 ms
轮换主密钥:只重新包裹 DEK / 重新加密全部数据(5 万条 × 1 KB)
4.04% → 0
轮换期间两个字段分别读取的失败率 → 不可变快照的失败率
50 天
每秒 1000 条消息时随机 nonce 的单个密钥用满 2^32 条所需时间

密文带 key id

实测 · 本机执行输出
  轮换前(只有 key 1):密文 45 字节,头部的 key id = 1,能解密:订单 #1001
  轮换后(新增 key 2,当前 = 2,key 1 仍在钥匙环里):新密文的 key id = 2;旧密文(key id = 1)仍能解密:订单 #1001;新密文能解密:订单 #1003
  过早地从钥匙环里删除 key 1 之后,解密旧密文 → NoSuchElementException: 没有 key id 1 对应的密钥(已被删除或从未存在)
  惰性重加密(读到旧密文时解密再用当前密钥加密回写):旧密文 key id 1 → 新密文 key id 2;之后可以安全删除 key 1(所有密文都重加密完成之后)
  篡改头部里的 key id(把 2 改成别的 id,key id 同时是 AAD 与选钥依据)→ NoSuchElementException: 没有 key id 258 对应的密钥(已被删除或从未存在)
  把 key id 改成 1(密文实际是 key 2 加密的)→ AEADBadTagException: Tag mismatch

信封加密

实测 · 本机执行输出
  50000 条各 1 KB 的记录:直接用主密钥加密 388 ms;信封加密(每条记录一个 DEK,KEK 包裹 DEK)279 ms
  轮换主密钥:直接加密方案要对每条记录 解密 + 重新加密 215 ms(处理 53 MB 数据);信封加密只需对每条记录的 40 字节包裹后的 DEK 解包 + 重新包裹 71 ms,数据体不动——快 3 倍
  KEK 轮换之后,用新 KEK 解包的 DEK 解密原来的数据体 → 内容一致
  用新 KEK 去解旧的包裹 DEK → InvalidKeyException: javax.crypto.IllegalBlockSizeException: Integrity check failed

轮换时的并发

实测 · 本机执行输出
  一个线程不停地轮换 key 1 ↔ 2(共 102651099 次),4 个线程各做 60000 次「读当前 id 与密钥 → 加密 → 用钥匙环按头部 key id 解密」:
  两个字段分别读取(id 与 key 不是同一时刻):成功 230314,失败 9686(4.04%)——密文头部写的 id 与实际使用的密钥不一致
  读取一个不可变的钥匙环快照(AtomicReference<Keyring>,id 与密钥一起换):成功 240000,失败 0

nonce 预算

实测 · 本机执行输出
  单线程加密 256 字节小消息(含生成随机 nonce、创建 Cipher):2.0 μs/条,约 510329 条/秒
  随机 96 位 nonce 的 GCM,单个密钥最多加密 2^32 = 4.29e+09 条消息(NIST 建议值,生日界):按单线程最高速度 510329 条/秒算,2.3 小时用完;按 1000 条/秒算 50 天;按 100 条/秒算 497 天
  所以轮换周期应该同时由两个条件决定:时间(泄露窗口)与消息数(nonce 预算),取先到者;消息数需要有计数(或用确定性的计数器 nonce)
  钥匙环里有 3 个版本时解密一条消息 1.6 μs/条(按 key id 查表,几乎不增加成本)

分步演示

密钥轮换
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK(Temurin 17.0.20、Homebrew 21.0.12 与 27,每篇注明实际用的版本);用 javac 编译、java 直接运行,需要第三方库时用 Maven 解析依赖;计时用 System.nanoTime,数字每次运行会有小幅抖动。JDK 21.0.12,AES-256-GCM 与 AESWrap;所有密钥由测试程序生成,没有接 KMS
    • 没有验证:KMS/HSM 里的密钥轮换与审计、密钥泄露后的应急重加密流程、分布式环境下各节点钥匙环的一致性、2^32 条消息上限的标准出处(这里只引用了计算结果,没有核对标准原文)、密钥缓存的失效时间、JWT 等具体格式里的 kid
    京ICP备2026067502号-1京公网安备11010502063487号