Spring Boot中使用Bouncy Castle实现SM2国密算法(与前端JS加密交互)

在现代Web应用中,数据安全传输至关重要。SM2作为我国自主设计的非对称加密算法,在安全性、效率和合规性方面具有显著优势。本文将详细介绍如何在Spring Boot中集成SM2算法,实现与前端JS的无缝加密交互。
一、环境准备
技术栈:
- Java 1.8
- Spring Boot 2.1.18
- Bouncy Castle 1.68+
- 前端:sm-crypto或类似库
Maven核心依赖:
xml
<dependencies>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
<version>1.68</version>
</dependency>
</dependencies>
二、核心实现
- 密钥生成服务
java
@RestController
@RequestMapping("/sm2")
public class SM2Controller {
@GetMapping("/keypair")
public Map<String, String> generateKeyPair() throws Exception {
KeyPair keyPair = SM2CryptoUtil.generateKeyPair();
ECPublicKey publicKey = (ECPublicKey) keyPair.getPublic();
ECPrivateKey privateKey = (ECPrivateKey) keyPair.getPrivate();
String publicKeyHex = Hex.toHexString(publicKey.getQ().getEncoded(false));
String privateKeyHex = privateKey.getD().toString(16);
// 标准化私钥格式(64字符)
privateKeyHex = String.format("%64s", privateKeyHex).replace(' ', '0');
return Map.of(
"publicKey", publicKeyHex, // 130字符带04前缀
"privateKey", privateKeyHex // 64字符
);
}
}
- SM2解密服务
java
import org.bouncycastle.asn1.gm.GMNamedCurves;
import org.bouncycastle.asn1.x9.X9ECParameters;
import org.bouncycastle.crypto.digests.SM3Digest;
import org.bouncycastle.crypto.params.ECDomainParameters;
import org.bouncycastle.jce.ECNamedCurveTable;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.jce.spec.ECNamedCurveParameterSpec;
import org.bouncycastle.math.ec.ECPoint;
import org.bouncycastle.util.BigIntegers;
import org.bouncycastle.util.encoders.Hex;
import java.math.BigInteger;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.SecureRandom;
import java.security.Security;
import java.util.Arrays;
/**
* @author cmamg
* @title: Base64Util
* @projectName
* @description: TODO
* @date 2025/7/29
*/
public class SM2CryptoUtil {
// 加密模式常量
public static final int C1C2C3 = 0;
public static final int C1C3C2 = 1;
// 椭圆曲线参数
private static final X9ECParameters EC_PARAMS;
private static final ECDomainParameters DOMAIN_PARAMS;
private static final BigInteger CURVE_ORDER;
static {
Security.addProvider(new BouncyCastleProvider());
EC_PARAMS = GMNamedCurves.getByName("sm2p256v1");
DOMAIN_PARAMS = new ECDomainParameters(
EC_PARAMS.getCurve(),
EC_PARAMS.getG(),
EC_PARAMS.getN(),
EC_PARAMS.getH());
CURVE_ORDER = EC_PARAMS.getN();
}
/**
* 生成SM2密钥对
*/
public static KeyPair generateKeyPair() throws Exception {
ECNamedCurveParameterSpec spec = ECNamedCurveTable.getParameterSpec("sm2p256v1");
KeyPairGenerator kpg = KeyPairGenerator.getInstance("EC", "BC");
kpg.initialize(spec, new SecureRandom());
return kpg.generateKeyPair();
}
/**
* 获取压缩公钥十六进制字符串
*/
public static String getCompressedPublicKey(ECPoint publicKey) {
byte[] compressed = publicKey.getEncoded(true);
return Hex.toHexString(compressed);
}
/**
* 获取未压缩公钥十六进制字符串(不带04前缀)
*/
public static String getUncompressedPublicKey(ECPoint publicKey) {
byte[] uncompressed = publicKey.getEncoded(false);
// 去掉开头的04标识
return Hex.toHexString(uncompressed);
}
/**
* 从十六进制字符串解析公钥
*/
public static ECPoint parsePublicKey(String publicKeyHex) {
// 添加04前缀表示未压缩格式
byte[] pubKeyBytes = Hex.decode( publicKeyHex);
return DOMAIN_PARAMS.getCurve().decodePoint(pubKeyBytes);
}
private static BigInteger parsePrivateKey(String privateKeyHex) {
if (privateKeyHex == null || privateKeyHex.length() != 64) {
throw new IllegalArgumentException("私钥必须是64字符十六进制字符串");
}
try {
BigInteger privateKey = new BigInteger(privateKeyHex, 16);
// 验证私钥范围 [1, n-1]
if (privateKey.signum() <= 0 || privateKey.compareTo(CURVE_ORDER) >= 0) {
throw new IllegalArgumentException("私钥超出有效范围");
}
return privateKey;
} catch (NumberFormatException e) {
throw new IllegalArgumentException("无效的私钥格式", e);
}
}
public static String decryptStr(String ciphertextHex, String privateKeyHex) throws Exception {
return new String(decrypt(ciphertextHex,privateKeyHex, 1), "UTF-8");
}
/**
* SM2解密
*/
public static byte[] decrypt(String ciphertextHex, String privateKeyHex, int cipherMode) throws Exception {
// 1. 验证并解析私钥
BigInteger privateKey = parsePrivateKey(privateKeyHex);
// 2. 解析密文
byte[] ciphertext = Hex.decode(ciphertextHex);
// 验证最小长度 = C1(64) + C3(32) = 96字节
if (ciphertext.length < 96) {
throw new IllegalArgumentException("密文太短");
}
// 3. 拆分密文
byte[] c1 = Arrays.copyOfRange(ciphertext, 0, 64); // 64字节
byte[] c3;
byte[] c2;
if (cipherMode == C1C2C3) {
// C1C2C3模式: C1(64) + C2 + C3(32)
c3 = Arrays.copyOfRange(ciphertext, ciphertext.length - 32, ciphertext.length);
c2 = Arrays.copyOfRange(ciphertext, 64, ciphertext.length - 32);
} else {
// C1C3C2模式: C1(64) + C3(32) + C2
c3 = Arrays.copyOfRange(ciphertext, 64, 96);
c2 = Arrays.copyOfRange(ciphertext, 96, ciphertext.length);
}
// 4. 重建C1点
byte[] c1Full = new byte[65]; // 04 + 64字节
c1Full[0] = 0x04; // 添加未压缩标识
System.arraycopy(c1, 0, c1Full, 1, 64);
ECPoint c1Point;
try {
c1Point = DOMAIN_PARAMS.getCurve().decodePoint(c1Full);
} catch (Exception e) {
throw new IllegalArgumentException("无效的C1点", e);
}
// 5. 计算共享点 (x2, y2) = privateKey * C1
ECPoint s = c1Point.multiply(privateKey).normalize();
// 验证点是否在曲线上
if (!s.isValid()) {
throw new SecurityException("计算出的点不在曲线上");
}
byte[] x2 = BigIntegers.asUnsignedByteArray(32, s.getXCoord().toBigInteger());
byte[] y2 = BigIntegers.asUnsignedByteArray(32, s.getYCoord().toBigInteger());
// 6. KDF生成密钥流
byte[] z = new byte[x2.length + y2.length];
System.arraycopy(x2, 0, z, 0, x2.length);
System.arraycopy(y2, 0, z, x2.length, y2.length);
byte[] t = kdf(z, c2.length);
// 7. 异或解密
byte[] msg = new byte[c2.length];
for (int i = 0; i < c2.length; i++) {
msg[i] = (byte) (c2[i] ^ t[i]);
}
// 8. 验证C3
byte[] u = new byte[x2.length + msg.length + y2.length];
System.arraycopy(x2, 0, u, 0, x2.length);
System.arraycopy(msg, 0, u, x2.length, msg.length);
System.arraycopy(y2, 0, u, x2.length + msg.length, y2.length);
byte[] calculatedC3 = sm3(u);
if (!Arrays.equals(c3, calculatedC3)) {
throw new SecurityException("C3验证失败: 数据可能被篡改或密钥错误");
}
return msg;
}
/**
* KDF密钥派生函数
*/
private static byte[] kdf(byte[] z, int keylen) {
int ct = 1;
int offset = 0;
byte[] result = new byte[keylen];
SM3Digest digest = new SM3Digest();
while (offset < keylen) {
// 准备计数器字节
byte[] ctBytes = new byte[]{
(byte) (ct >>> 24),
(byte) (ct >>> 16),
(byte) (ct >>> 8),
(byte) ct
};
// 计算SM3哈希
digest.update(z, 0, z.length);
digest.update(ctBytes, 0, 4);
byte[] hash = new byte[digest.getDigestSize()];
digest.doFinal(hash, 0);
// 填充结果
int copyLen = Math.min(keylen - offset, hash.length);
System.arraycopy(hash, 0, result, offset, copyLen);
offset += copyLen;
ct++;
digest.reset();
}
return result;
}
/**
* SM3哈希计算
*/
private static byte[] sm3(byte[] input) {
SM3Digest digest = new SM3Digest();
digest.update(input, 0, input.length);
byte[] hash = new byte[digest.getDigestSize()];
digest.doFinal(hash, 0);
return hash;
}
}
- 前端加密示例
javascript
import { sm2 } from 'sm-crypto';
// 使用后端生成的公钥(130字符带04前缀)
const publicKey = '04d4de...';
function encryptMessage(message) {
// 使用C1C3C2模式加密
const ciphertext = sm2.doEncrypt(
message,
publicKey,
1 // cipherMode=1 表示C1C3C2
);
return ciphertext; // 十六进制字符串
}
// 调用示例
const encrypted = encryptMessage('敏感数据123');
三、前后端交互流程
密钥获取:
http
GET /sm2/keypair
Response: { "publicKey": "04...", "privateKey": "a1b2..." }
前端加密:
javascript
const ciphertext = sm2.doEncrypt(data, publicKey, 1);
后端解密:
http
POST /sm2/decrypt
{
"ciphertext": "a1b2c3...",
"privateKey": "a1b2...",
"mode": 1
}
四、关键问题解决方案
- 公钥格式一致性
前端要求公钥带04前缀(未压缩格式),后端需确保:
java
public String getPublicKeyHex(ECPoint publicKey) {
return Hex.toHexString(publicKey.getEncoded(false)); // 带04前缀
}
- 私钥范围验证
防止Scalar not in interval错误:
java
private static final BigInteger CURVE_ORDER = EC_PARAMS.getN();
if (privateKey.signum() <= 0 ||
privateKey.compareTo(CURVE_ORDER) >= 0) {
throw new IllegalArgumentException("无效私钥范围");
}
- C1点重建
前端密文中的C1点不带04前缀,后端需重建:
java
byte[] c1Full = new byte[65];
c1Full[0] = 0x04; // 添加前缀
System.arraycopy(c1, 0, c1Full, 1, 64);
五、常见问题排查
错误现象 可能原因 解决方案
Scalar not in interval 私钥格式错误或越界 验证私钥长度64字符,值在[1, n-1]范围内
C3验证失败 密钥错误或数据篡改 检查公私钥配对,重试加密流程
无效的C1点 密文格式错误 确认使用C1C3C2模式,检查密文长度
解密乱码 编码不一致 统一使用UTF-8编码
六、最佳实践建议
密钥管理:
前端不存储私钥
后端使用HSM或KMS管理私钥
定期轮换密钥
性能优化:
java
// 重用SM3Digest实例
private static final ThreadLocal<SM3Digest> sm3Cache =
ThreadLocal.withInitial(SM3Digest::new);
安全增强:
java
// 防止时序攻击
if (!MessageDigest.isEqual(c3, calculatedC3)) {
throw new SecurityException("C3验证失败");
}
七、总结
本文实现了Spring Boot中完整的SM2算法集成方案,重点解决了:
密钥生成与格式标准化
与前端JS的加密交互
解密过程中的异常处理
通过此方案,开发者可以快速构建符合国密标准的安全应用,确保数据传输的机密性和完整性。在实际业务中,建议结合HTTPS等传输层安全措施,构建纵深防御体系。