通俗易懂!3分钟彻底看懂国密 SM4-CBC 加密算法(含 Java 实战代码)
·
通俗易懂!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 有什么用?(实际业务场景)
- 金融与政务合规(密评与等保要求):我国的银行系统、数字人民币、政务服务平台、医疗机构等,在系统安全建设中(如“等保三级”),被法律法规强制要求必须采用国密算法(SM4/SM2/SM3)替代 AES/RSA。
- 数据库敏感字段加密:用户的身份证号、手机号、支付密码等落库保存时,先在后台用 SM4-CBC 加密成密文再写入数据库,即使数据库被盗,黑客也拿不到明文。
- 接口通信安全: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 的
Key和IV必须正好是 16 字节(16 个字符),多一个字符或少一个字符 Java 都会直接抛出InvalidKeyException。 - 为什么会有
PKCS5Padding?:因为 SM4 要求数据必须按 16 字节切块。如果你的明文只有 10 个字节,最后一块装不满,PKCS5Padding就会按照固定规则自动补齐剩下的 6 字节,保证加密顺利进行。 - 密文传输选 Base64/Hex:加密后的结果是乱码二进制,不能直接以普通文本形式保存或传输,必须转成 Base64 字符串或者 Hex(16进制) 格式。
更多推荐
所有评论(0)