通俗易懂!3分钟彻底看懂国密 SM4-CBC 加密算法(含 Java 实战代码)

简单来说,SM4-CBC 就是我国国家商用密码标准中的一种既高效又极其安全的“数据加密打包”方案

如果把敏感数据(比如你的银行卡密码)比作一封私密信件,SM4 就是锁住信件的防爆保险箱,而 CBC 则是给每个保险箱加上“环环相扣”的联动连锁机构。


一、拆解名字:SM4 和 CBC 到底是个啥?

1. SM4(国密算法)

  • 出身:SM4 是我国自主研发的分组对称加密算法,专门用来替代国际上的 AES 算法(SM 代表“商密”)。
  • 对称加密:加密和解密使用的是同一把钥匙(Key)。就像你家大门的钥匙,锁门和开门都是同一把。
  • 分组加密:它不会一次性把一整本书加密,而是把数据切成一小块一小块来处理,SM4 规定每一块的长度必须是 16 字节(128 位)

2. CBC(Cipher Block Chaining,密文分组链接模式)

如果你的数据很长(比如一段 1000 字的聊天记录),切成许多 16 字节的小块后,该怎么加密?
这就是 CBC 模式发挥作用的地方——它负责把这些小数据块像铁链一样一环扣一环地连起来加密


二、生活化举例:为什么一定要用 CBC?

假设我们要加密一段重复的话:“苹果苹果苹果苹果”。

1. 如果不加 CBC(比如使用普通的 ECB 模式):

数据被切成 4 个“苹果”。加密后,密文可能变成:“X9#K X9#K X9#K X9#K”。

  • 缺陷:黑客虽然不知道加密前是啥,但他打眼一看就知道:“这 4 段内容是一模一样的!” 这就给破解留下了巨大破绽。

2. 如果使用了 CBC 模式:

CBC 会在加密第 2 个“苹果”时,先把它和第 1 个“苹果”加密后的结果混合搅拌(异或运算),再去加密。

  • 结果:加密后的密文变成了:“X9#K A8@m P2!x W7$L”,完全看不出任何重复规律。

3. 核心概念:IV(初始向量)

既然第 2 个块依赖第 1 个块,那第 1 个块前面没有数据块可依赖,怎么办?
这就需要用到 IV(Initialization Vector,初始向量)。它就像是铁链起点的“第一道挂钩”,也是一个 16 字节的随机数,用来给第一个数据块做初始搅拌。


三、SM4-CBC 有什么用?(实际业务场景)

  1. 金融与政务合规(密评与等保要求):我国的银行系统、数字人民币、政务服务平台、医疗机构等,在系统安全建设中(如“等保三级”),被法律法规强制要求必须采用国密算法(SM4/SM2/SM3)替代 AES/RSA。
  2. 数据库敏感字段加密:用户的身份证号、手机号、支付密码等落库保存时,先在后台用 SM4-CBC 加密成密文再写入数据库,即使数据库被盗,黑客也拿不到明文。
  3. 接口通信安全:APP 或网页端与 Java 后端交互时,将 HTTP 报文用 SM4-CBC 加密传输,防止中途被“中间人”窃听修改。

四、Java 实战:手把手教你实现 SM4-CBC 加解密

由于 Java 标准 JDK 原生并不内置 SM4 算法,我们需要借助 Java 密码学界最权威的开源库 Bouncy Castle (BC)

1. 引入 Maven 依赖

在你的 pom.xml 中引入:

<dependency>
    <groupId>org.bouncycastle</groupId>
    <artifactId>bcprov-jdk18on</artifactId>
    <version>1.78</version>
</dependency>

2. 核心 Java 代码实现

下面的工具类展示了完整的加解密流程:

import org.bouncycastle.jce.provider.BouncyCastleProvider;

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.Security;
import java.util.Base64;

public class Sm4CbcUtil {

    // 算法定义:SM4 算法 / CBC 模式 / PKCS5Padding 填充方式
    private static final String ALGORITHM_NAME = "SM4";
    private static final String CIPHER_ALGORITHM = "SM4/CBC/PKCS5Padding";

    static {
        // 注册 Bouncy Castle 加密提供者
        if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
            Security.addProvider(new BouncyCastleProvider());
        }
    }

    /**
     * SM4-CBC 加密
     * @param plainText 待加密的明文
     * @param key 16字节密钥
     * @param iv 16字节初始向量
     * @return Base64编码后的密文
     */
    public static String encrypt(String plainText, String key, String iv) throws Exception {
        byte[] keyBytes = key.getBytes(StandardCharsets.UTF_8);
        byte[] ivBytes = iv.getBytes(StandardCharsets.UTF_8);

        SecretKeySpec secretKeySpec = new SecretKeySpec(keyBytes, ALGORITHM_NAME);
        IvParameterSpec ivParameterSpec = new IvParameterSpec(ivBytes);

        // 获取 Cipher 实例,并指定使用 BC 库
        Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM, BouncyCastleProvider.PROVIDER_NAME);
        cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec);

        byte[] encryptedBytes = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));
        // 将二进制密文转为 Base64 字符串,方便在网络中传输和存储
        return Base64.getEncoder().encodeToString(encryptedBytes);
    }

    /**
     * SM4-CBC 解密
     * @param cipherTextBase64 Base64编码的密文
     * @param key 16字节密钥
     * @param iv 16字节初始向量
     * @return 解密后的原明文
     */
    public static String decrypt(String cipherTextBase64, String key, String iv) throws Exception {
        byte[] keyBytes = key.getBytes(StandardCharsets.UTF_8);
        byte[] ivBytes = iv.getBytes(StandardCharsets.UTF_8);

        SecretKeySpec secretKeySpec = new SecretKeySpec(keyBytes, ALGORITHM_NAME);
        IvParameterSpec ivParameterSpec = new IvParameterSpec(ivBytes);

        Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM, BouncyCastleProvider.PROVIDER_NAME);
        cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec);

        byte[] encryptedBytes = Base64.getDecoder().decode(cipherTextBase64);
        byte[] decryptedBytes = cipher.doFinal(encryptedBytes);

        return new String(decryptedBytes, StandardCharsets.UTF_8);
    }

    public static void main(String[] args) throws Exception {
        // 注意:密钥Key和初始向量IV必须正好是 16 个字符(16字节 / 128位)
        String key = "1234567890123456"; 
        String iv  = "abcdefghijklmnop"; 
        
        String originalText = "Hello, 这是一条国密SM4加密的敏感数据!";

        // 1. 加密
        String cipherText = encrypt(originalText, key, iv);
        System.out.println("加密后的密文: " + cipherText);

        // 2. 解密
        String decryptedText = decrypt(cipherText, key, iv);
        System.out.println("解密后的明文: " + decryptedText);
    }
}


五、小白避坑指南(开发常犯错误)

  • 钥匙和向量的长度固定:SM4 的 KeyIV 必须正好是 16 字节(16 个字符),多一个字符或少一个字符 Java 都会直接抛出 InvalidKeyException
  • 为什么会有 PKCS5Padding:因为 SM4 要求数据必须按 16 字节切块。如果你的明文只有 10 个字节,最后一块装不满,PKCS5Padding 就会按照固定规则自动补齐剩下的 6 字节,保证加密顺利进行。
  • 密文传输选 Base64/Hex:加密后的结果是乱码二进制,不能直接以普通文本形式保存或传输,必须转成 Base64 字符串或者 Hex(16进制) 格式。
Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐