2. 对称加密算法基础

这一章咱们聊聊对称加密算法。说白了,就是加密和解密用同一把钥匙。你在 SDK 里调接口时,最常打交道的就是 AES、DES 和 SM4 这几个。我记得刚入行那会儿,总觉得算法原理离我挺远,直到有一次项目里选错了工作模式,线上数据解密全乱套……嗯,从那以后我再也不敢把算法原理当「八股文」来背了。

2.1 三大主流算法:AES、DES、SM4

先看个对比表,心里有个底。

算法 密钥长度 分组大小 轮数 安全强度 典型场景
DES 56 位 64 位 16 低(已不被推荐) 遗留系统兼容
3DES 112 或 168 位 64 位 48 中(效率低) 金融 IC 卡老协议
AES 128 / 192 / 256 位 128 位 10 / 12 / 14 高 Web API、文件加密、TLS
SM4 128 位 128 位 32 高(国标) 国内政务、金融、物联网

关键点:DES 的 56 位密钥用现代 GPU 几小时就能爆破。我见过一个老系统还在用 DES,后来出安全通报才紧急升级到 AES-256。

AES 是目前全球最通用的。SM4 是国密标准,原理和 AES 相似但轮函数不同。我个人习惯在海外项目里用 AES-256-GCM,在国内合规项目里用 SM4-GCM。

2.2 算法原理速览

你不需要手写轮函数,但理解核心流程能帮你排错。对称加密都是「分组 + 轮迭代」。

  • 初始变换:把明文分组和密钥做一次异或。
  • 多轮迭代:每轮做「字节代换 → 行移位 → 列混淆 → 轮密钥加」(AES 是四步,SM4 略有不同)。
  • 最后一轮:省略列混淆。
  • 密钥扩展:从主密钥生成每轮子密钥。

为什么会设计这么多轮?说白了就是为了「混淆」和「扩散」,让密文完全看不出明文的规律。你在调 SDK 时不用操作这些细节,但理解它能帮你解释为什么有些 Key 长度会报错——比如 AES 不支持 192 位的时候,可能就是底层硬件没实现。

2.3 工作模式:ECB、CBC、CFB、OFB、CTR、GCM

工作模式决定了分组怎么连起来。这个选不对,直接关系到安全。我在项目中踩过 ECB 的坑,下面仔细说。

模式 加密方式 是否需要 IV 并行 错误传播 常见问题
ECB 每个分组独立加密 否 是 无 相同明文得到相同密文,易被识别
CBC 前一个密文与当前明文异或后再加密 是 否 传播到下一分组 IV 随机性不足会泄露信息
CFB 把分组密码当流密码用 是 否 传播到下一分组 性能不如 CTR
CTR 计数器 + 密钥生成密钥流,与明文异或 是(Nonce + Counter) 是 无 Nonce 重复会导致密钥流重用
GCM CTR 模式 + 认证标签 是(IV / Nonce) 是 无 IV 建议 12 字节,其他长度需额外处理

我曾经 接手过一个信用卡号加密项目,前人用的 ECB。密文字段里有规律重复的块,攻击者一看就知道是相同卡号。后来全部改成 CBC + 随机 IV 才解决。

我个人建议:除非你非常确定不会产生重复明文块,否则永远不要用 ECB。拿来做测试可以,上线不行。

2.4 填充方式

对称加密要求明文长度是分组大小的整数倍。不是整数倍怎么办?填充。常见的几种:

  • PKCS#7 / PKCS#5:缺多少字节就填对应数值。比如 AES 128 位分组(16 字节),明文 10 字节,就填 6 个 0x06。最常用。
  • Zero Padding:补 0x00。问题在于无法区分补的 0 和明文里的 0。
  • ISO 10126:最后一个字节填缺的字节数,其余随机。
  • ANS X.923:最后一个字节填缺的字节数,其余填 0。
  • No Padding:要求明文长度必须是分组整数倍,否则报错。

你在 SDK 里调 AES/CBC/PKCS7Padding 就是最常见的组合。有一个细节:如果明文刚好是 16 的倍数,PKCS#7 会额外补 16 个 0x10,解密后需要自己去掉。有些 SDK 自动处理了,有些需要你手动去除——调一下 setPadding 或者用 GCM 模式就省心了。

2.5 SDK 中的典型调用方式

下面给两个例子。一个是 Java 的 javax.crypto,一个是 Node.js 的 crypto。代码风格我尽量贴近真实项目写法。

Java 示例:AES-256-CBC-PKCS7Padding

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;

public class AesUtil {
    public static String encrypt(String plainText, byte[] key, byte[] iv) throws Exception {
        SecretKeySpec keySpec = new SecretKeySpec(key, "AES");
        IvParameterSpec ivSpec = new IvParameterSpec(iv);
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        // 注意:Java 里 PKCS5Padding 和 PKCS7Padding 在 AES 上等价
        cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
        byte[] encrypted = cipher.doFinal(plainText.getBytes("UTF-8"));
        return Base64.getEncoder().encodeToString(encrypted);
    }
}

Java 默认的 PKCS5Padding 就是 PKCS#7,因为 Java 的 BlockSize 固定 8 字节,但 AES 是 16 字节——实际上实现里兼容了。你不用纠结这个名字。

Node.js 示例:AES-256-GCM(推荐)

const crypto = require('crypto');

function encryptAesGcm(plaintext, key, iv) {
    const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
    let encrypted = cipher.update(plaintext, 'utf8', 'base64');
    encrypted += cipher.final('base64');
    const authTag = cipher.getAuthTag().toString('base64');
    return { encrypted, authTag };
}

// 解密时需传入 authTag
function decryptAesGcm(ciphertext, key, iv, authTag) {
    const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
    decipher.setAuthTag(Buffer.from(authTag, 'base64'));
    let decrypted = decipher.update(ciphertext, 'base64', 'utf8');
    decrypted += decipher.final('utf8');
    return decrypted;
}

注意上面 GCM 需要额外存储认证标签。我项目里一般把 IV、密文、authTag 一起拼成 IV + 密文 + Tag 的格式,解码时按长度拆开。

2.6 知识体系图

下面这张 SVG 汇总了本节的逻辑关系,方便对照复习。

对称加密核心逻辑 算法选择 AES DES / 3DES SM4 工作模式 ECB(不推荐) CBC(常用) CTR / GCM(推荐) CFB / OFB 填充方式 PKCS#7 No Padding Zero Padding 模式 --> 填充 --> SDK 典型调用栈 开发时 优先选GCM 避免ECB IV必须是 随机+不重复 (尤其CTR/GCM)

2.7 避坑汇总

  1. IV 重用:CBC 模式下 IV 被攻击者知道后虽然无法直接解密,但相同 IV 会导致相同前缀;CTR/GCM 模式下 IV 重用直接导致密钥流重用,后果是密文被破解。我习惯用 crypto.randomBytes(12) 生成 IV,一次一扔。
  2. 密钥长度:有些老版 JDK 限制 128 位 AES,需要安装 JCE 无限强度策略文件。现在 JDK 8 以上默认支持 256 位。你可以在 SDK 里先检查 Cipher.getMaxAllowedKeyLength("AES")。
  3. 填充攻击:CBC 模式配合 PKCS#7 有 Padding Oracle 攻击风险。如果攻击者能判断解密后的填充是否合法,就能逐字节猜出明文。解决方法:用 GCM 或 CCM 这类认证加密模式。
  4. 3DES 性能:3DES 加密速度比 AES 慢好几倍。我优化过一个支付通道,把 3DES 换成 AES-256-GCM 后吞吐量提升了 4 倍。能不用就别用。

你想想看,SDK 对你是黑盒,但底层这些细节决定了系统是不是「看起来安全」。调 encrypt() 前多花一分钟确认工作模式,省得后来熬夜改 bug。