AEAD算法实验及原理说明
什么是AEAD算法¶
AEAD(Authenticated Encryption with Associated Data,认证加密与关联数据)是一种密码学规范和标准,旨在同时提供数据的加密和认证功能。它不仅保证了密文数据的隐私、完整性和真实性,还能对未加密的关联数据提供完整性保证。
在现代密码学和网络安全协议(如 TLS 1.3、IPsec、SSH)中,AEAD 已经是绝对的主流。
常见的AEAD算法有哪些¶
AES-GCM (Galois/Counter Mode)¶
- 原理:基于 AES 块密码的 CTR(计数器)模式进行加密,使用 GHASH(基于伽罗瓦域的乘法)生成认证标签(MAC)。
- 特点:目前**应用最广泛**的 AEAD 算法。
- 优点:现代 CPU(如 Intel/AMD 的 AES-NI 和 PCLMULQDQ 指令集,以及 ARM 的 CE 指令集)提供了极强的硬件加速,吞吐量极高;支持并行处理。
- 缺点:对 Nonce(随机数/IV)重用 极其敏感。如果同一个密钥下两次使用了相同的 Nonce,不仅会泄漏明文,攻击者还能轻易恢复出认证密钥,从而伪造任何消息。
- 应用场景:TLS 1.2/1.3、HTTPS、IPsec、Wi-Fi (WPA3)、云存储加密。
ChaCha20-Poly1305¶
- 原理:由 Google 推广。使用 ChaCha20 流密码进行加密,使用 Poly1305 多项式 MAC 进行认证。
- 特点:在**没有硬件加速**的设备上表现极其优异。
- 优点:纯软件实现速度极快,安全性高,抗侧信道攻击(如时序攻击)能力强。
- 缺点:在有 AES 硬件加速的现代服务器 CPU 上,速度略逊于 AES-GCM。
- 应用场景:移动端设备(Android/iOS)、老旧服务器、OpenSSH、TLS 1.3(作为 AES-GCM 的完美替代方案,通常浏览器会根据 CPU 是否支持 AES-NI 自动在两者间切换)。
AES-CCM (Counter with CBC-MAC)¶
- 原理:结合了 CTR 模式(加密)和 CBC-MAC(认证)。
- 特点:较早的 AEAD 标准,只需要 AES 的加密原语即可同时实现加密和认证。
- 优点:不需要额外的硬件指令(如 GHASH 所需的 Carry-less 乘法指令),适合资源受限但只有基础 AES 硬件的嵌入式设备。
- 缺点:双通道(Two-pass)算法,即加密和认证需要分别对数据进行处理,效率不如 GCM;不支持并行处理。
- 应用场景:WPA2 (Wi-Fi)、Zigbee、蓝牙低功耗 (BLE)、IPsec。
SM4-GCM¶
这是目前国密体系中**最主流、应用最广泛**的 AEAD 方案,完全对标国际上的 AES-GCM。
- 组成:使用 SM4 算法在 CTR(计数器)模式 下进行数据加密,使用 GHASH(伽罗瓦域运算)生成认证标签(MAC)。
- 特点:
- 高性能:与 AES-GCM 一样,支持并行计算,加解密和认证可以同时进行(单通道)。
- 硬件加速:随着国产 CPU(如鲲鹏、飞腾、海光、龙芯等)和国产密码卡/加密机的发展,SM4 的硬件指令集加速已经非常成熟,SM4-GCM 的吞吐量已经能够满足高速网络的需求。
- 应用场景:国密 TLS 1.3 协议、IPsec VPN、政务云存储加密、高速网络数据传输。
AES-GCM实现示例¶
- 客户端Demo

- 服务端Demo(python实现)
from flask import Flask, request, jsonify, send_file
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from cryptography.exceptions import InvalidTag
import base64
import hashlib
import json
import os
import secrets
from pathlib import Path
APP_DIR = Path(__file__).parent
# 服务端监听 0.0.0.0:8000
# 如果服务器 IP 是 192.168.139.3,则浏览器访问 http://192.168.139.3:8000
HOST = "0.0.0.0"
PORT = 8000
# ============================================================
# 演示用预共享密钥
# 注意:生产环境绝不能这样写,更不能把密钥暴露给前端
# ============================================================
PSK = "demo-login-psk-2026"
# 使用 SHA-256 派生 AES-256 密钥
# 前端 JS 也会使用相同字符串做相同的 SHA-256
AES_KEY = hashlib.sha256(PSK.encode("utf-8")).digest()
# AES-GCM 请求和响应绑定不同的附加认证数据 AAD
REQUEST_AAD = b"login-request"
RESPONSE_AAD = b"login-response"
# AES-GCM 加密器
aesgcm = AESGCM(AES_KEY)
app = Flask(__name__)
# ============================================================
# 允许跨域,仅用于演示
# 如果前端页面由同一个 Flask 服务提供,可以不开启 CORS
# ============================================================
@app.after_request
def add_cors(response):
response.headers["Access-Control-Allow-Origin"] = "*"
response.headers["Access-Control-Allow-Methods"] = "GET, POST, OPTIONS"
response.headers["Access-Control-Allow-Headers"] = "Content-Type"
return response
def encrypt_payload(data: bytes, aad: bytes) -> str:
"""
AES-GCM 加密:
返回 Base64(nonce + ciphertext + tag)
"""
nonce = os.urandom(12)
ciphertext_and_tag = aesgcm.encrypt(nonce, data, aad)
return base64.b64encode(nonce + ciphertext_and_tag).decode("ascii")
def decrypt_payload(payload_b64: str, aad: bytes) -> bytes:
"""
AES-GCM 解密:
输入 Base64(nonce + ciphertext + tag)
"""
raw = base64.b64decode(payload_b64)
# 至少包含 12 字节 nonce + 16 字节 tag
if len(raw) < 12 + 16:
raise ValueError("Invalid AES-GCM payload")
nonce = raw[:12]
ciphertext_and_tag = raw[12:]
return aesgcm.decrypt(nonce, ciphertext_and_tag, aad)
@app.get("/")
def index():
"""
直接由 Flask 提供前端页面
"""
index_path = APP_DIR / "index.html"
if not index_path.exists():
return "index.html not found", 500
return send_file(str(index_path))
@app.route("/login", methods=["OPTIONS"])
def login_preflight():
"""
CORS 预检请求
"""
return "", 204
@app.route("/login", methods=["POST"])
def login():
try:
body = request.get_json(force=True, silent=False) or {}
payload_b64 = body.get("payload")
if not payload_b64:
raise ValueError("missing payload")
# 解密请求
plaintext = decrypt_payload(payload_b64, REQUEST_AAD)
credential = json.loads(plaintext.decode("utf-8"))
username = str(credential.get("username", "")).strip()
password = str(credential.get("password", ""))
print(f"[login] username={username}")
# 演示账号
if username == "admin" and password == "Admin@123":
result = {
"code": 0,
"message": "登录成功",
"data": {
"username": username,
"token": secrets.token_urlsafe(32),
},
}
else:
result = {
"code": 1001,
"message": "用户名或密码错误",
}
except InvalidTag:
# AES-GCM Tag 校验失败
# 说明 nonce / ciphertext / tag / AAD 被篡改
print("[login] AES-GCM InvalidTag: data integrity broken")
result = {
"code": 4001,
"message": "数据完整性被破坏:请求密文或附加数据被篡改",
}
result_bytes = json.dumps(result, ensure_ascii=False).encode("utf-8")
encrypted_result = encrypt_payload(result_bytes, RESPONSE_AAD)
return jsonify({"payload": encrypted_result}), 400
except Exception as e:
print(f"[login] error: {e}")
result = {
"code": 500,
"message": "请求非法或解密失败",
}
result_bytes = json.dumps(result, ensure_ascii=False).encode("utf-8")
encrypted_result = encrypt_payload(result_bytes, RESPONSE_AAD)
return jsonify({"payload": encrypted_result}), 400
# 正常响应
result_bytes = json.dumps(result, ensure_ascii=False).encode("utf-8")
encrypted_result = encrypt_payload(result_bytes, RESPONSE_AAD)
return jsonify({"payload": encrypted_result})
if __name__ == "__main__":
app.run(host=HOST, port=PORT, debug=False)
- bp抓包结果

可以看到请求和响应都是密文,此时数据保密性得到了保障,下面我们来试验数据完整性

勾选"模拟攻击者篡改请求密文",此时进行如下修改:
/**
* 模拟攻击者篡改 AES-GCM 密文
*
* payload 结构:
* Base64(iv + ciphertext + tag)
*
* 这里修改第一个密文字节,也就是 iv 后面的字节。
* AES-GCM 解密时会因为 Tag 校验失败而报错。
*/
function tamperPayload(payloadB64) {
const raw = forge.util.decode64(payloadB64);
// 至少要有 12 字节 iv + 16 字节 tag
if (raw.length <= 12 + 16) {
return payloadB64;
}
const bytes = new Uint8Array(raw.length);
for (let i = 0; i < raw.length; i++) {
bytes[i] = raw.charCodeAt(i);
}
// 修改第一个密文字节
// bytes[0] 到 bytes[11] 是 iv
// bytes[12] 开始是密文
bytes[12] ^= 0x01;
let binary = "";
for (let i = 0; i < bytes.length; i++) {
binary += String.fromCharCode(bytes[i]);
}
return forge.util.encode64(binary);
}
该函数用于模拟在真实环境下攻击者修改密文的操作,此时后端会返回请求完整性被破坏,保证了数据完整性
AES-GCM原理解释¶
可以简单理解为:
普通 AES 只负责“加密”,AES-GCM 在加密的基础上还负责“防篡改”。
AES-GCM 的认证过程¶
AES-GCM 发送数据时,一般包含三部分:
1. 发送方加密并生成 Tag¶
发送方发送登录请求时:
AES-GCM 会做两件事:
第一步:加密明文¶
使用 AES 计数器模式,也就是 CTR 模式,把明文加密成密文:
此时保证的是**机密性**,别人看不到明文。
第二步:计算认证标签 Tag¶
AES-GCM 还会对下面这些内容计算一个认证值:
最终生成一个 Tag,例如 16 字节:
这个 Tag 相当于:
所以最后发送:
2. 接收方验证 Tag¶
后端收到数据后,不会直接相信密文。
它会做:
如果一致:
如果不一致:
也就是我们前面示例中的:
AES-GCM 和普通 AES 最大的不同¶
这里说的“普通 AES”,通常指类似:
这些模式本身只做加密,不提供完整性保护。
对比表¶
| 能力 | 普通 AES | AES-GCM |
|---|---|---|
| 加密数据 | 支持 | 支持 |
| 防止别人看到明文 | 支持 | 支持 |
| 防止别人篡改密文 | 不支持 | 支持 |
| 是否能发现数据被修改 | 不能 | 能 |
| 是否生成 Tag | 不生成 | 生成 |
| 是否支持 AAD | 通常不支持 | 支持 |
| 安全类型 | 加密算法 | AEAD 认证加密 |
一个直观例子¶
假设原始密文是:
攻击者把它改成:
如果是普通 AES¶
后端可能照样解密,只是解密出来的内容变了。
后端不一定能发现数据被篡改。
例如可能解出一个错误用户名、错误密码,甚至导致程序解析异常。
如果是 AES-GCM¶
后端会先验证 Tag。
因为密文被改了,但攻击者没有密钥,无法重新生成正确的 Tag。
所以后端会发现:
然后直接拒绝:
一句话总结¶
普通 AES 只解决“别人看不看得到”的问题。
AES-GCM 不仅解决“别人看不看得到”,还解决“别人有没有偷偷改过”的问题。
也就是:
AES-GCM的传输格式¶
简短结论:AES-GCM 本身没有规定一个统一的“固定传输格式”。它只规定了加密/认证后会得到几类数据,通常包括:
ciphertext:密文,长度一般等于明文长度;tag:认证标签,常见长度是 16 字节,但也可以是 12、13、14、15、16 字节等;nonce/IV:随机数或初始化向量,GCM 推荐常用 12 字节,但不是唯一选择;- 可选的
AAD/AD:附加认证数据,不加密,但参与认证。
所以后端要分清哪部分是 tag、哪部分是密文,必须依赖事先约定好的封装格式或加密库的行为。如果没有任何约定,仅看一串二进制数据,是无法可靠区分密文和 tag 的,因为它们看起来都是随机字节。
AES-GCM 的输出组成¶
AES-GCM 加密逻辑上会产生这些东西:
也就是说,完整解密通常需要:
其中:
| 字段 | 是否保密 | 是否需要传给后端 | 常见长度 |
|---|---|---|---|
| nonce/IV | 通常不保密 | 需要 | 常见 12 字节 |
| ciphertext | 保密 | 需要 | 等于明文长度 |
| tag | 不保密,但不可篡改 | 需要 | 常见 16 字节 |
| AAD | 不保密 | 如果有,需要一起传 | 任意 |
常见封装格式¶
1. 最清晰:JSON 字段分开传¶
例如:
{
"alg": "AES-256-GCM",
"iv": "base64...",
"ciphertext": "base64...",
"tag": "base64...",
"aad": "base64..."
}
后端解析时非常明确:
这是最推荐的方式之一,因为不容易混淆。
2. 拼接格式:nonce || ciphertext || tag¶
很多系统会约定一个二进制拼接格式:
+--------+----------------+-------+
| nonce | ciphertext | tag |
+--------+----------------+-------+
12字节 明文长度 16字节
比如约定:
后端解析方式:
伪代码:
这种格式常见,但前提是后端必须知道:
如果没有这个约定,后端无法准确切分。
3. 拼接格式:nonce || ciphertext+tag¶
有些加密库会把 ciphertext 和 tag 拼在一起返回,看起来像:
+--------+-------------------------+
| nonce | ciphertext || tag |
+--------+-------------------------+
也就是:
其中 encrypted_data_with_tag 的最后 16 字节通常是 tag。
例如:
raw = base64_decode(data)
nonce = raw[:12]
ct_with_tag = raw[12:]
ciphertext = ct_with_tag[:-16]
tag = ct_with_tag[-16:]
很多库或接口会这样处理,比如:
- Web Crypto API 的
AES-GCM; - Go 的
crypto/cipher的Seal/Open; - OpenSSL 的一些使用方式;
- 某些高层加密库。
4. 使用标准协议格式¶
如果你们使用的是 JWE、JOSE、JWT、Age、TLS 记录协议、NaCl/SecretBox 等协议,那么格式由协议规定,而不是由 AES-GCM 本身规定。
例如 JWE 可能包含:
这种情况下后端按照 JWE 规范解析即可。
生产环境需特别注意的点¶
生产环境使用 AES-GCM 时,最重要的不是“算法本身是否安全”,而是**密钥管理、Nonce 管理、协议设计、部署方式**是否正确。
AES-GCM 算法本身是安全的,但如果使用方式错误,仍然会出严重问题。
1. 密钥绝不能硬编码在前端¶
错误示例:
或者:
如果密钥写在前端 JS 中,任何人都可以通过浏览器开发者工具看到。
此时 AES-GCM 的加密和认证基本没有意义,因为攻击者拿到密钥后,可以:
生产环境中,前端不应该保存长期对称密钥。
更合理的做法是:
如果确实需要应用层加密,应使用:
而不是前后端共享一个固定密钥。
2. AES-GCM 的 Nonce 绝对不能重复¶
AES-GCM 中,同一个密钥下:
如果 Nonce 重复,后果非常严重。
同一个密钥 + 同一个 Nonce 加密两条不同明文,攻击者可能:
所以生产环境必须保证:
常见做法:
随机 Nonce¶
例如每次生成 12 字节随机数:
适合请求量不是极端高的场景。
计数器 Nonce¶
例如:
适合高并发场景,但必须保证计数器不重复。
3. 必须正确验证 Tag,失败后立即拒绝¶
AES-GCM 解密时如果 Tag 不匹配,必须认为数据不可信。
不能出现这种情况:
正确逻辑:
在 Python 中通常捕获:
例如:
try:
plaintext = aesgcm.decrypt(nonce, ciphertext_and_tag, aad)
except InvalidTag:
raise ValueError("数据完整性被破坏")
4. 不要使用弱密钥¶
该算法密钥只对长度有要求,对复杂度没有要求,很多开发者为了图方便,就使用了一些默认的测试密钥,比如很经典的"1234567890123456","password123456",这些密钥虽然长度比较长且符合算法要求,但是属于弱密钥,攻击者完全可以爆破或者猜测出来