SM4 国密算法企业级实战:加密解密、数据脱敏与合规存储完整方案
SM4国密算法在企业应用中的实践
引言
随着《密码法》和《数据安全法》的实施,国内金融、政务、医疗等行业对数据加密提出了明确的合规要求——必须使用国家密码管理局认定的国密算法。SM4作为我国自主研发的分组密码算法,是对称加密的国密标准,在国际上与AES具有同等安全强度。
本文将介绍SM4算法的基本原理,与AES的对比分析,以及在密码加密、数据脱敏等企业场景中的实践方案。
一、SM4算法概述
1.1 算法基本信息
| 属性 | SM4 | AES |
|---|---|---|
| 发布时间 | 2006年 | 2001年 |
| 发布机构 | 国家密码管理局 | NIST(美国) |
| 算法类型 | 分组密码 | 分组密码 |
| 分组长度 | 128位 | 128位 |
| 密钥长度 | 128位 | 128/192/256位 |
| 加密轮数 | 32轮 | 10/12/14轮 |
| 安全强度 | 128位 | 128/192/256位 |
1.2 SM4加密流程
flowchart LR
A[128位明文] --> B[初始变换]
B --> C[32轮迭代变换]
C --> D[反序变换]
D --> E[128位密文]
subgraph 每轮变换
F[输入4个32位字] --> G[轮函数F]
G --> H[S盒非线性变换]
H --> I[线性变换L]
I --> J[轮密钥异或]
J --> K[输出4个32位字]
end
C --> F
style E fill:#f9d5d5
style A fill:#d5f9d5
SM4的核心是32轮非线性迭代变换,每轮使用不同的轮密钥。轮密钥由加密密钥通过密钥扩展算法生成,共32个轮密钥。
1.3 工作模式
SM4支持多种工作模式,不同模式适用于不同场景:
| 模式 | 特点 | 适用场景 |
|---|---|---|
| ECB | 每块独立加密,相同明文→相同密文 | 不推荐(不安全) |
| CBC | 链式加密,需要IV | 通用加密 |
| CTR | 计数器模式,可并行加密 | 高性能场景 |
| GCM | 认证加密,提供完整性校验 | 推荐模式 |
二、Java实现SM4加密
2.1 使用BouncyCastle库
BouncyCastle是Java生态中最成熟的安全库,完整支持SM4算法:
<!-- Maven依赖 -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk18on</artifactId>
<version>1.78.1</version>
</dependency>
2.2 SM4工具类
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.SecureRandom;
import java.security.Security;
import java.util.Base64;
/**
* SM4加密工具类
* 支持CBC和GCM两种工作模式
*/
public class Sm4Util {
static {
// 注册BouncyCastle安全提供者
Security.addProvider(new BouncyCastleProvider());
}
/** 算法名称 */
private static final String ALGORITHM = "SM4";
/** CBC模式 */
private static final String CBC_MODE = "SM4/CBC/PKCS7Padding";
/** GCM模式(推荐) */
private static final String GCM_MODE = "SM4/GCM/NoPadding";
/** IV长度(16字节 = 128位) */
private static final int IV_LENGTH = 16;
/** GCM认证标签长度(128位) */
private static final int GCM_TAG_LENGTH = 128;
/**
* 生成SM4密钥
* @return Base64编码的密钥
*/
public static String generateKey() {
SecureRandom random = new SecureRandom();
byte[] key = new byte[16]; // SM4密钥固定16字节
random.nextBytes(key);
return Base64.getEncoder().encodeToString(key);
}
/**
* 生成随机IV
* @return IV字节数组
*/
private static byte[] generateIv() {
SecureRandom random = new SecureRandom();
byte[] iv = new byte[IV_LENGTH];
random.nextBytes(iv);
return iv;
}
/**
* SM4-CBC加密
* @param plaintext 明文
* @param keyBase64 Base64编码的密钥
* @return Base64编码的密文(前16字节为IV)
*/
public static String encryptCbc(String plaintext, String keyBase64) {
try {
byte[] key = Base64.getDecoder().decode(keyBase64);
byte[] iv = generateIv();
byte[] data = plaintext.getBytes(StandardCharsets.UTF_8);
Cipher cipher = Cipher.getInstance(CBC_MODE, BouncyCastleProvider.PROVIDER_NAME);
cipher.init(Cipher.ENCRYPT_MODE,
new SecretKeySpec(key, ALGORITHM),
new IvParameterSpec(iv));
byte[] encrypted = cipher.doFinal(data);
// 将IV拼接在密文前面(解密时需要)
byte[] result = new byte[iv.length + encrypted.length];
System.arraycopy(iv, 0, result, 0, iv.length);
System.arraycopy(encrypted, 0, result, iv.length, encrypted.length);
return Base64.getEncoder().encodeToString(result);
} catch (Exception e) {
throw new RuntimeException("SM4加密失败", e);
}
}
/**
* SM4-CBC解密
* @param ciphertextBase64 Base64编码的密文(前16字节为IV)
* @param keyBase64 Base64编码的密钥
* @return 明文
*/
public static String decryptCbc(String ciphertextBase64, String keyBase64) {
try {
byte[] key = Base64.getDecoder().decode(keyBase64);
byte[] result = Base64.getDecoder().decode(ciphertextBase64);
// 提取IV(前16字节)
byte[] iv = new byte[IV_LENGTH];
System.arraycopy(result, 0, iv, 0, IV_LENGTH);
// 提取密文
byte[] encrypted = new byte[result.length - IV_LENGTH];
System.arraycopy(result, IV_LENGTH, encrypted, 0, encrypted.length);
Cipher cipher = Cipher.getInstance(CBC_MODE, BouncyCastleProvider.PROVIDER_NAME);
cipher.init(Cipher.DECRYPT_MODE,
new SecretKeySpec(key, ALGORITHM),
new IvParameterSpec(iv));
byte[] decrypted = cipher.doFinal(encrypted);
return new String(decrypted, StandardCharsets.UTF_8);
} catch (Exception e) {
throw new RuntimeException("SM4解密失败", e);
}
}
}
2.3 使用示例
public class Sm4Example {
public static void main(String[] args) {
// 1. 生成密钥(通常在系统初始化时生成一次,存储在安全配置中)
String key = Sm4Util.generateKey();
System.out.println("密钥: " + key);
// 2. 加密
String plaintext = "这是需要加密的敏感数据";
String ciphertext = Sm4Util.encryptCbc(plaintext, key);
System.out.println("密文: " + ciphertext);
// 3. 解密
String decrypted = Sm4Util.decryptCbc(ciphertext, key);
System.out.println("明文: " + decrypted);
// 验证
assert plaintext.equals(decrypted);
}
}
三、密码加密场景
3.1 密码存储的正确姿势
重要原则:密码永远不应使用对称加密存储,而应使用不可逆的哈希算法。SM4用于密码加密的场景是可逆加密(如需要还原明文的场景),而非密码存储。
密码存储推荐使用SM3(国密哈希算法)或bcrypt:
/**
* 密码加密服务
* 使用SM3+盐值进行密码哈希
*/
@Component
public class PasswordEncoder {
/** 盐值长度 */
private static final int SALT_LENGTH = 32;
/** 迭代次数 */
private static final int ITERATIONS = 1024;
/**
* 加密密码
* @param rawPassword 原始密码
* @return 加密后的密码(格式:盐值$迭代次数$哈希值)
*/
public String encode(String rawPassword) {
// 生成随机盐值
SecureRandom random = new SecureRandom();
byte[] salt = new byte[SALT_LENGTH];
random.nextBytes(salt);
String saltHex = Hex.toHexString(salt);
// SM3哈希 + 迭代
byte[] hash = rawPassword.getBytes(StandardCharsets.UTF_8);
for (int i = 0; i < ITERATIONS; i++) {
hash = sm3Hash(saltHex.getBytes(StandardCharsets.UTF_8), hash);
}
return saltHex + "$" + ITERATIONS + "$" + Hex.toHexString(hash);
}
/**
* 验证密码
* @param rawPassword 原始密码
* @param encodedPassword 加密后的密码
* @return 是否匹配
*/
public boolean matches(String rawPassword, String encodedPassword) {
String[] parts = encodedPassword.split("\$");
String salt = parts[0];
int iterations = Integer.parseInt(parts[1]);
byte[] hash = rawPassword.getBytes(StandardCharsets.UTF_8);
for (int i = 0; i < iterations; i++) {
hash = sm3Hash(salt.getBytes(StandardCharsets.UTF_8), hash);
}
return Hex.toHexString(hash).equals(parts[2]);
}
/**
* SM3哈希计算
*/
private byte[] sm3Hash(byte[]... datas) {
SM3Digest digest = new SM3Digest();
for (byte[] data : datas) {
digest.update(data, 0, data.length);
}
byte[] result = new byte[digest.getDigestSize()];
digest.doFinal(result, 0);
return result;
}
}
四、数据脱敏场景
4.1 可逆脱敏
某些场景需要在展示时脱敏,但在业务处理时需要还原原始数据。SM4加密是理想的方案:
/**
* 数据脱敏服务
* 支持可逆脱敏(SM4加密)和不可逆脱敏(掩码)
*/
@Component
public class DataMaskingService {
@Autowired
private Sm4Config sm4Config;
/**
* 手机号脱敏(可逆)
* 存储:SM4加密后的密文
* 展示:138****1234
*/
public String maskPhone(String phone) {
// 存储时加密
return Sm4Util.encryptCbc(phone, sm4Config.getKey());
}
/**
* 手机号展示脱敏(不可逆掩码)
* 13812341234 → 138****1234
*/
public String displayPhone(String phone) {
if (phone == null || phone.length() < 7) {
return phone;
}
return phone.substring(0, 3) + "****" + phone.substring(phone.length() - 4);
}
/**
* 身份证号展示脱敏
* 110101199001011234 → 110101****1234
*/
public String displayIdCard(String idCard) {
if (idCard == null || idCard.length() < 8) {
return idCard;
}
return idCard.substring(0, 6) + "********" + idCard.substring(idCard.length() - 4);
}
/**
* 邮箱展示脱敏
* example@domain.com → e****e@domain.com
*/
public String displayEmail(String email) {
if (email == null || !email.contains("@")) {
return email;
}
String[] parts = email.split("@");
String name = parts[0];
if (name.length() <= 2) {
return name.charAt(0) + "****@" + parts[1];
}
return name.charAt(0) + "****" + name.charAt(name.length() - 1) + "@" + parts[1];
}
/**
* 解密还原
*/
public String unmask(String maskedData) {
return Sm4Util.decryptCbc(maskedData, sm4Config.getKey());
}
}
4.2 MyBatis-Plus类型处理器
通过MyBatis-Plus的类型处理器,实现数据库字段的透明加解密:
/**
* SM4加密类型处理器
* 写入数据库时自动加密,读取时自动解密
*/
@MappedTypes(String.class)
public class Sm4EncryptTypeHandler extends BaseTypeHandler<String> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i,
String parameter, JdbcType jdbcType) throws SQLException {
// 写入时加密
ps.setString(i, Sm4Util.encryptCbc(parameter, getSm4Key()));
}
@Override
public String getNullableResult(ResultSet rs, String columnName) throws SQLException {
// 读取时解密
String value = rs.getString(columnName);
return value != null ? Sm4Util.decryptCbc(value, getSm4Key()) : null;
}
@Override
public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
String value = rs.getString(columnIndex);
return value != null ? Sm4Util.decryptCbc(value, getSm4Key()) : null;
}
@Override
public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
String value = cs.getString(columnIndex);
return value != null ? Sm4Util.decryptCbc(value, getSm4Key()) : null;
}
private String getSm4Key() {
// 从安全配置中获取密钥
return SpringUtils.getBean(Sm4Config.class).getKey();
}
}
五、密钥管理
5.1 密钥管理架构
flowchart TB
A[应用启动] --> B{密钥来源}
B -->|开发环境| C[配置文件]
B -->|生产环境| D[密钥管理服务KMS]
C --> E[加载密钥到内存]
D --> F[从KMS获取密钥]
F --> G[密钥缓存到Redis<br/>TTL=1小时]
G --> E
E --> H[应用使用密钥]
subgraph 密钥轮换
I[定期轮换密钥] --> J[生成新密钥]
J --> K[双密钥并行期<br/>新旧密钥均可解密]
K --> L[数据重新加密]
L --> M[切换到新密钥]
end
style D fill:#d5f9d5
style H fill:#e8f4f8
5.2 密钥安全要求
- 密钥不硬编码:密钥存储在环境变量或KMS中,不写入代码
- 传输加密:密钥在传输过程中使用TLS加密
- 定期轮换:建议每90天轮换一次密钥
- 访问控制:密钥访问需要最小权限原则
- 审计日志:记录所有密钥访问和操作
六、SM4 vs AES对比总结
| 维度 | SM4 | AES |
|---|---|---|
| 安全强度 | 128位(等效AES-128) | 128/192/256位 |
| 合规性 | 符合国密标准,国内合规 | 国际标准,海外合规 |
| 性能 | 略慢于AES(约5-10%) | 硬件加速支持好 |
| 生态支持 | BouncyCastle支持 | JDK原生支持 |
| 国际认可 | ISO/IEC 18033-3标准 | 全球通用标准 |
结论与建议
核心要点
- SM4是国密合规的必选项:面向国内金融、政务、医疗行业的系统必须支持SM4
- 密码存储用SM3/bcrypt,不用SM4:密码存储必须使用不可逆哈希
- 数据脱敏用SM4:需要还原的敏感数据使用SM4可逆加密
- 推荐GCM模式:提供加密+完整性校验,比CBC更安全
实践建议
- 密钥管理:生产环境密钥必须通过KMS管理,禁止硬编码
- 双算法支持:同时支持SM4和AES,满足国内外不同合规要求
- 性能测试:SM4性能略低于AES,高并发场景需做压测
- 密钥轮换:建立密钥轮换机制,支持双密钥并行过渡
- 审计合规:记录所有加解密操作,满足等保审计要求