到底什么是 AEAD?再探究竟

_ 2025年4月28日 _

你可能对这个问题很熟悉:我总是忘记 AEAD 到底是什么意思,以及为什么需要使用它。是的,我知道这个缩写代表 "Authenticated Encryption with Associated Data"(带有关联数据的认证加密),但这对理解有什么帮助呢?对我来说并没有,所以我最终决定坐下来写这篇博客文章,为未来的自己提供帮助……也为其他觉得 AEAD 难以记住的人提供帮助。

为什么需要关注 AEAD?

简单来说,AEAD 加密是当前的行业标准。听起来是个值得关注的好理由,至少如果你关心理解你的构建块的话。你不必相信我的话,下面是一些相关的数据点:

这个列表可以更长,但希望这足以证明 AEAD 将继续存在。正如 Thomas Ptacek 在他著名的 Cryptographic Right Answers 中所说的那样:“[AEAD] 是你在 2015 年唯一想使用的加密方式”。(是的,那是 10 年前的事情了。)

第一部分 - 认证加密

认证什么?

当我想到身份验证时,我脑海中的关联是登录网站。 然而,在密码学中,身份验证意味着_证明加密的消息是真实的_,即它在加密后没有被更改,因此完全来自有权访问密钥的人。

身份验证不仅仅是一个“锦上添花”的功能,正如你最初可能认为的那样。 它通常是系统安全的基本1条件2。 例如,在某些情况下,即使没有密钥,缺乏身份验证也会让拦截器解密消息!

走向更合理的默认值

当我第一次学习密码学时3,通常的做法是分步执行_加密_和_身份验证_。 你会选择一种加密方案(例如,AES-256CBC 模式 中),一种身份验证方案(例如,HMAC-SHA256),并在你的代码中仔细地将它们组合在一起,以确保一切都经过适当的身份验证4

以下伪代码显示了当时的加密和解密过程:

# Sender: encrypt and generate authentication tag
(nonce, ciphertext) = encrypt(key, "hello world")
tag = hmac(key, nonce + ciphertext)
send(nonce, ciphertext, tag)
# Receiver: verify authentication tag and decrypt
(nonce, ciphertext, tag) = receive()
assert tag == hmac(key, nonce + ciphertext)
assert decrypt(key, nonce, ciphertext) == "hello world"

相当冗长,不是吗? 不像仅仅调用 encryptdecrypt 那么简单。 难怪人们经常搞砸,就像 Apple 的 iMessage 漏洞 是由于……完全未能包含身份验证步骤而引起的! 顺便说一句,即使你记得进行身份验证,你仍然需要以正确的顺序应用加密和身份验证,否则 The Cryptographic Doom Principle找上你

幸运的是,在过去十年中,行业已经引入了更能抵抗误用的原语。 看一下下面的伪代码(libsodiumcrypto_secretbox_easy 函数的简化版本):

# Sender: encrypt, including an authentication tag in the ciphertext
(nonce, ciphertext) = encrypt_auth(key, "hello world")
send(nonce, ciphertext).
# Receiver: verify message authenticity and decrypt
# (`decrypt` throws an exception if verification fails)
(nonce, ciphertext) = receive()
assert decrypt_auth(key, nonce, ciphertext) == "hello world"

很好,不是吗? 在底层,此 API 仍然对加密和身份验证使用单独的步骤,但是 API 的用户再也不会搞砸了。 对于像我这样,严重依赖 API 的设计来指导我编写正确的代码的人来说,这_远_比那些让你搬起石头砸自己的脚的旧 API 好。

第二部分 - 关联数据

但为什么?

我们已经采用了经过身份验证的加密。 这还不足以保证我们消息的秘密性吗? 所有关于“关联数据”的大惊小怪是什么? 为什么还要增加额外的复杂性?

经过身份验证的加密确实足以保证消息的秘密性,但事实证明,你经常需要将未加密的数据与加密的消息一起发送。 这段未加密的数据就是密码学家所说的“关联数据”。 让我用一个例子来说明这一点。

例如,假设你正在开发一个多用户聊天应用程序。 当两个用户进行对话时,他们会协商一个密钥并通过服务器开始交换消息。 正如你可能期望的那样,服务器无法看到消息的内容,因为它们已加密。 尽管如此,当发送新消息时,服务器需要访问接收者的用户 ID,以便正确地将消息路由给他们。 为此,当从客户端发送加密消息时,它还包含未加密的接收者用户 ID。 换句话说,接收者的用户 ID 作为加密消息的关联数据发送。

现在,如果中间人拦截该消息并将原始接收者的用户 ID 替换为其他用户 ID 会发生什么? 有两种可能性:

让我们进行身份验证

与验证加密的字节数组类似,我们可以使用身份验证方案(例如,HMAC-SHA256)来验证加密的消息及其关联数据。 像这样:

# Sender: encrypt and send together with tagged associated data
associated_data = "an unencrypted string"
(nonce, ciphertext) = encrypt(key, "hello world")
tag = hmac(key, nonce + ciphertext + associated_data)
send(nonce, ciphertext, associated_data, tag)
# Receiver: verify encrypted and associated data, then decrypt
(nonce, ciphertext, associated_data, tag) = receive()
assert tag == hmac(key, nonce + ciphertext + associated_data)
assert decrypt(key, nonce, ciphertext) == "hello world"

再次非常冗长,对吗? 实际上,在我看来,这看起来非常复杂,以至于我甚至不确定它是正确的……密码学库不能让我们的生活更轻松吗? 对于这样的事情,我宁愿信任它们而不是我自己的代码。

AEAD 来救援

正如我上面提到的,行业已经转向更能抵抗误用的原语。 我们之前提到的同一个 libsodium 库提供了验证加密位_和_关联数据的加密函数。 听起来很熟悉吗? 我们终于在讨论带Authenticated Encryption with Associated Data(AEAD)了!

让我们更详细地看一下。 下面的简化伪代码已从 libsodium 进行了调整,并在实践中说明了 AEAD 的用法:

# Sender: encrypt, including an authentication tag in the ciphertext
# The authentication tag applies to both the encrypted bits and the unencrypted associated data.
associated_data = "an unencrypted string"
(nonce, ciphertext) = encrypt_aead(key, "hello world", associated_data)
# Receiver: verify message authenticity and decrypt
# (`decrypt` throws an exception if verification fails for the encrypted bits or the associated data)
(nonce, ciphertext, associated_data) = receive()
assert decrypt_aead(key, nonce, ciphertext, associated_data) == "hello world"

正如你所看到的,该 API 现在“强制”我们验证加密位和关联数据,从而防止了各种错误。 如果你足够努力,仍然可以引入错误,但该 API 至少会将你引导到成功的陷阱

第三部分 - 在库中使用 AEAD

如果你无法使用 libsodium 怎么办? 鉴于 AEAD 的普及,已经标准化了多种 AEAD 密码,这意味着你可以选择最适合你的密码,并在各种库和编程语言中使用它。 你可能已经见过像 AES256-GCMChaCha20-Poly1305 这样的名称,所以现在出现了一个显而易见的问题:我应该选择哪种 AEAD 原语?

我不是密码学家,因此除非我有非常特殊的要求,否则我会遵循 Tink 的 choose a primitive 页面所推荐的任何内容。 但是请记住,通用的密码学建议在定义上是有限的。 在某些情况下,即使是 Tink 的建议也需要谨慎对待。 希望你当地的密码学家可以帮助你提出明智的建议:)

结束

到底什么是 AEAD 呢? 恐怕我必须回到本文的开头并再次阅读一遍……

特别感谢@ctz@cpu,他们审阅了本文的早期草稿,提出了改进意见,并验证了我的说法是准确的。 没有他们的审查,我不敢发布它! 任何遗留的错误显然都是我自己的。

  1. 此 StackExchange 评论 用简洁的语言表达了这一点:“默认情况下,加密应该是经过身份验证的加密,除非你确定不需要它”。 对于像我这样的凡人来说,这意味着在所有情况下都使用经过身份验证的加密,除非我信任的专家说服我否则。 ↩︎
  2. 本文 深入探讨了一些陷阱并详细解释了它们。 问题的核心是 malleability 的概念。 ↩︎
  3. 我很幸运能够拿到这本优秀的(尽管现在有些过时)Cryptography Engineering: Design Principles and Practical Applications↩︎
  4. 根据此报告,这是 WhatsApp 用来保护消息的精确加密和身份验证组合。 ↩︎
  5. 在撰写本文时,Tink 建议你日常加密需求使用 AES128-GCM。 另一方面,Libsodium 警告不要使用 AES-GCM尽管由于在 TLS 中使用,它是最流行的 AEAD 构造,但在不同的上下文中安全地使用 AES-GCM 非常棘手。 (……)除非你绝对需要 AES-GCM,否则请使用 AEGIS-256(……)代替。 阅读链接页面以获取更多信息。 ↩︎