国密算法SM2与SM3在Java项目中的实践指南
在信息安全领域,密码算法是保障数据安全的核心基石。随着国家对信息安全自主可控要求的不断提升,国密算法(即国家商用密码算法)的应用越来越广泛。其中,SM2椭圆曲线公钥密码算法和SM3密码杂凑算法作为国密体系中的核心算法,分别用于数据加密签名和数据完整性校验。本文将结合Java项目实战,详细讲解SM2和SM3的原理概要及具体使用方法,帮助开发者快速上手国密算法集成。
一、国密算法基础认知:SM2与SM3是什么?
在进行代码实现前,我们先简单梳理下SM2和SM3的核心定位与区别,避免在实际使用中混淆场景。
1.1 SM2:椭圆曲线公钥密码算法
SM2是由我国自主设计的椭圆曲线公钥密码算法,其安全性基于椭圆曲线离散对数问题(ECDLP),相较于国际通用的RSA算法,在相同安全强度下,SM2的密钥长度更短(推荐256位),计算效率更高,更适合移动设备、物联网等资源受限场景。
SM2的核心应用场景包括:数据加密/解密 、数字签名/验签。与RSA不同的是,SM2在签名过程中会加入用户的身份信息,进一步提升了签名的安全性和抗攻击能力。
1.2 SM3:密码杂凑算法
SM3是一种密码散列函数,类似于国际通用的SHA-256,其作用是将任意长度的输入数据(明文)转换为固定长度(256位)的输出数据(哈希值/摘要)。SM3具有抗碰撞性、抗原像性等特性,即无法通过哈希值反推明文,也难以找到两个不同明文对应相同的哈希值。
SM3的核心应用场景包括:数据完整性校验 、密码加密存储 (如用户密码加盐哈希后存储)、数字签名的辅助(对明文先做SM3哈希,再用SM2签名哈希值,提升效率)。
二、Java项目集成国密算法的前提:环境搭建
Java原生的JCE(Java Cryptography Extension)并不直接支持SM2和SM3算法,因此需要借助第三方开源库来实现。目前主流的国密算法Java实现库是BouncyCastle(一个开源的密码学库,支持多种国际和国家密码算法)。
2.1 引入依赖(Maven)
在Java项目的pom.xml文件中引入BouncyCastle的依赖,推荐使用最新稳定版本:
xml
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15to18</artifactId>
<version>1.78.1</version>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk15to18</artifactId>
<version>1.78.1</version>
</dependency>
说明:bcprov-jdk15to18是基础密码算法库,包含SM2、SM3的核心实现;bcpkix-jdk15to18是扩展库,提供了更便捷的密钥管理和签名加密封装。
2.2 注册BouncyCastle安全提供者
Java中使用密码算法需要通过"安全提供者"(Security Provider)机制加载,因此需要在使用SM2和SM3前,将BouncyCastle注册为安全提供者。有两种注册方式:
方式一:代码动态注册(推荐)
在项目启动时或算法使用前执行以下代码,无需修改系统配置,灵活性高:
java
import java.security.Security;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
// 注册BouncyCastle提供者
public class SMUtil {
static {
// 避免重复注册
if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
Security.addProvider(new BouncyCastleProvider());
}
}
}
方式二:系统级注册(不推荐)
修改JRE安装目录下的lib/security/java.security文件,添加一行:security.provider.10=org.bouncycastle.jce.provider.BouncyCastleProvider(数字10需根据现有提供者顺序调整)。该方式影响所有使用该JRE的项目,灵活性差,不推荐生产环境使用。
三、SM3算法在Java中的实现:数据哈希
SM3的使用相对简单,核心是通过BouncyCastle提供的算法名称"SM3"获取消息摘要器,然后对输入数据进行哈希计算。以下是完整的工具类实现,包含普通哈希和加盐哈希两种场景(加盐哈希更适合密码存储,可防止彩虹表攻击)。
3.1 SM3工具类实现
java
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.Security;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
/**
* SM3哈希算法工具类
*/
public class SM3Util {
// 算法名称
private static final String ALGORITHM_NAME = "SM3";
// 编码格式
private static final String CHARSET = "UTF-8";
// 哈希结果长度(256位,转换为16进制后为64位)
private static final int HASH_LENGTH = 64;
static {
// 注册BouncyCastle提供者
if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
Security.addProvider(new BouncyCastleProvider());
}
}
/**
* 普通SM3哈希(无盐)
* @param content 待哈希的明文
* @return 16进制格式的哈希值
* @throws NoSuchAlgorithmException 算法不存在异常
*/
public static String sm3Hash(String content) throws NoSuchAlgorithmException {
return sm3HashWithSalt(content, null);
}
/**
* SM3加盐哈希(推荐密码存储使用)
* @param content 待哈希的明文
* @param salt 盐值(可为null,为null时等同于普通哈希)
* @return 16进制格式的哈希值
* @throws NoSuchAlgorithmException 算法不存在异常
*/
public static String sm3HashWithSalt(String content, String salt) throws NoSuchAlgorithmException {
// 若有盐值,将盐值拼接到明文前(也可拼接到后面,需统一规则)
String contentWithSalt = content + (salt != null ? salt : "");
// 获取SM3消息摘要器
MessageDigest messageDigest = MessageDigest.getInstance(ALGORITHM_NAME, BouncyCastleProvider.PROVIDER_NAME);
// 计算哈希值(字节数组)
byte[] hashBytes = messageDigest.digest(contentWithSalt.getBytes());
// 转换为16进制字符串
return bytesToHex(hashBytes);
}
/**
* 字节数组转换为16进制字符串
* @param bytes 字节数组
* @return 16进制字符串
*/
private static String bytesToHex(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
String hex = Integer.toHexString(b & 0xFF);
if (hex.length() == 1) {
sb.append("0");
}
sb.append(hex);
}
return sb.toString();
}
// 测试方法
public static void main(String[] args) throws NoSuchAlgorithmException {
String content = "测试SM3哈希";
String salt = "random_salt_123"; // 实际使用中建议随机生成盐值,每个用户单独存储
// 普通哈希
String normalHash = sm3Hash(content);
System.out.println("SM3普通哈希结果:" + normalHash);
System.out.println("哈希结果长度:" + normalHash.length()); // 应输出64
// 加盐哈希
String saltHash = sm3HashWithSalt(content, salt);
System.out.println("SM3加盐哈希结果:" + saltHash);
}
}
3.2 关键说明
-
盐值使用:密码存储时,务必使用加盐哈希,盐值建议随机生成(如16位随机字符串),并与哈希结果一同存储(每个用户单独一个盐值)。
-
算法名称:BouncyCastle中SM3的算法名称为"SM3",获取MessageDigest时需指定提供者为BouncyCastleProvider.PROVIDER_NAME(即"BC")。
-
哈希长度:SM3输出为256位哈希值,转换为16进制字符串后长度为64位,可通过此特性验证哈希结果是否正确。
四、SM2算法在Java中的实现:加密解密与签名验签
SM2的使用场景比SM3更复杂,核心包括"密钥对生成""加密解密""签名验签"三个环节。以下将基于BouncyCastle实现完整的工具类,并讲解各环节的关键注意事项。
4.1 SM2工具类实现(核心代码)
java
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.Security;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import org.bouncycastle.asn1.gm.GMNamedCurves;
import org.bouncycastle.asn1.x9.X9ECParameters;
import org.bouncycastle.crypto.engines.SM2Engine;
import org.bouncycastle.crypto.params.ECDomainParameters;
import org.bouncycastle.crypto.params.ECKeyParameters;
import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
import org.bouncycastle.crypto.params.ParametersWithRandom;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.jce.spec.ECParameterSpec;
import org.bouncycastle.util.encoders.Hex;
/**
* SM2椭圆曲线算法工具类(加密解密、签名验签)
*/
public class SM2Util {
// 算法名称
private static final String ALGORITHM_NAME = "SM2";
// 编码格式
private static final String CHARSET = "UTF-8";
// SM2推荐曲线参数(国密标准曲线:sm2p256v1)
private static final X9ECParameters X9_EC_PARAMETERS = GMNamedCurves.getByName("sm2p256v1");
private static final ECParameterSpec EC_PARAMETER_SPEC = new ECParameterSpec(
X9_EC_PARAMETERS.getCurve(),
X9_EC_PARAMETERS.getG(),
X9_EC_PARAMETERS.getN(),
X9_EC_PARAMETERS.getH()
);
private static final ECDomainParameters EC_DOMAIN_PARAMETERS = new ECDomainParameters(
X9_EC_PARAMETERS.getCurve(),
X9_EC_PARAMETERS.getG(),
X9_EC_PARAMETERS.getN(),
X9_EC_PARAMETERS.getH()
);
static {
// 注册BouncyCastle提供者
if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
Security.addProvider(new BouncyCastleProvider());
}
}
/**
* 生成SM2密钥对(公钥+私钥)
* @return 密钥对(包含公钥和私钥的字节数组)
* @throws Exception 异常
*/
public static KeyPairVO generateKeyPair() throws Exception {
// 获取SM2密钥对生成器
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(ALGORITHM_NAME, BouncyCastleProvider.PROVIDER_NAME);
// 初始化密钥对生成器(指定曲线参数和随机数)
keyPairGenerator.initialize(EC_PARAMETER_SPEC, new SecureRandom());
// 生成密钥对
KeyPair keyPair = keyPairGenerator.generateKeyPair();
// 提取公钥和私钥(字节数组格式)
byte[] publicKeyBytes = keyPair.getPublic().getEncoded();
byte[] privateKeyBytes = keyPair.getPrivate().getEncoded();
// 封装为VO返回(16进制格式,便于存储和传输)
return new KeyPairVO(
Hex.toHexString(publicKeyBytes),
Hex.toHexString(privateKeyBytes)
);
}
/**
* SM2加密(公钥加密)
* @param publicKeyHex 16进制格式的公钥
* @param content 待加密的明文
* @return 16进制格式的密文
* @throws Exception 异常
*/
public static String encrypt(String publicKeyHex, String content) throws Exception {
// 1. 解析公钥(从16进制字符串转换为PublicKey对象)
byte[] publicKeyBytes = Hex.decode(publicKeyHex);
X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(publicKeyBytes);
PublicKey publicKey = Security.getInstance(ALGORITHM_NAME, BouncyCastleProvider.PROVIDER_NAME)
.generatePublic(x509EncodedKeySpec);
ECPublicKeyParameters ecPublicKeyParameters = (ECPublicKeyParameters) ECKeyParameters.getInstance(publicKey);
// 2. 初始化SM2加密引擎(使用C1C3C2格式,国密标准推荐)
SM2Engine sm2Engine = new SM2Engine(SM2Engine.Mode.C1C3C2);
sm2Engine.init(true, new ParametersWithRandom(ecPublicKeyParameters, new SecureRandom()));
// 3. 执行加密并返回结果(字节数组转换为16进制字符串)
byte[] contentBytes = content.getBytes(CHARSET);
byte[] encryptedBytes = sm2Engine.processBlock(contentBytes, 0, contentBytes.length);
return Hex.toHexString(encryptedBytes);
}
/**
* SM2解密(私钥解密)
* @param privateKeyHex 16进制格式的私钥
* @param encryptedContentHex 16进制格式的密文
* @return 解密后的明文
* @throws Exception 异常
*/
public static String decrypt(String privateKeyHex, String encryptedContentHex) throws Exception {
// 1. 解析私钥(从16进制字符串转换为PrivateKey对象)
byte[] privateKeyBytes = Hex.decode(privateKeyHex);
PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(privateKeyBytes);
PrivateKey privateKey = Security.getInstance(ALGORITHM_NAME, BouncyCastleProvider.PROVIDER_NAME)
.generatePrivate(pkcs8EncodedKeySpec);
ECPrivateKeyParameters ecPrivateKeyParameters = (ECPrivateKeyParameters) ECKeyParameters.getInstance(privateKey);
// 2. 初始化SM2解密引擎(需与加密格式一致,C1C3C2)
SM2Engine sm2Engine = new SM2Engine(SM2Engine.Mode.C1C3C2);
sm2Engine.init(false, ecPrivateKeyParameters);
// 3. 执行解密并返回结果(字节数组转换为字符串)
byte[] encryptedBytes = Hex.decode(encryptedContentHex);
byte[] decryptedBytes = sm2Engine.processBlock(encryptedBytes, 0, encryptedBytes.length);
return new String(decryptedBytes, CHARSET);
}
/**
* SM2签名(私钥签名)
* @param privateKeyHex 16进制格式的私钥
* @param content 待签名的明文
* @return 16进制格式的签名结果
* @throws Exception 异常
*/
public static String sign(String privateKeyHex, String content) throws Exception {
// 1. 解析私钥
byte[] privateKeyBytes = Hex.decode(privateKeyHex);
PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(privateKeyBytes);
PrivateKey privateKey = Security.getInstance(ALGORITHM_NAME, BouncyCastleProvider.PROVIDER_NAME)
.generatePrivate(pkcs8EncodedKeySpec);
ECPrivateKeyParameters ecPrivateKeyParameters = (ECPrivateKeyParameters) ECKeyParameters.getInstance(privateKey);
// 2. 初始化SM2签名引擎(使用SM3作为哈希算法,国密标准)
org.bouncycastle.crypto.signers.SM2Signer sm2Signer = new org.bouncycastle.crypto.signers.SM2Signer();
sm2Signer.init(true, new ParametersWithRandom(ecPrivateKeyParameters, new SecureRandom()));
// 3. 执行签名并返回结果
byte[] contentBytes = content.getBytes(CHARSET);
sm2Signer.update(contentBytes, 0, contentBytes.length);
byte[] signBytes = sm2Signer.generateSignature();
return Hex.toHexString(signBytes);
}
/**
* SM2验签(公钥验签)
* @param publicKeyHex 16进制格式的公钥
* @param content 待验签的明文
* @param signHex 16进制格式的签名结果
* @return 验签结果(true:验签通过,false:验签失败)
* @throws Exception 异常
*/
public static boolean verify(String publicKeyHex, String content, String signHex) throws Exception {
// 1. 解析公钥
byte[] publicKeyBytes = Hex.decode(publicKeyHex);
X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(publicKeyBytes);
PublicKey publicKey = Security.getInstance(ALGORITHM_NAME, BouncyCastleProvider.PROVIDER_NAME)
.generatePublic(x509EncodedKeySpec);
ECPublicKeyParameters ecPublicKeyParameters = (ECPublicKeyParameters) ECKeyParameters.getInstance(publicKey);
// 2. 初始化SM2验签引擎
org.bouncycastle.crypto.signers.SM2Signer sm2Signer = new org.bouncycastle.crypto.signers.SM2Signer();
sm2Signer.init(false, ecPublicKeyParameters);
// 3. 执行验签并返回结果
byte[] contentBytes = content.getBytes(CHARSET);
sm2Signer.update(contentBytes, 0, contentBytes.length);
return sm2Signer.verifySignature(Hex.decode(signHex));
}
/**
* 密钥对VO(用于封装公钥和私钥)
*/
public static class KeyPairVO {
private String publicKey; // 16进制公钥
private String privateKey; // 16进制私钥
public KeyPairVO(String publicKey, String privateKey) {
this.publicKey = publicKey;
this.privateKey = privateKey;
}
// getter和setter
public String getPublicKey() { return publicKey; }
public void setPublicKey(String publicKey) { this.publicKey = publicKey; }
public String getPrivateKey() { return privateKey; }
public void setPrivateKey(String privateKey) { this.privateKey = privateKey; }
}
// 测试方法
public static void main(String[] args) throws Exception {
// 1. 生成密钥对
KeyPairVO keyPairVO = generateKeyPair();
System.out.println("SM2公钥:" + keyPairVO.getPublicKey());
System.out.println("SM2私钥:" + keyPairVO.getPrivateKey());
// 2. 加密解密测试
String content = "测试SM2加密解密";
String encrypted = encrypt(keyPairVO.getPublicKey(), content);
System.out.println("SM2加密后密文:" + encrypted);
String decrypted = decrypt(keyPairVO.getPrivateKey(), encrypted);
System.out.println("SM2解密后明文:" + decrypted);
// 3. 签名验签测试
String sign = sign(keyPairVO.getPrivateKey(), content);
System.out.println("SM2签名结果:" + sign);
boolean verifyResult = verify(keyPairVO.getPublicKey(), content, sign);
System.out.println("SM2验签结果:" + verifyResult); // 应输出true
}
}
4.2 核心环节关键说明
(1)密钥对生成
-
曲线参数:SM2必须使用国密标准推荐的曲线"sm2p256v1",不可使用其他椭圆曲线(如secp256r1),否则会导致兼容性问题。
-
密钥存储:生成的公钥和私钥建议转换为16进制字符串或Base64格式存储(工具类中使用16进制),避免直接存储字节数组。私钥需严格保密,建议加密后存储(如使用KMS密钥管理服务)。
(2)加密解密
-
加密模式:SM2有C1C2C3和C1C3C2两种加密结果格式,国密标准推荐使用C1C3C2格式,工具类中已指定,需确保加密和解密使用相同格式。
-
公钥加密:加密使用公钥,任何人都可获取公钥对数据加密,但只有对应的私钥持有者才能解密,适用于敏感数据传输(如用户密码传输)。
(3)签名验签
-
哈希算法:SM2签名默认使用SM3作为哈希算法(工具类中已集成),无需额外指定,符合国密标准要求。
-
私钥签名:签名使用私钥,验签使用公钥,可用于身份认证和数据防篡改(如接口请求参数签名)。即使明文被篡改,验签也会失败。
五、生产环境使用注意事项
在将SM2和SM3集成到生产环境时,除了代码实现外,还需关注以下安全和兼容性问题:
5.1 安全防护
-
私钥安全:SM2私钥是核心保密信息,禁止硬编码在代码中或明文存储在配置文件中,建议使用密钥管理服务(KMS)、加密机或硬件安全模块(HSM)存储和管理。
-
随机数安全:密钥生成、加密、签名过程中使用的随机数必须是密码学安全的随机数(工具类中使用SecureRandom),不可使用普通随机数生成器(如java.util.Random)。
-
盐值管理:SM3加盐哈希的盐值需随机生成,每个用户单独存储,不可多个用户共用一个盐值。
5.2 兼容性问题
-
库版本一致性:项目中使用的BouncyCastle版本需统一,避免不同模块使用不同版本导致冲突。建议在pom.xml中锁定版本。
-
跨语言兼容性:若Java项目需与其他语言(如C++、Go、Python)的系统进行SM2/SM3交互,需确保双方使用相同的曲线参数、加密格式(如C1C3C2)、哈希规则和编码格式(16进制/Base64)。
5.3 性能优化
-
密钥缓存:SM2密钥对生成成本较高,若需频繁使用同一密钥对(如服务端密钥),建议将公钥和私钥缓存到内存中,避免重复生成或解析。
-
批量处理:若需对大量数据进行SM3哈希或SM2加密,建议分批次处理,避免单次处理过大数据导致内存溢出。
六、总结
SM2和SM3作为国密体系的核心算法,在数据安全保障中发挥着重要作用。本文通过BouncyCastle库实现了SM3的哈希计算和SM2的加密解密、签名验签功能,并提供了完整的工具类和测试方法。开发者在实际集成时,需重点关注密钥安全、算法参数一致性和跨系统兼容性问题,确保国密算法的正确、安全使用。随着国密合规要求的不断提升,掌握国密算法的集成技巧将成为开发者的必备能力之一。