跳转至

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

image-20260817113732299

  • 服务端Demo(python实现)
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抓包结果

image-20260817113803436

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

image-20260817114000924

勾选"模拟攻击者篡改请求密文",此时进行如下修改:

JavaScript
/**
     * 模拟攻击者篡改 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 发送数据时,一般包含三部分:

Text Only
Nonce + AES加密后密文 + Tag

1. 发送方加密并生成 Tag

发送方发送登录请求时:

JSON
{
  "username": "admin",
  "password": "Admin@123"
}

AES-GCM 会做两件事:

第一步:加密明文

使用 AES 计数器模式,也就是 CTR 模式,把明文加密成密文:

Text Only
明文 ---> AES-GCM 加密 ---> 密文

此时保证的是**机密性**,别人看不到明文。


第二步:计算认证标签 Tag

AES-GCM 还会对下面这些内容计算一个认证值:

Text Only
Nonce
密文
附加认证数据 AAD
数据长度信息

最终生成一个 Tag,例如 16 字节:

Text Only
Tag = 基于密钥和密文等信息计算出的“防篡改指纹”

这个 Tag 相当于:

Text Only
只有拥有密钥的人,才能为这份密文生成正确的认证标签

所以最后发送:

Text Only
Nonce + 密文 + Tag

2. 接收方验证 Tag

后端收到数据后,不会直接相信密文。

它会做:

Text Only
1. 用同一个密钥、Nonce、密文、AAD 重新计算 Tag
2. 比较计算出来的 Tag 是否等于请求中带来的 Tag

如果一致:

Text Only
说明数据没有被篡改,允许解密

如果不一致:

Text Only
说明数据被修改过,直接报错

也就是我们前面示例中的:

Text Only
数据完整性被破坏:请求密文或附加数据被篡改

AES-GCM 和普通 AES 最大的不同

这里说的“普通 AES”,通常指类似:

Text Only
AES-ECB
AES-CBC
AES-CTR

这些模式本身只做加密,不提供完整性保护。


对比表

能力 普通 AES AES-GCM
加密数据 支持 支持
防止别人看到明文 支持 支持
防止别人篡改密文 不支持 支持
是否能发现数据被修改 不能 能
是否生成 Tag 不生成 生成
是否支持 AAD 通常不支持 支持
安全类型 加密算法 AEAD 认证加密

一个直观例子

假设原始密文是:

Text Only
ciphertext = ABCD

攻击者把它改成:

Text Only
ciphertext = ABCE

如果是普通 AES

后端可能照样解密,只是解密出来的内容变了。

后端不一定能发现数据被篡改。

例如可能解出一个错误用户名、错误密码,甚至导致程序解析异常。


如果是 AES-GCM

后端会先验证 Tag。

因为密文被改了,但攻击者没有密钥,无法重新生成正确的 Tag。

所以后端会发现:

Text Only
Tag 校验失败

然后直接拒绝:

Text Only
数据完整性被破坏

一句话总结

普通 AES 只解决“别人看不看得到”的问题。

AES-GCM 不仅解决“别人看不看得到”,还解决“别人有没有偷偷改过”的问题。

也就是:

Text Only
普通 AES = 机密性
AES-GCM = 机密性 + 完整性 + 认证

AES-GCM的传输格式

简短结论:AES-GCM 本身没有规定一个统一的“固定传输格式”。它只规定了加密/认证后会得到几类数据,通常包括:

  1. ciphertext:密文,长度一般等于明文长度;
  2. tag:认证标签,常见长度是 16 字节,但也可以是 12、13、14、15、16 字节等;
  3. nonce/IV:随机数或初始化向量,GCM 推荐常用 12 字节,但不是唯一选择;
  4. 可选的 AAD/AD:附加认证数据,不加密,但参与认证。

所以后端要分清哪部分是 tag、哪部分是密文,必须依赖事先约定好的封装格式或加密库的行为。如果没有任何约定,仅看一串二进制数据,是无法可靠区分密文和 tag 的,因为它们看起来都是随机字节。


AES-GCM 的输出组成

AES-GCM 加密逻辑上会产生这些东西:

Text Only
plaintext  --加密-->  ciphertext
plaintext + ciphertext + AAD + nonce --认证--> tag

也就是说,完整解密通常需要:

Text Only
nonce/IV
ciphertext
tag
AAD,如果有的话

其中:

字段 是否保密 是否需要传给后端 常见长度
nonce/IV 通常不保密 需要 常见 12 字节
ciphertext 保密 需要 等于明文长度
tag 不保密,但不可篡改 需要 常见 16 字节
AAD 不保密 如果有,需要一起传 任意

常见封装格式

1. 最清晰:JSON 字段分开传

例如:

JSON
{
  "alg": "AES-256-GCM",
  "iv": "base64...",
  "ciphertext": "base64...",
  "tag": "base64...",
  "aad": "base64..."
}

后端解析时非常明确:

Text Only
iv         = json.iv
ciphertext = json.ciphertext
tag        = json.tag
aad        = json.aad

这是最推荐的方式之一,因为不容易混淆。


2. 拼接格式:nonce || ciphertext || tag

很多系统会约定一个二进制拼接格式:

Text Only
+--------+----------------+-------+
| nonce  |   ciphertext   |  tag  |
+--------+----------------+-------+
  12字节      明文长度        16字节

比如约定:

Text Only
nonce 长度 = 12 字节
tag 长度   = 16 字节
格式       = nonce + ciphertext + tag

后端解析方式:

Text Only
raw = base64_decode(data)

nonce      = raw[0 : 12]
tag        = raw[-16 : ]
ciphertext = raw[12 : -16]

伪代码:

Python
nonce = data[:12]
tag = data[-16:]
ciphertext = data[12:-16]

这种格式常见,但前提是后端必须知道:

Text Only
nonceLen = 12
tagLen = 16
tag 在末尾

如果没有这个约定,后端无法准确切分。


3. 拼接格式:nonce || ciphertext+tag

有些加密库会把 ciphertext 和 tag 拼在一起返回,看起来像:

Text Only
+--------+-------------------------+
| nonce  | ciphertext || tag       |
+--------+-------------------------+

也就是:

Text Only
nonce + encrypted_data_with_tag

其中 encrypted_data_with_tag 的最后 16 字节通常是 tag。

例如:

Text Only
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 可能包含:

Text Only
protected header
encrypted key
iv
ciphertext
tag

这种情况下后端按照 JWE 规范解析即可。

生产环境需特别注意的点

生产环境使用 AES-GCM 时,最重要的不是“算法本身是否安全”,而是**密钥管理、Nonce 管理、协议设计、部署方式**是否正确。

AES-GCM 算法本身是安全的,但如果使用方式错误,仍然会出严重问题。

1. 密钥绝不能硬编码在前端

错误示例:

JavaScript
const KEY = "1234567890abcdef";

或者:

JavaScript
const PSK = "demo-login-psk-2026";

如果密钥写在前端 JS 中,任何人都可以通过浏览器开发者工具看到。

此时 AES-GCM 的加密和认证基本没有意义,因为攻击者拿到密钥后,可以:

Text Only
解密请求
伪造请求
篡改后重新加密
伪造合法 Tag

生产环境中,前端不应该保存长期对称密钥。

更合理的做法是:

Text Only
HTTPS/TLS 保护传输层
+
服务端会话认证

如果确实需要应用层加密,应使用:

Text Only
ECDH 密钥协商
或
服务端下发一次性会话密钥
或
PAKE / OPAQUE 等安全认证协议

而不是前后端共享一个固定密钥。


2. AES-GCM 的 Nonce 绝对不能重复

AES-GCM 中,同一个密钥下:

Text Only
Nonce 绝对不能重复

如果 Nonce 重复,后果非常严重。

同一个密钥 + 同一个 Nonce 加密两条不同明文,攻击者可能:

Text Only
恢复明文异或值
破坏认证机制
伪造消息
恢复认证子密钥

所以生产环境必须保证:

Text Only
每次加密使用唯一Nonce

常见做法:

随机 Nonce

例如每次生成 12 字节随机数:

Python
nonce = os.urandom(12)

适合请求量不是极端高的场景。

计数器 Nonce

例如:

Text Only
服务端 ID + 时间戳 + 单调递增计数器

适合高并发场景,但必须保证计数器不重复。


3. 必须正确验证 Tag,失败后立即拒绝

AES-GCM 解密时如果 Tag 不匹配,必须认为数据不可信。

不能出现这种情况:

Text Only
Tag 校验失败,但仍然继续处理部分数据

正确逻辑:

Text Only
Tag 验证失败
→ 丢弃全部数据
→ 返回错误
→ 记录安全日志

在 Python 中通常捕获:

Python
from cryptography.exceptions import InvalidTag

例如:

Python
try:
    plaintext = aesgcm.decrypt(nonce, ciphertext_and_tag, aad)
except InvalidTag:
    raise ValueError("数据完整性被破坏")

4. 不要使用弱密钥

该算法密钥只对长度有要求,对复杂度没有要求,很多开发者为了图方便,就使用了一些默认的测试密钥,比如很经典的"1234567890123456","password123456",这些密钥虽然长度比较长且符合算法要求,但是属于弱密钥,攻击者完全可以爆破或者猜测出来