3. 对称加密API实战
对称加密是密码API中使用频率最高的一类接口:加密与解密共用同一把密钥,速度快、适合处理大块数据。但真正落到代码里,开发者最容易踩坑的并不是“调用哪个函数”,而是三件事:密钥怎么生成与保存、加密模式怎么选、填充怎么处理。本章以 AES 与 SM4 为主线,把这三件事讲清楚,并给出可直接参考的调用方式。
一、先分清算法、模式与填充三个层次
初学者常把 AES、GCM、PKCS7 混为一谈,其实它们属于不同层次:
- 算法(Algorithm):AES、SM4,决定分组长度与密钥长度。AES 分组固定 128 位,密钥可取 128/192/256 位;SM4 分组与密钥均为 128 位。
- 模式(Mode):GCM、CBC、CTR、ECB 等,决定如何把多个分组串联起来,以及是否提供完整性校验。
- 填充(Padding):PKCS7、NoPadding 等,决定最后一个不满分组如何补齐。GCM、CTR 属于流式模式,本身不需要填充。
这三层是正交组合关系:同一个 AES 算法,可以配 CBC+PKCS7,也可以配 GCM+NoPadding。API 调用时通常也是分开传参的,理解这一点,后面看代码就不会乱。
二、密钥生成:长度、随机源与生命周期
密钥必须由密码学安全的随机数生成器(CSPRNG)产生,不能用 rand()、时间戳或用户口令直接充当。以 AES-256 为例,需要 32 字节随机密钥;SM4 需要 16 字节。下面以 Java 的 JCA 为例:
// 生成 AES-256 密钥(32字节)
KeyGenerator kg = KeyGenerator.getInstance("AES");
kg.init(256, new SecureRandom()); // 使用 CSPRNG
SecretKey aesKey = kg.generateKey();
byte[] keyBytes = aesKey.getEncoded(); // 32 字节
// 生成 SM4 密钥(16字节),需引入 BouncyCastle
KeyGenerator sm4Kg = KeyGenerator.getInstance("SM4", "BC");
sm4Kg.init(128, new SecureRandom());
SecretKey sm4Key = sm4Kg.generateKey(); // 16 字节
密钥生成后要解决“放哪里”。生产环境推荐用 KMS 或 HSM 托管,应用只持有密钥引用;若必须本地保存,应使用主密钥(KEK)对数据密钥(DEK)做信封加密,而不是把明文密钥写进配置文件或代码。此外,同一密钥不应无限期使用,建议设定轮换周期,例如数据密钥 90 天轮换一次。
三、模式选择:GCM 优先,CBC 谨慎,ECB 禁用
模式决定了安全强度,选择原则可以概括为:
- GCM:认证加密(AEAD),一次调用同时得到机密性与完整性,解密时若 Tag 校验失败会直接抛异常。适合网络传输、API 报文等场景,是当前首选。
- CBC:只提供机密性,不防篡改,需要额外做 MAC 才能保证完整性。适合与既有系统兼容的场景,必须使用随机 IV。
- CTR:流式模式,无需填充,但同样不提供完整性,且计数器绝不能重复。
- ECB:相同明文分组产生相同密文,会泄露数据模式,任何场景都不应使用。
GCM 有一个致命约束:同一密钥下 Nonce 绝不能重复。一旦重复,攻击者可恢复认证密钥,安全性彻底崩塌。因此 Nonce 推荐用 12 字节随机值,或“固定前缀+单调计数器”的方式生成,并保证在密钥生命周期内唯一。
四、AES-GCM 调用示例
下面用 Java JCA 演示 AES-256-GCM 的加密与解密。注意 GCM 不需要指定填充,标签长度默认 128 位:
// ===== 加密 =====
byte[] nonce = new byte[12];
new SecureRandom().nextBytes(nonce); // 12字节随机Nonce
Cipher enc = Cipher.getInstance("AES/GCM/NoPadding");
GCMParameterSpec spec = new GCMParameterSpec(128, nonce); // Tag=128位
enc.init(Cipher.ENCRYPT_MODE, aesKey, spec);
enc.updateAAD(headerBytes); // 可选:附加认证数据
byte[] ct = enc.doFinal(plainBytes); // 密文(含Tag)
// 传输格式:nonce(12) || ct
ByteBuffer buf = ByteBuffer.allocate(12 + ct.length);
buf.put(nonce).put(ct);
// ===== 解密 =====
ByteBuffer in = ByteBuffer.wrap(received);
byte[] rNonce = new byte[12];
in.get(rNonce);
byte[] rCt = new byte[in.remaining()];
Cipher dec = Cipher.getInstance("AES/GCM/NoPadding");
dec.init(Cipher.DECRYPT_MODE, aesKey, new GCMParameterSpec(128, rNonce));
dec.updateAAD(headerBytes);
byte[] pt = dec.doFinal(rCt); // Tag校验失败会抛 AEADBadTagException
关键点:Nonce 必须随密文一起传输,但不需要保密;解密时若抛出 AEADBadTagException,说明密文、AAD 或 Tag 被篡改,应直接拒绝,绝不能返回部分明文。
五、AES-CBC 与填充处理
CBC 需要 16 字节随机 IV,并使用 PKCS7 填充。PKCS7 的规则是:若末组缺 n 字节,就补 n 个值为 n 的字节;若明文恰好是分组整数倍,则额外补一整组 16 个 0x10。解密后必须校验并去除填充,且要防止填充预言攻击(Padding Oracle)。
// ===== AES-CBC 加密 =====
byte[] iv = new byte[16];
new SecureRandom().nextBytes(iv); // 随机IV
Cipher enc = Cipher.getInstance("AES/CBC/PKCS5Padding"); // PKCS5=PKCS7
enc.init(Cipher.ENCRYPT_MODE, aesKey, new IvParameterSpec(iv));
byte[] ct = enc.doFinal(plainBytes);
// 传输格式:iv(16) || ct
// ===== 解密 =====
Cipher dec = Cipher.getInstance("AES/CBC/PKCS5Padding");
dec.init(Cipher.DECRYPT_MODE, aesKey, new IvParameterSpec(rIv));
byte[] pt = dec.doFinal(rCt); // 填充错误会抛 BadPaddingException
BadPaddingException 与其它错误区分返回给攻击者,就可能被利用做填充预言攻击。正确做法是统一返回“解密失败”,不泄露具体原因,并配合 HMAC 做完整性校验(Encrypt-then-MAC)。六、SM4 调用方式
SM4 是我国商用密码分组算法,分组与密钥均为 128 位。Java 标准库不直接支持,通常借助 BouncyCastle。SM4 常用 CBC 与 GCM 模式,调用方式与 AES 高度相似:
Security.addProvider(new BouncyCastleProvider());
// SM4-CBC
Cipher enc = Cipher.getInstance("SM4/CBC/PKCS5Padding", "BC");
enc.init(Cipher.ENCRYPT_MODE, sm4Key, new IvParameterSpec(iv));
byte[] ct = enc.doFinal(plainBytes);
// SM4-GCM
Cipher gcm = Cipher.getInstance("SM4/GCM/NoPadding", "BC");
gcm.init(Cipher.ENCRYPT_MODE, sm4Key, new GCMParameterSpec(128, nonce));
byte[] ct2 = gcm.doFinal(plainBytes);
SM4 与 AES 的差异主要在密钥长度固定为 16 字节、分组固定 16 字节,其余模式与填充规则一致。若系统需要国密合规,应优先选用 SM4-GCM 或 SM4-CBC+HMAC。
七、参数速查与常见错误
| 项目 | AES-256-GCM | AES-256-CBC | SM4-GCM |
|---|---|---|---|
| 密钥长度 | 32 字节 | 32 字节 | 16 字节 |
| IV/Nonce | 12 字节随机 | 16 字节随机 | 12 字节随机 |
| 填充 | NoPadding | PKCS7 | NoPadding |
| 完整性 | 内置 Tag | 需额外 HMAC | 内置 Tag |
| 典型API串 | AES/GCM/NoPadding | AES/CBC/PKCS5Padding | SM4/GCM/NoPadding |
最后列出几个高频错误,供自查:把 ECB 当作默认模式;GCM 的 Nonce 用固定值或计数器越界复用;CBC 的 IV 复用或写死;密钥硬编码在源码中;解密失败时把异常细节返回给调用方。避开这些,对称加密 API 的使用就基本可靠了。