一、先理解三件事
- AES 是对称加密:加密解密用同一个密钥,速度快,适合加密大量数据。
- 密钥长度 128/192/256 位,256 位最稳,性能差异在现代 CPU 上很小(有 AES-NI 指令)。
- 分组长度固定 128 位,超过一块的数据要靠「模式」串起来——模式选错,安全性会大打折扣。
二、ECB:绝对不要用
ECB 把每 16 字节独立加密,相同明文块产生相同密文块。后果很直观:
- 加密一张纯色图片,轮廓依然清晰可见(著名的「ECB 企鹅」)。
- 攻击者能看出「这两段数据相同」,可以做统计分析。
任何场景都不该用 ECB。它的存在主要是教学和历史兼容。
三、CBC:经典但要小心 IV
CBC 让每个明文块先和前一个密文块异或,因此需要:
- IV(初始化向量):16 字节,随机生成,每次加密都不同,可以随密文一起明文传输,但绝不能用固定值。
- 填充:明文长度不是 16 的倍数时要补(常用 PKCS#7)。
密文 = AES-CBC(key, IV, PKCS7(明文))
CBC 只提供机密性,不提供完整性。攻击者可以在不知道密钥的情况下篡改密文,导致解密结果被可控修改(比特翻转攻击)。所以 CBC 通常还要配一个 HMAC 做校验,而这正是最容易做错的地方。
四、GCM:现在应该默认选它
GCM 是认证加密(AEAD),一次运算同时给出密文和 16 字节认证标签(tag):
- 解密时会校验 tag,被篡改直接失败,不用额外加 HMAC。
- 支持并行计算,性能好。
- 需要 12 字节 nonce,同一个 key 下 nonce 绝不能重复——重复会直接泄露密钥流,是灾难级错误。
// Node.js 示例(AES-256-GCM)
const iv = crypto.randomBytes(12);
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
const enc = Buffer.concat([cipher.update(plain), cipher.final()]);
const tag = cipher.getAuthTag(); // 必须和密文一起保存
除了 GCM,ChaCha20-Poly1305 在移动端(无 AES 硬件加速时)表现更好,也是 AEAD。
五、实际实现里最容易错的地方
| 错误 | 后果 |
|---|---|
| IV/nonce 写死或用时间戳 | 相同明文得到相同密文,可被比对分析 |
| nonce 重复(GCM) | 密钥流复用,可恢复明文 |
用 Math.random() 生成 IV | 随机性不足,可预测 |
| 忘记保存 tag/IV | 解密必然失败 |
| 密钥硬编码在前端 | 等于没有加密 |
| 明文填充方式两边不一致 | 解密出来尾部有多余字节或报错 |
密钥管理才是真正的难点:密钥不能进代码仓库、不能放前端、要能轮换。生产环境用 KMS 或环境变量注入,别在 JS 里写 const key = "1234567890123456"。
六、为什么两边总是解不开
跨语言对接 AES 失败,99% 是这四个参数没对齐:
- 算法全名:
AES/CBC/PKCS5Padding(Java)对应 Node 的aes-128-cbc+ 自动 padding。 - 密钥怎么来:是原始 16 字节,还是把字符串做 MD5/SHA256 得到 32 字节?
- IV 从哪来:是密文前 16 字节,还是单独字段?编码是 hex 还是 Base64?
- 输出编码:Base64 还是十六进制,带不带换行。
调不通时,用同一组固定的 key/IV/明文,两边各算一次,把中间结果(十六进制)逐字节比对,比猜快得多。
七、什么时候不该用 AES
- 只需要校验没被改:用 HMAC,不用加密。
- 要给对方公钥持有者发数据:用 RSA/ECC 或混合加密(RSA 加密 AES 密钥)。
- 要存密码:不要加密,用 bcrypt/Argon2id 做不可逆哈希。
想验证某个模式的实际输出,可以用 AES 加解密工具 在本地试;密钥和内容都不上传,适合对照联调。需要把密文放进 JSON 或 URL 时,用 Base64 编解码。
小结
ECB 禁用;CBC 要随机 IV + 填充 + 额外完整性校验;GCM 是现在的默认选择,但 nonce 绝不能重复。跨语言解不开时,先对齐算法名、密钥派生、IV 位置和输出编码这四项。