跳到主要内容
ecdsa.com

AES-GCM vs ChaCha20-Poly1305:该选哪种 AEAD

两者都是使用 128 位标签和 96 位 nonce 的标准 AEAD。在带 AES 指令的 CPU 上和 FIPS 环境中,AES-GCM 胜出;其他场合则是 ChaCha20-Poly1305 胜出。

在 AES-GCM 和 ChaCha20-Poly1305 之间做选择,就是在承载几乎所有加密流量的两种认证加密算法之间做选择:TLS 1.3、QUIC、SSH、WireGuard、age 以及大多数应用层加密都在使用它们。两者在正确使用时都是安全的,一旦 nonce 重复都会严重失效,而它们的速度在很大程度上取决于硬件。

概览

AES-GCMChaCha20-Poly1305
规范NIST SP 800-38D(2007)RFC 8439(2018,取代 RFC 7539)
密钥长度128、192、256 位256 位
Nonce推荐 96 位96 位(XChaCha20 为 192 位)
认证标签128 位(允许更短,但不推荐)128 位
单条消息最大数据量约 64 GiB(2³² − 2 个块)256 GiB(2³² 个 64 字节的块)
每个密钥的随机 nonce 预算2³² 条消息(SP 800-38D)使用 96 位 nonce 时数量级相同;使用 XChaCha20 时实际上不受限制
Nonce 重用泄露两段明文的异或值和 GHASH 密钥,使伪造成为可能泄露两段明文的异或值,并使伪造成为可能
吞吐量,16 KiB,OpenSSL 3.6.510.4 GB/s(AES-128),8.8 GB/s(AES-256)2.1 GB/s
吞吐量,16 字节消息0.14 GB/s(AES-256)0.52 GB/s
无硬件支持时的常量时间实现困难(基于查表的 AES 会通过缓存泄露信息)天然具备(加法、循环移位、异或)
获 FIPS 140-3 批准是否
TLS 1.3TLS_AES_128_GCM_SHA256(必须,MUST),TLS_AES_256_GCM_SHA384(应当,SHOULD)TLS_CHACHA20_POLY1305_SHA256(应当,SHOULD)
其他用途JWE A128GCM 和 A256GCM、IPsec、SSH aes256-gcm@openssh.com、磁盘加密和云端加密WireGuard、SSH chacha20-poly1305@openssh.com、age、libsodium

吞吐量用 openssl speed -evp 在 Apple M4 Max 的单个核心上测得,这款芯片带有 AES 指令和多项式乘法指令。

性能取决于 CPU

AES-GCM 是为硬件设计的:AES 的每一轮都作为单条指令执行(x86 上的 AES-NI、ARMv8 的密码学扩展),GHASH 则使用无进位乘法。这两类指令都可用时,它在这台机器上每个核心可达约 10 GB/s,大约是 ChaCha20-Poly1305 的五倍。没有这些指令时,AES 要么使用查找表,速度慢,还会通过缓存计时泄露密钥位;要么使用位切片代码,虽然是常量时间的,但同样很慢。ChaCha20 只需要 32 位加法、循环移位和异或,因此在任何 CPU 上都能快速地以常量时间运行,包括低端手机和微控制器。

消息较小时,差距会缩小甚至反转:输入为 16 字节时,ChaCha20-Poly1305 达到 0.52 GB/s,而 AES-256-GCM 只有 0.14 GB/s,因为此时 GCM 的初始化开销占了主导。对于服务器上典型的 TLS 记录(最大 16 KiB),AES-GCM 更快;对于大量极小的数据包,无论选哪种,差别都不大。

Nonce 与上限

两者都是搭配一次性认证器的计数器模式加密算法,因此同一个(密钥,nonce)组合绝不能重复出现。一旦重复,就会暴露两段明文的异或值;对于 GCM,攻击者还能恢复出认证子密钥,并伪造出在该密钥下有效的消息;对于 Poly1305,则会暴露一次性密钥,使攻击者能针对该 nonce 伪造消息。NIST SP 800-38D 规定,使用随机生成的 96 位 IV 时,每个密钥最多只能调用 2³² 次,以确保碰撞概率可以忽略不计;TLS 则通过从记录序列号派生 nonce 并更新密钥来规避这个问题。

如果你的设计在同一个密钥下为大量消息生成随机 nonce,应使用 nonce 空间更大或具备抗误用能力的构造:

  • XChaCha20-Poly1305(192 位 nonce,由一份 IRTF 互联网草案描述,libsodium 等库已实现):随机 nonce 实际上可以安全地用于无限多条消息
  • AES-GCM-SIV(RFC 8452):nonce 重用只会暴露两条消息是否相同
  • 或者按消息或按会话派生密钥,就像 TLS、age 和 HPKE 那样

两种算法都不具备密钥承诺性:可以精心构造出一段密文,使它在两个不同的密钥下都能有效解密。这对多接收者或基于密码的设计很重要(即“隐形蝾螈”(invisible salamanders)问题)。如果攻击者可以选择密钥,应加入密钥承诺。

Node.js 26:两种算法使用同一套 AEAD 接口
import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";

const plaintext = Buffer.from("card ending 4242, amount 19.99");
const aad = Buffer.from("order=7781");

for (const algorithm of ["aes-256-gcm", "chacha20-poly1305"]) {
  const key = randomBytes(32);
  const nonce = randomBytes(12); // 96-bit nonce for both; never reuse with the same key
  const cipher = createCipheriv(algorithm, key, nonce, { authTagLength: 16 });
  cipher.setAAD(aad);
  const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
  const tag = cipher.getAuthTag();

  const decipher = createDecipheriv(algorithm, key, nonce, { authTagLength: 16 });
  decipher.setAAD(aad);
  decipher.setAuthTag(tag);
  const roundTrip = Buffer.concat([decipher.update(ciphertext), decipher.final()]);

  console.log(algorithm.padEnd(18), "ciphertext", ciphertext.length, "bytes + tag", tag.length, "bytes, decrypts:", roundTrip.equals(plaintext));
}
输出
aes-256-gcm        ciphertext 30 bytes + tag 16 bytes, decrypts: true
chacha20-poly1305  ciphertext 30 bytes + tag 16 bytes, decrypts: true

TLS 如何在两者之间选择

TLS 1.3 要求每个实现都支持 TLS_AES_128_GCM_SHA256,并推荐支持 AES-256-GCM 和 ChaCha20-Poly1305。没有 AES 硬件的客户端会把 ChaCha20 排在前面;尊重这一偏好的服务器就能为每个客户端提供对它来说更快的加密算法。OpenSSL 通过 PrioritizeChaCha 选项(SSL_OP_PRIORITIZE_CHACHA)实现这一点:只有当客户端把 ChaCha20 列在首位时,才会优先选择它;BoringSSL 则用同等优先级分组达到同样的目的。TLS 配置生成器生成的服务器设置会同时保留这两种密码套件。

如何选择

  • TLS 和 QUIC 服务器:两者都启用;优先使用 AES-GCM,但让把 ChaCha20 排在首位的客户端用上 ChaCha20。
  • FIPS 140-3 环境:AES-GCM;ChaCha20-Poly1305 不是获批算法。
  • 没有 AES 加速的移动设备、物联网设备和微控制器:ChaCha20-Poly1305。
  • 使用随机 nonce 和长期密钥的应用层加密:XChaCha20-Poly1305 或 AES-GCM-SIV。
  • JWE:A256GCM(或 A128GCM);ChaCha20 不在 JWA 核心注册表中。
  • 超大的单条消息(超过 64 GiB):拆分成块,每块使用各自的 nonce(age 就是这样做的,它使用 64 KiB 的 ChaCha20-Poly1305 块);不要依赖单条消息的上限。

迁移说明

在设计良好的系统中,更换 AEAD 只会改变密文格式,其他一切都不受影响。

  1. 为格式加上版本号:为每段密文同时存储算法标识符和密钥标识符,确保旧数据依然可读。
  2. 每种算法使用独立的密钥:派生不同的密钥(例如用 HKDF 搭配不同的 info 字符串),不要让两种算法共用同一个密钥。
  3. 重新检查 nonce 的生成方式:计数器必须持久化,重启后不能丢失;随机 nonce 必须控制在上文的上限之内,否则改用 XChaCha20。
  4. 惰性重新加密:除非合规期限要求批量迁移,否则在访问到某条记录时,用旧算法解密,再用新算法写回。
  5. 在实际硬件上做基准测试:在服务器 CPU 和小型设备之间,速度排名会颠倒。

参考资料

全部对比