14-字段级SM4加密与SM2签名
EA03字段元数据里有encrypted和signed两个布尔------身份证号加密存库、关键业务数据加签名防篡改。这篇拆两个策略类的实现:BouncyCastle的SM4/CBC/PKCS7、SM3withSM2签名算法、hex密钥装载,以及它们在引擎里的触发点。
文章目录
- 14-字段级SM4加密与SM2签名
-
- 一、两个SPI接口
- 二、Sm4EncryptStrategy:82行实现
-
- [2.1 静态块注册Provider](#2.1 静态块注册Provider)
- [2.2 构造与密钥](#2.2 构造与密钥)
- [2.3 encrypt的三步](#2.3 encrypt的三步)
- [2.4 hexToBytes](#2.4 hexToBytes)
- 三、Sm2SignStrategy:71行实现
-
- [3.1 密钥装载](#3.1 密钥装载)
- [3.2 sign与verify](#3.2 sign与verify)
- 四、引擎里的触发点
- 五、加密与签名的分工
源码:
browise-metadata/src/main/java/com/browise/ea/core/crypto/Sm4EncryptStrategy.java(82行)
browise-metadata/src/main/java/com/browise/ea/core/crypto/Sm2SignStrategy.java(71行)
一、两个SPI接口
引擎层只认接口不认实现:
java
// EncryptStrategy.java(187B)
public interface EncryptStrategy {
String encrypt(String plaintext) throws Exception;
String decrypt(String ciphertext) throws Exception;
}
// SignStrategy.java(188B)
public interface SignStrategy {
String sign(String data) throws Exception;
boolean verify(String data, String signature) throws Exception;
}
EaConfig.customEncryptStrategy()可注入自定义实现------政务客户要求"必须走加密机"时,实现一对接口注册进去,引擎无感知切换(第32篇的starter条件装配就是干这个的)。
二、Sm4EncryptStrategy:82行实现
2.1 静态块注册Provider
java
static {
if (Security.getProvider(PROVIDER) == null) {
Security.addProvider(new BouncyCastleProvider());
}
}
JDK不带SM4------必须BouncyCastle。静态块在类加载时注册一次,if null判断防止重复注册(多模块都加载BC时不会冲突)。
2.2 构造与密钥
java
public Sm4EncryptStrategy(String key, String iv, String mode) {
this.key = key; // hex编码的密钥字符串
this.iv = iv; // CBC模式的初始向量(hex)
this.mode = mode != null ? mode.toUpperCase() : "CBC";
}
配置来源(application.yml):
yaml
browise:
metadata:
encrypt-key: "0123456789abcdef0123456789abcdef" # 32个hex字符=16字节=SM4密钥长度
encrypt-iv: "fedcba9876543210fedcba9876543210"
encrypt-mode: "CBC" # 或ECB
2.3 encrypt的三步
java
public String encrypt(String plaintext) throws Exception {
Cipher cipher = Cipher.getInstance("SM4/" + mode + "/PKCS7Padding", PROVIDER);
// SM4/CBC/PKCS7Padding------算法/模式/填充三段式
SecretKeySpec keySpec = new SecretKeySpec(hexToBytes(key), ALGORITHM);
if ("CBC".equals(mode) && iv != null) {
IvParameterSpec ivSpec = new IvParameterSpec(hexToBytes(iv));
cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec); // CBC带IV
} else {
cipher.init(Cipher.ENCRYPT_MODE, keySpec); // ECB不带IV
}
byte[] encrypted = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(encrypted); // 密文Base64存储
}
三个技术点:
PKCS7Padding------SM4是块加密(16字节一块),明文不够一块要填充。PKCS7是国密标准推荐填充(JDK自带的是PKCS5,BC的PKCS7对SM4块长兼容)。
CBC vs ECB ------CBC(密码块链接)每块加密前先异或前一块密文+IV,相同明文加密出不同密文(身份证号重复也不暴露);ECB相同明文块加密结果相同(模式图风险)。默认CBC,ECB只在不方便管IV的极简场景用。
Base64输出------密文是二进制,数据库列是VARCHAR------Base64编码成字符串落库。解密先Base64 decode再doFinal。
2.4 hexToBytes
java
private static byte[] hexToBytes(String hex) {
byte[] data = new byte[hex.length() / 2];
for (int i = 0; i < hex.length(); i += 2) {
data[i / 2] = (byte) ((Character.digit(hex.charAt(i), 16) << 4)
+ Character.digit(hex.charAt(i + 1), 16));
}
return data;
}
配置文件里密钥用hex字符串(可打印、可校验长度),运行时转字节数组。16字节密钥=32个hex字符------配置错了长度,SecretKeySpec不报错但加密结果错------这是SM4排障的第一检查点。
三、Sm2SignStrategy:71行实现
3.1 密钥装载
java
private static final String SIGN_ALGORITHM = "SM3withSM2"; // 国密标准签名算法
public Sm2SignStrategy(String privateKeyHex, String publicKeyHex) {
KeyFactory kf = KeyFactory.getInstance("EC", "BC");
this.privateKey = kf.generatePrivate(new PKCS8EncodedKeySpec(hexToBytes(privateKeyHex)));
this.publicKey = kf.generatePublic(new X509EncodedKeySpec(hexToBytes(publicKeyHex)));
}
SM3withSM2 不是裸SM2------是"用SM3做摘要、SM2椭圆曲线做签名"的组合算法(对标RSAwithSHA256的组合方式)。密钥格式:私钥PKCS8、公钥X509------标准的非对称密钥编码,gm工具或BC的KeyPairGenerator可生成。
3.2 sign与verify
java
public String sign(String data) throws Exception {
Signature sig = Signature.getInstance(SIGN_ALGORITHM, "BC");
sig.initSign(privateKey);
sig.update(data.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(sig.sign()); // 签名值Base64
}
public boolean verify(String data, String signature) throws Exception {
Signature sig = Signature.getInstance(SIGN_ALGORITHM, "BC");
sig.initVerify(publicKey);
sig.update(data.getBytes(StandardCharsets.UTF_8));
return sig.verify(Base64.getDecoder().decode(signature));
}
对称的典型JCA用法------签名私钥、验签公钥。私钥只在签名方------验证方拿公钥就能验,不触碰私钥。
四、引擎里的触发点
查询侧:postProcess逐字段解密
EaEngine.executeQuery → resultSetToList → postProcess(row, fields)(第06篇讲过):
java
private void postProcess(Map<String, Object> row, List<EaFieldMeta> fields) {
for (EaFieldMeta f : fields) {
String key = f.getFieldAlias().toLowerCase();
Object val = row.get(key);
if (val == null) continue;
String strVal = val.toString();
if (f.isEncrypted() && encryptStrategy != null) {
strVal = encryptStrategy.decrypt(strVal); // ①先解密
row.put(key, strVal);
}
if (f.isMasked()) {
row.put(key, mask(strVal, f.getMaskRule())); // ②再脱敏
}
}
}
①②顺序不可反 ------密文脱敏没有意义,脱敏后的星号解不开。库里的Base64密文 → 内存里解密成明文 → 输出前脱敏成 3201**1234。
写入侧:EaController的save路径
browise-formbuilder/.../EaController.java 的save链路在执行insert/update前,按fields元数据把encrypted字段的明文加密------前端传明文,库里存密文,查询吐明文(或脱敏)。加密对业务代码完全透明。
策略初始化的null语义
java
// EaEngine.initEncryptStrategy()
EncryptStrategy custom = config.customEncryptStrategy();
if (custom != null) { this.encryptStrategy = custom; return; }
if (config.getEncryptKey() != null) {
this.encryptStrategy = new Sm4EncryptStrategy(...);
}
// 不配key → encryptStrategy保持null → postProcess跳过解密
没配密钥=加密功能关闭 。字段标了encrypted=1但引擎没有策略------查出来的还是库里的密文原样(不报错)。这是容错也是坑:上线前要验证配置真生效(用一条加密数据查一遍确认吐的是明文)。
五、加密与签名的分工
| SM4加密 | SM2签名 | |
|---|---|---|
| 解决什么 | 看不到(保密性) | 改不了(完整性) |
| 密钥 | 对称------加解密同一把 | 非对称------私签公验 |
| 数据库存储 | 密文(Base64) | 明文+MAC列(Base64签名值) |
| 查询输出 | 解密→(脱敏) | 验签校验 |
| 典型字段 | 身份证号/银行卡号 | 待遇金额/审批结论 |
为什么待遇金额不加密只签名------金额要参与SQL聚合(SUM/ORDER),加密了没法算;但绝不能被悄悄篡改------签名保证"这行数据的金额从写入起没被人改过"。保密性和完整性是两个独立需求,各用各的算法。
✅ 亮点:两个82/71行的策略类完整拆解------SM4/CBC/PKCS7Padding三段式、hex密钥与Base64密文的编码链、SM3withSM2组合算法、postProcess先解密再脱敏的顺序约束、加密与签名解决保密vs完整性的分工。适合做国密落地的人。扩展方向:第15篇mask脱敏规则、第32篇starter的BC/HSM条件装配。