bigywater.tech
Java · 257

AEAD 与 GCM:加密时为什么绝不能复用 nonce

AES-GCM 同时做到加密与认证,但前提是同一把密钥下 nonce 绝不重复。JDK 帮你挡住了什么,又挡不住什么?重复之后到底会泄露什么?这篇用自己生成的密钥和数据逐项真实运行,包括随机 nonce 的生日碰撞实验。

16 字节
GCM 认证标签的长度(密文比明文多出的字节数)
39.3%
随机 32 位 nonce 发 65536 条消息出现重复的比例(公式预测 39.3%)
XOR
同一密钥与 IV 加密两条消息:密文 XOR = 明文 XOR

加密、标签与 AEADBadTagException

实测 · 本机执行输出
  明文 23 字节 → 密文+标签 39 字节(多出 16 字节 = 16 字节认证标签);IV 12 字节
  正确解密:转账 100 元给 alice
  改动密文 1 个 bit → AEADBadTagException: Tag mismatch
  改动标签 1 个 bit → AEADBadTagException: Tag mismatch
  AAD(头部)不一致 → AEADBadTagException: Tag mismatch
  IV 不一致 → AEADBadTagException: Tag mismatch
  密钥不对 → AEADBadTagException: Tag mismatch
  不给 IV 的 init:JDK 自己生成随机 IV,getIV().length = 12,getParameters().getParameterSpec(GCMParameterSpec).getTLen() = 128 位

nonce 重用

实测 · 本机执行输出
  同一个 Cipher 对象不重新 init 连续加密两次 → IllegalStateException: Must use either different key or  iv for GCM encryption
  同一个 Cipher 对象用同一个密钥和 IV 重新 init → InvalidAlgorithmParameterException: Cannot reuse iv for GCM encryption
  两个不同的 Cipher 对象用同一个密钥和同一个 IV 各自 init → 都成功,JDK 无法跨对象阻止 nonce 重用
  同一把密钥、同一个 IV 加密两条消息:密文 XOR = 0000000003030b4345000908…,明文 XOR = 0000000003030b4345000908…,两者相同 = true(密文泄露了两条明文的异或)
  已知第一条明文,由密文 XOR 恢复第二条明文:「PAY bob   9999 CNY; token=BBBB」(只靠两个密文与已知的第一条明文,没有用密钥)
  同一个 (密钥, IV) 加密同一条明文两次,密文是否完全相同 = true(确定性,也泄露「两条消息相同」)

随机 nonce 的碰撞概率

实测 · 本机执行输出
  随机 24 位 nonce,每个密钥发 3000 条消息:2000 次实验里出现重复的有 466 次(23.3%);生日公式 1−exp(−n²/2N) 预测 23.5%
  随机 32 位 nonce,每个密钥发 65536 条消息:2000 次实验里出现重复的有 785 次(39.3%);生日公式 1−exp(−n²/2N) 预测 39.3%
  12 字节(96 位)随机 nonce:发 2^32 条消息时重复概率约 1.16e-10(NIST 对随机 nonce 的 GCM 要求不超过 2^32 条/密钥)
  计数器 nonce:1000 条消息 1000 个不同值 = true(计数器不会重复,但要保证重启后不回绕、多实例不共用同一个密钥与计数区间)

流式限制与性能

实测 · 本机执行输出
  GCM 解密:喂入 16 MB 密文分 16 次 update,update 返回的明文累计 0 字节(标签验证通过之前不释放明文)
  doFinal 之后一次性返回 16 MB 明文
  GCM 加密:喂入 16 MB 明文分 16 次 update,update 返回的密文累计 16 MB(加密可以流式输出)
  AES/GCM/NoPadding    加密 1 MB:最好 10.34 ms(97 MB/s)
  ChaCha20-Poly1305    加密 1 MB:最好 1.85 ms(541 MB/s)

分步演示

AEAD 与 GCM
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK(Temurin 17.0.20、Homebrew 21.0.12 与 27,每篇注明实际用的版本);用 javac 编译、java 直接运行,需要第三方库时用 Maven 解析依赖;计时用 System.nanoTime,数字每次运行会有小幅抖动。JDK 21.0.12(Homebrew 构建,aarch64);密钥与数据都是测试程序自己生成的随机值
    • 没有验证:GCM 认证密钥泄露与伪造攻击(只演示了 XOR 泄露,这是 nonce 重用最直观的后果)、NIST SP 800-38D 对 2^32 条随机 nonce 的要求(这里只引用了计算出的概率,没有核对标准原文)、AES-GCM-SIV 等抗误用算法、硬件 AES 指令在其它平台上的性能(这台机器上 AES/GCM 明显慢于 ChaCha20-Poly1305,原因没有深究)、大文件分块加密时的 nonce 派生
    京ICP备2026067502号-1京公网安备11010502063487号