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 汇总了本节的逻辑关系,方便对照复习。
2.7 避坑汇总
- IV 重用:CBC 模式下 IV 被攻击者知道后虽然无法直接解密,但相同 IV 会导致相同前缀;CTR/GCM 模式下 IV 重用直接导致密钥流重用,后果是密文被破解。我习惯用
crypto.randomBytes(12)生成 IV,一次一扔。 - 密钥长度:有些老版 JDK 限制 128 位 AES,需要安装 JCE 无限强度策略文件。现在 JDK 8 以上默认支持 256 位。你可以在 SDK 里先检查
Cipher.getMaxAllowedKeyLength("AES")。 - 填充攻击:CBC 模式配合 PKCS#7 有 Padding Oracle 攻击风险。如果攻击者能判断解密后的填充是否合法,就能逐字节猜出明文。解决方法:用 GCM 或 CCM 这类认证加密模式。
- 3DES 性能:3DES 加密速度比 AES 慢好几倍。我优化过一个支付通道,把 3DES 换成 AES-256-GCM 后吞吐量提升了 4 倍。能不用就别用。
你想想看,SDK 对你是黑盒,但底层这些细节决定了系统是不是「看起来安全」。调 encrypt() 前多花一分钟确认工作模式,省得后来熬夜改 bug。