SM4 对称加密实战:ECB/CBC 模式与密钥安全

国密改造里用得最多的对称加密算法就是 SM4。你想想看,大部分业务数据传输都需要加密,而 SM4 的效率和安全性在国产环境里算是很稳的。不过,我发现在很多项目中,大家把注意力全放在算法调用上,密钥的管理反而成了最薄弱的环节。这一章咱们就动手实现 SM4 的 ECB 和 CBC 模式,再重点聊聊密钥怎么生成、怎么安全存。

4.1 SM4 算法快速回顾

SM4 分组长度 128 位,密钥长度也是 128 位。说白了和 AES 的结构很像,但算法细节完全不同。在 Java 里,我习惯用 BouncyCastle 或者 Hutool 来封装,省去自己写底层的麻烦。

提示:如果你的 Spring Cloud 项目里已经引了 BouncyCastle 依赖,直接调用 Provider 即可。Hutool 的 Sm4 工具类底层也是基于 BC 的,但上层 API 更友好。

4.2 ECB 和 CBC 模式:差异与选型

很多新手上来就问:用哪个模式好?我建议你先搞懂它们的区别。

对比维度 ECB 模式 CBC 模式
加密方式 每个分组独立加密 前一个密文参与当前分组加密
是否需要 IV 不需要 需要,且每次加密必须随机
并行处理 支持并行 不支持并行
相同明文结果 相同密钥下结果完全一样 每次 IV 不同,结果不同
安全风险 易受重放攻击和模式识别 相对安全,但 IV 不能重复

我记得早年在一个内网项目里,团队为了简单直接用了 ECB,结果安全审计时发现多个设备发送的相同数据加密后密文一模一样,很容易被统计分析。后来我们全线改成了 CBC 模式。

警告:千万不要在传输多个数据块时使用 ECB 模式。如果你的业务场景必须并行加密,可以考虑 CTR 或 GCM 模式,但国密 SM4 目前官方只推荐 ECB、CBC 和 CTR(参考 GM/T 0002)。

4.3 密钥生成实战

密钥生成看起来就一行代码,但坑特别多。看代码:

import org.bouncycastle.jce.provider.BouncyCastleProvider;
import javax.crypto.KeyGenerator;
import java.security.Key;
import java.security.SecureRandom;
import java.security.Security;

Security.addProvider(new BouncyCastleProvider());

KeyGenerator keyGen = KeyGenerator.getInstance("SM4", "BC");
// 生成 128 位密钥
keyGen.init(128, new SecureRandom());
Key secretKey = keyGen.generateKey();
byte[] keyBytes = secretKey.getEncoded();

这里有两个重点:

  • 随机数质量:一定要用 SecureRandom,别用 Random。SecureRandom 在 Linux 下默认取 /dev/random,高安全场景可以用 SecureRandom.getInstanceStrong()。
  • 密钥长度:SM4 固定 128 位,如果传了 256 位会抛异常。

在实际项目中,我还见过有人每次启动都新生成密钥,导致之前加密的数据全部无法解密。所以密钥生成后必须持久化存储,并且要保证唯一性。

4.4 密钥安全存储方案

我经常说:密钥暴露是最大的灾难。密钥不能硬编码,更不能明文放在配置文件中。给大家推荐几种我亲自用过的方案:

  1. Java KeyStore(JKS/PKCS12):适合单体应用或非容器化部署。把 SM4 密钥导入 keystore,应用启动时通过密码加载。
  2. 配置中心加密存储:比如 Apollo 或 Nacos,开启配置加密插件,密钥以密文形式存储在配置中心,应用启动时解密加载。
  3. 硬件安全模块(HSM)或 KMS:金融项目必选。密钥不出设备,应用只调用加密接口。

我曾经犯过一个低级的错:为了调试方便,把 SM4 密钥写死在 properties 文件里,并且不小心提交到了 Git。结果整个分支的密钥泄露,不得不紧急重置所有敏感数据。从那以后,我强制要求团队所有密钥必须走密钥管理服务。

核心原则:密钥存储的全生命周期管理比算法本身更重要。如果你的 Spring Cloud 项目里还在把密钥写成字符串变量,请立刻改掉。

下面是用 KeyStore 加载 SM4 密钥的示例(PKCS12):

// 加载 keystore
KeyStore ks = KeyStore.getInstance("PKCS12");
try (FileInputStream fis = new FileInputStream("sm4-keys.p12")) {
    ks.load(fis, "storepass".toCharArray());
}
// 读取 SM4 密钥
Key key = ks.getKey("sm4-key-alias", "keypass".toCharArray());
// 还原为 SecretKeySpec
SecretKeySpec sm4Key = new SecretKeySpec(key.getEncoded(), "SM4");

4.5 ECB 和 CBC 模式完整实现

这里我直接给出基于 BouncyCastle 的通用加解密方法,同时兼容两种模式:

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.SecureRandom;

public class Sm4Util {

    private static final String ALGORITHM = "SM4";
    private static final String ECB_PADDING = "SM4/ECB/PKCS7Padding";
    private static final String CBC_PADDING = "SM4/CBC/PKCS7Padding";

    // ECB 加密
    public static byte[] encryptEcb(byte[] data, byte[] key) throws Exception {
        SecretKeySpec keySpec = new SecretKeySpec(key, ALGORITHM);
        Cipher cipher = Cipher.getInstance(ECB_PADDING, "BC");
        cipher.init(Cipher.ENCRYPT_MODE, keySpec);
        return cipher.doFinal(data);
    }

    // CBC 加密(IV 随机生成)
    public static byte[] encryptCbc(byte[] data, byte[] key, byte[] iv) throws Exception {
        SecretKeySpec keySpec = new SecretKeySpec(key, ALGORITHM);
        Cipher cipher = Cipher.getInstance(CBC_PADDING, "BC");
        IvParameterSpec ivSpec = new IvParameterSpec(iv);
        cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
        return cipher.doFinal(data);
    }

    // 生成随机 IV(16 字节)
    public static byte[] generateIv() {
        byte[] iv = new byte[16];
        new SecureRandom().nextBytes(iv);
        return iv;
    }
}

使用 CBC 时,务必将 IV 和密文一起传输(IV 不需要加密,但不能被篡改)。我习惯把 IV 拼接在密文前面,放到同一个字段里传递给接收方。

个人建议:在实际微服务调用中,建议统一使用 CBC 模式。虽然 ECB 性能略好,但安全短板太大。如果一定要用 ECB,请确保每个被加密的数据块都是完全独立且不可预测的。

4.6 典型流程:密钥分发与加解密

下面这张图展示了在 Spring Cloud 架构中,如何通过密钥管理服务分发 SM4 密钥,并在服务间进行加密通信:

SM4 对称加密在微服务中的密钥分发与加解密流程 密钥管理服务 (KMS / KeyStore) 服务 A SM4 加密 网络传输 密文 + IV 服务 B SM4 解密 密钥分发给服务A 密钥分发给服务B 加密后发送 接收密文 关键要点 • 密钥由统一服务生成并分发 • 服务 A/B 使用相同 SM4 密钥 • 每次 CBC 加密需要新 IV • IV 随密文传输,不保密 • 密钥必须持久化保护

4.7 常见问题与避坑

  • 填充方式:SM4 标准推荐 PKCS7 填充,但有些旧 SDK 默认无填充,需要显式指定。
  • IV 管理:每次加密必须使用不同的随机 IV。我习惯把 IV 拼接在密文前 16 字节,解密时直接截取。
  • 密钥轮换:定期更换密钥。在 Spring Cloud 中可以通过配置中心的动态刷新来热更新密钥。
  • 性能问题:ECB 虽然可以并行加密,但若数据长度不是 16 字节整数倍,仍需要串行处理补齐。
小结:SM4 的实战核心不在于调用 encrypt/decrypt 那两行代码,而在于密钥的全生命周期管理——生成、存储、分发、轮换。ECB 能不用就不用,CBC 结合随机 IV 是更安全的选择。