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 调用时通常也是分开传参的,理解这一点,后面看代码就不会乱。

对称加密API的三层参数与调用链 ① 算法 Algorithm AES-128 / AES-256 SM4(分组=密钥=128位) 决定分组长度与密钥长度 ② 模式 Mode GCM(认证加密) CBC / CTR / ECB 决定分组串联与完整性 ③ 填充 Padding PKCS7 / NoPadding GCM、CTR 无需填充 决定末组补齐方式 密钥 Key CSPRNG 生成,禁止硬编码 AES-256=32字节 / SM4=16字节 IV / Nonce CBC:16字节随机IV GCM:12字节Nonce,不可复用 附加认证数据 AAD 仅GCM使用,如报文头 参与校验但不加密 API 输出 CBC:密文 Ciphertext(需另存IV) GCM:密文 + 16字节认证标签 Tag(解密时校验)

二、密钥生成:长度、随机源与生命周期

密钥必须由密码学安全的随机数生成器(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
CBC 模式下,若把 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-GCMAES-256-CBCSM4-GCM
密钥长度32 字节32 字节16 字节
IV/Nonce12 字节随机16 字节随机12 字节随机
填充NoPaddingPKCS7NoPadding
完整性内置 Tag需额外 HMAC内置 Tag
典型API串AES/GCM/NoPaddingAES/CBC/PKCS5PaddingSM4/GCM/NoPadding

最后列出几个高频错误,供自查:把 ECB 当作默认模式;GCM 的 Nonce 用固定值或计数器越界复用;CBC 的 IV 复用或写死;密钥硬编码在源码中;解密失败时把异常细节返回给调用方。避开这些,对称加密 API 的使用就基本可靠了。