对称加密基础与AES:一把钥匙开一把锁

对称加密,听名字就知道——加密和解密用同一把密钥。你想想看,就像你用一把钥匙锁上抽屉,再用同一把钥匙打开它。这是最古老也最直觉的加密方式。我在项目里跟非技术同事解释时,就举这个例子,他们秒懂。

对称加密的核心三要素

对称加密要跑起来,离不开三样东西:明文、密钥、算法。明文是你想藏起来的数据,密钥是那把唯一的钥匙,算法就是锁芯。算法的强度决定了你这把锁容不容易被撬。

我个人习惯把对称加密分成两类:

  • 流密码:逐位加密,像一条流动的河。典型代表是 RC4,但现在已经不推荐用了。我在一个旧项目中处理过遗留的 RC4 加密,查出来的时候吓一跳——那玩意儿早就被攻破了。
  • 分组密码:把明文切成固定大小的块,一块一块处理。AES 就是最典型的分组密码,块大小固定 128 位。

为什么要分组?因为现实中数据是连续且变长的,不能指望每段消息都刚好是块大小的整数倍。于是就有了填充模式——这是后来很多人踩坑的地方。

AES:当今的行业标准

AES(Advanced Encryption Standard)是 2001 年 NIST 正式发布的标准。它取代了老旧的 DES,成为全球用得最广的对称加密算法。你手机上、浏览器里、银行转账——背后几乎都有 AES。

AES 支持三种密钥长度:128 位、192 位、256 位。位数越高越安全,但计算开销也越大。我怎么选?

密钥长度 轮数 安全等级 性能表现
AES-128 10 轮 足够安全(除量子攻击外) 最快
AES-192 12 轮 更高 稍慢
AES-256 14 轮 最高 最慢
我的建议:大多数场景用 AES-128 足够。如果你在加密密钥本身、或者要防未来量子威胁,才上 AES-256。别盲目追求 256,性能差距在移动端很明显。

AES 的工作模式:不止是拧一下钥匙

AES 本身只规定了一个块怎么加解密。但如果每个块都独立加解密,相同明文就会产生相同密文——攻击者能看出规律。这就是为什么我们需要工作模式。

我归纳了最常见的几种:

  1. ECB(电子密码本模式)——最糟糕的老模式,每个块独立加密。我曾在项目中接手一个用 ECB 加密的图片文件,密文里还能看出原图的轮廓。别用 ECB,永远别。
  2. CBC(密码分组链接模式)——每个块在加密前先和前一个密文块异或。需要初始化向量(IV)。安全性比 ECB 强很多,但并行性差。
  3. CTR(计数器模式)——把 AES 当作流密码用。每秒能处理海量数据,支持并行。我参与的一个大数据管道就用 CTR,吞吐量很满意。
  4. GCM(伽罗瓦/计数器模式)——CTR 加上身份认证。一次操作同时完成加密和校验完整性。说白了,它不仅能防泄密,还能防篡改。这是我现在最推荐的标准模式。
模式 并行性 完整性保护 常见用途
ECB ✅ 支持 ❌ 无 已废弃,仅做测试
CBC ❌ 不支持 ❌ 无 传统协议(如 SSL 3.0)
CTR ✅ 支持 ❌ 无 高性能场景
GCM ✅ 支持 ✅ 支持 现代 TLS / VPN / 密文检索

填充与 IV:最容易出错的细节

明文长度不一定正好是 16 字节的整数倍。这时候需要填充。PKCS7 是最常见的方案——缺几个字节就补几,如果恰好够 16 字节,则补一整块 16。听起来简单,但实现时没写对会导致解密密文失败。我曾经调试过一整个下午,最后发现是填充校验写反了。

IV(初始化向量)在 CBC、CTR、GCM 里都必须使用。它不需要保密,但不能重用。同一把密钥下,IV 重复一次,就可能被攻击者恢复明文。我建议用 12 字节随机数 + 4 字节计数器的组合(GCM 的做法)。

注意:IV 千万不要硬编码!我见过有人把 IV 写成全零固定值,理由是“数据不敏感”。结果呢?一周后就被安全审计抓出来了。IV 必须每次随机生成,并跟密文一起存储或传输。

AES 的底层结构(概览)

虽然咱们不是实现算法,但理解它的骨架能帮你判断密钥边界。AES 的核心操作包括:

  • SubBytes:用 S 盒替换每个字节
  • ShiftRows:行移位,打乱顺序
  • MixColumns:列混合,扩散字节
  • AddRoundKey:轮密钥异或

每一轮把这四种操作串起来。密钥从初始密钥通过密钥扩展算法生成每一轮需要的子密钥。

下面这张图展示了 AES-128 的整体流程,把密钥边界和加密路径理清楚:

AES-128 加密流程与密钥边界 明文 (128 bit) AddRoundKey (轮0) 初始密钥 (128 bit) 密钥扩展算法 轮密钥 K₀~K₁₀ 各 128 bit 第 1 ~ 9 轮 SubBytes → ShiftRows → MixColumns → AddRoundKey 轮密钥 第 10 轮 SubBytes → ShiftRows → AddRoundKey 密文 (128 bit) 密钥边界 密钥只参与 AddRoundKey 不参与 SubBytes/ShiftRows/MixColumns 密钥长度=128 bit 状态矩阵 4×4 字节

从图中你可以看出:密钥只在 AddRoundKey 这一步介入,不参与非线性替换和扩散。这意味着密钥的强度依赖扩展算法的随机性。一旦初始密钥泄漏,整条链全完。所以密钥的生成、存储、分发才是真正的边界约束。

代码示例:使用 OpenSSL 做 AES-GCM 加密

纸上得来终觉浅。我贴一段实际工作中写的 C 代码片段,用了 OpenSSL 的 EVP 接口。GCM 模式自带认证标签,非常适合密文检索场景。

#include <openssl/evp.h>
#include <string.h>

int aes_gcm_encrypt(const unsigned char *plaintext, int plaintext_len,
                    const unsigned char *key, const unsigned char *iv,
                    unsigned char *ciphertext, unsigned char *tag) {
    EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();
    if (!ctx) return -1;

    // 使用 AES-256-GCM
    if (1 != EVP_EncryptInit_ex(ctx, EVP_aes_256_gcm(), NULL, NULL, NULL))
        goto err;

    // 设置 IV 长度(GCM 推荐 12 字节)
    if (1 != EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, 12, NULL))
        goto err;

    // 初始化密钥和 IV
    if (1 != EVP_EncryptInit_ex(ctx, NULL, NULL, key, iv))
        goto err;

    int len = 0;
    // 加密
    if (1 != EVP_EncryptUpdate(ctx, ciphertext, &len, plaintext, plaintext_len))
        goto err;
    int ciphertext_len = len;

    // 结束加密
    if (1 != EVP_EncryptFinal_ex(ctx, ciphertext + len, &len))
        goto err;
    ciphertext_len += len;

    // 获取认证标签
    if (1 != EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG, 16, tag))
        goto err;

    EVP_CIPHER_CTX_free(ctx);
    return ciphertext_len;

err:
    EVP_CIPHER_CTX_free(ctx);
    return -1;
}
关键点:代码里用 EVP_CTRL_GCM_SET_IVLEN 明确设置 IV 长度,不要依赖缺省值。另外,tag 的长度取 16 字节(最多 16),但在传输时建议至少带上 12 字节。

对称加密在密文检索中的挑战

我们这门课叫《密文检索场景下密钥边界约束方案》。对称加密在密文检索里有一个天然矛盾:加密后数据失去可比较性。如果直接用 AES 加密每个字段,你没法在密文上做范围查询、关键词匹配。所以需要引入保留格式加密、顺序保留加密或者安全索引。

但无论用什么高级方案,密钥的边界定义始终是第一道防线。密钥放在哪?谁生成?怎么轮换?这些是架构层面必须回答的问题。对称加密本身很成熟,出错的地方往往在边界——比如密钥被埋在代码里、IV 重复使用、密钥长度选错。

我曾经在一个项目里看到团队用了 AES-256-CBC,密钥明文写死在配置文件里。我当时就问:你们管这个叫安全?后来他们换了密钥管理服务,才过了合规审计。


对称加密是一门基础但需要敬畏的手艺。AES 算法本身固若金汤,但密钥和 IV 的管理才是真正考验工程师的地方。下一层我们需要深入密钥派生和密钥协商,但那是后话了。先把基础打牢,才能盖好上层建筑。