常见对称加密算法:AES、DES、3DES 原理、模式及 Java 实现
对称加密,说白了就是加密和解密用同一把钥匙。你想想看,就像你家大门钥匙,开门关门都用它。我刚开始接触这个领域时,总觉得算法越复杂越安全,后来踩了几个坑才明白,选对算法和模式,比盲目套用更重要。
这一章我把几个最常用的对称加密算法——DES、3DES、AES 过一遍,顺便聊聊它们的工作模式(ECB、CBC、CFB)。最后会给一段 Java 实现的代码,都是我自己在项目中验证过的。
DES:老前辈,但别在生产环境用了
DES 是 1970 年代的产物,密钥长度只有 56 位。56 位什么概念?以现在的算力,暴力破解几个小时就能搞定。我在 2016 年帮客户做金融接口迁移时,对方的老系统还在用 DES 加密交易报文,吓得我连夜跟他们解释为什么必须换掉。
- 分组大小:64 位
- 密钥长度:56 位(实际存 64 位,每 8 位用作奇偶校验)
- 轮数:16 轮 Feistel 结构
- 现状:已淘汰,仅在兼容遗留系统时可能遇到
3DES:过渡方案,也有寿命限制
3DES 就是拿 DES 跑三遍。它有几种模式,最常见的是 EDE(加密-解密-加密),这样能兼容 DES 设备。密钥长度可以是 112 位(两把独立密钥)或 168 位(三把独立密钥)。
我记得有一次做支付接口的兼容测试,对方只支持 3DES,我们这边已经全面转型 AES 了。最后还是写了个中间层转接,性能确实差了一截,毕竟它要跑 48 轮 Feistel 网络。
| 对比项 | DES | 3DES | AES |
|---|---|---|---|
| 分组大小 | 64 bit | 64 bit | 128 bit |
| 密钥长度 | 56 bit | 112 / 168 bit | 128 / 192 / 256 bit |
| 轮数 | 16 | 48 | 10 / 12 / 14 |
| 安全强度 | 很低 | 中等(逐渐被放弃) | 高(当前标准) |
| 性能 | 快 | 慢(约 DES 的 1/3) | 快(硬件加速后更快) |
AES:当代首选,没有之一
AES 是 2001 年由 NIST 颁布的标准,设计者是 Joan Daemen 和 Vincent Rijmen。它支持 128/192/256 位密钥,分组大小固定 128 位。我个人习惯用 AES-256,虽然性能比 128 稍慢一点点,但密钥空间大得多,心里踏实。
AES 用的是 SPN(替换-置换网络)结构,不是 Feistel。每一轮包括字节代换、行移位、列混淆和轮密钥加。硬件上现代 CPU 都内置了 AES-NI 指令集,加密 1GB 数据可能只需要几百毫秒。我在做高并发 API 网关时遇到过性能瓶颈,换成 AES-128-GCM 配合流水线操作,吞吐量直接翻了三倍。
- 密钥长度:128 / 192 / 256 bit
- 分组大小:128 bit
- 轮数:10(128 位)/ 12(192 位)/ 14(256 位)
- 模式支持:ECB、CBC、CFB、OFB、CTR、GCM 等
加密模式:选错比用错更可怕
算法本身只定义如何加密一个分组,但实际数据通常比分组长,这就引出了“模式”。三种最常见的模式是 ECB、CBC 和 CFB。
ECB(电子密码本模式)
最简单,明文分组独立加密。问题是你想想看——相同明文块会生成相同密文块。如果图像用 ECB 加密,轮廓信息会完全暴露。
我曾经在项目评审时见过一个组用 ECB 加密 JSON 报文,请求头里同一个字段多次出现,密文块重复率极高,攻击者直接就能推断出数据结构。嗯,那次评审会开得挺尴尬的。
CBC(密码分组链接模式)
每个明文块先与上一个密文块异或,再加密。第一个块需要一个初始化向量(IV)。这样相同明文块加密后的密文也不同,安全多了。
注意:IV 必须随机且不可预测,而且不能重复使用同一对(Key, IV)。
CFB(密文反馈模式)
把块密码转成流密码。加密器先把 IV 加密,输出与明文异或得到密文,然后密文反馈回输入。CFB 模式不用填充,适合流式数据。
| 模式 | 特点 | 需填充 | 并行加密 | 错误传播 |
|---|---|---|---|---|
| ECB | 分组独立,简单但泄漏模式 | 是 | 是 | 无 |
| CBC | 链式依赖,安全性高 | 是 | 否(解密可并行) | 一个错块影响后续两个块 |
| CFB | 流模式,无需填充 | 否 | 否 | 一个错块影响后续两个块 |
Java 实现:AES/CBC/PKCS5Padding
说一千道一万,不如看代码。下面这段是我项目中常用的 AES/CBC/PKCS5Padding 工具类。密钥由用户提供,IV 由系统随机生成并拼接到密文前 16 个字节。解密时再取出 IV。
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.SecureRandom;
import java.util.Base64;
public class AesCbcUtil {
private static final String ALGORITHM = "AES/CBC/PKCS5Padding";
private static final int KEY_LENGTH = 32; // AES-256
public static String encrypt(String plainText, String key) throws Exception {
byte[] keyBytes = padKey(key.getBytes("UTF-8"));
SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "AES");
Cipher cipher = Cipher.getInstance(ALGORITHM);
byte[] iv = new byte[16];
SecureRandom.getInstanceStrong().nextBytes(iv);
IvParameterSpec ivSpec = new IvParameterSpec(iv);
cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
byte[] encrypted = cipher.doFinal(plainText.getBytes("UTF-8"));
// IV 拼在密文前
byte[] combined = new byte[iv.length + encrypted.length];
System.arraycopy(iv, 0, combined, 0, iv.length);
System.arraycopy(encrypted, 0, combined, iv.length, encrypted.length);
return Base64.getEncoder().encodeToString(combined);
}
public static String decrypt(String cipherText, String key) throws Exception {
byte[] combined = Base64.getDecoder().decode(cipherText);
byte[] iv = new byte[16];
System.arraycopy(combined, 0, iv, 0, iv.length);
byte[] encrypted = new byte[combined.length - iv.length];
System.arraycopy(combined, iv.length, encrypted, 0, encrypted.length);
byte[] keyBytes = padKey(key.getBytes("UTF-8"));
SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "AES");
Cipher cipher = Cipher.getInstance(ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, keySpec, new IvParameterSpec(iv));
byte[] decrypted = cipher.doFinal(encrypted);
return new String(decrypted, "UTF-8");
}
private static byte[] padKey(byte[] key) {
byte[] padded = new byte[KEY_LENGTH];
System.arraycopy(key, 0, padded, 0, Math.min(key.length, KEY_LENGTH));
return padded;
}
}
推荐组合:目前为止,如果没有特殊合规要求,我个人最推荐 AES-256-GCM(需要 JDK 8+ 或 Bouncy Castle),因为它不仅支持认证加密,还能检测数据是否被篡改。不过 GCM 已经是另一个话题了,我们会在后面的章节单独讲。