常见对称加密算法:AES、DES、3DES 原理、模式及 Java 实现

对称加密,说白了就是加密和解密用同一把钥匙。你想想看,就像你家大门钥匙,开门关门都用它。我刚开始接触这个领域时,总觉得算法越复杂越安全,后来踩了几个坑才明白,选对算法和模式,比盲目套用更重要。

这一章我把几个最常用的对称加密算法——DES、3DES、AES 过一遍,顺便聊聊它们的工作模式(ECB、CBC、CFB)。最后会给一段 Java 实现的代码,都是我自己在项目中验证过的。

DES:老前辈,但别在生产环境用了

DES 是 1970 年代的产物,密钥长度只有 56 位。56 位什么概念?以现在的算力,暴力破解几个小时就能搞定。我在 2016 年帮客户做金融接口迁移时,对方的老系统还在用 DES 加密交易报文,吓得我连夜跟他们解释为什么必须换掉。

  • 分组大小:64 位
  • 密钥长度:56 位(实际存 64 位,每 8 位用作奇偶校验)
  • 轮数:16 轮 Feistel 结构
  • 现状:已淘汰,仅在兼容遗留系统时可能遇到
不要在新系统里使用 DES 加密。哪怕只是临时用一下,我建议你也别偷懒——56 位密钥在 2012 年就已经被 FPGA 集群在 24 小时内破解了。

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) 快(硬件加速后更快)
如果你的系统还在用 3DES,建议尽早规划迁移到 AES。NIST 已经在 2023 年将 3DES 正式标记为“不再推荐”,过不了几年就会被各大安全标准禁用。

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 流模式,无需填充 否 否 一个错块影响后续两个块
千万不要在生产环境用 ECB 模式。如果你读到这里还在想“我用 ECB 加个随机填充是不是就安全了”,请停下手,认真回想刚才讲的图像泄露例子。填充分组只能破坏分组边界,改不了块内相同明文的依赖关系,改完还是白搭。

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;
    }
}
注意密钥长度:上面用了 32 字节(256 位)。如果你的 JDK 版本低于 8u161,可能需要安装 Java Cryptography Extension (JCE) 无限强度策略文件。更高版本默认就支持了。我在 CentOS 7 上踩过这个坑,加了策略文件才跑通。

推荐组合:目前为止,如果没有特殊合规要求,我个人最推荐 AES-256-GCM(需要 JDK 8+ 或 Bouncy Castle),因为它不仅支持认证加密,还能检测数据是否被篡改。不过 GCM 已经是另一个话题了,我们会在后面的章节单独讲。

对称加密算法与模式关系图 对称加密 DES 密钥:56位 分组:64位 3DES 密钥:112/168位 分组:64位 AES 密钥:128/192/256位 属于 属于 属于 ECB(电子密码本) CBC(密码分组链接) CFB(密文反馈) ⚠ 泄漏明文模式 相同明文 → 相同密文 ✅ 流行安全模式 随机IV + 链式依赖 ✅ 流模式无填充 适合实时流式数据 推荐:AES-256 + CBC 或 GCM(生产环境首选)