1. 国密SM2算法简介
第一次接触国密SM2算法时,我完全被它优雅的数学设计所吸引。作为我国自主研发的商用密码算法,SM2基于ECC椭圆曲线密码学,相比传统RSA算法有着天然的优势。最直观的感受是,在保证相同安全强度的情况下,SM2的密钥长度只需要256位,而RSA需要2048位。这意味着更小的存储空间、更快的运算速度和更低的网络传输开销。
记得去年重构一个金融项目时,我们将原有的RSA算法迁移到SM2后,API响应时间直接缩短了40%。特别是在移动端场景下,这种性能提升带来的用户体验改善非常明显。SM2算法包含数字签名、密钥交换和公钥加密三大功能,今天我们重点讨论的就是其中最常用的公钥加密场景。
2. 环境准备与依赖配置
2.1 BouncyCastle库的引入
在Java中实现SM2算法,BouncyCastle是绕不开的加密库。这个轻量级的加密包提供了对国密算法的完整支持。我推荐使用Maven管理依赖,在pom.xml中添加以下配置:
<dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcprov-jdk15to18</artifactId> <version>1.71</version> </dependency>这里有个小坑需要注意:不同JDK版本要选择对应的BouncyCastle版本。比如JDK 8可以用1.68版本,而JDK 11+建议使用1.71+版本。我曾经因为版本不匹配导致NoSuchMethodError错误,调试了半天才发现问题所在。
2.2 安全提供者注册
在使用前,我们需要将BouncyCastle注册为JVM的安全提供者。这个操作只需要在程序启动时执行一次:
import org.bouncycastle.jce.provider.BouncyCastleProvider; import java.security.Security; public class SM2Demo { static { if (Security.getProvider("BC") == null) { Security.addProvider(new BouncyCastleProvider()); } } }3. SM2密钥对生成实战
3.1 密钥生成核心代码
生成SM2密钥对是整个加密过程的第一步。下面这个工具方法我一直在生产环境使用,稳定性值得信赖:
public static KeyPair generateSM2KeyPair() throws Exception { ECGenParameterSpec sm2Spec = new ECGenParameterSpec("sm2p256v1"); KeyPairGenerator kpg = KeyPairGenerator.getInstance("EC", "BC"); kpg.initialize(sm2Spec, new SecureRandom()); return kpg.generateKeyPair(); }这里有几个技术细节值得注意:
- "sm2p256v1"是国密标准定义的椭圆曲线参数
- 必须指定使用BouncyCastle提供者("BC")
- SecureRandom确保密钥生成的随机性
3.2 密钥格式转换
实际项目中,我们经常需要将密钥转换为十六进制字符串方便存储。这是我常用的转换方法:
public static String getPublicKeyHex(PublicKey publicKey) { BCECPublicKey bcPubKey = (BCECPublicKey) publicKey; return Hex.toHexString(bcPubKey.getQ().getEncoded(false)); } public static String getPrivateKeyHex(PrivateKey privateKey) { BCECPrivateKey bcPrivKey = (BCECPrivateKey) privateKey; return bcPrivKey.getD().toString(16); }4. 数据加密实现详解
4.1 加密模式选择
SM2支持两种加密模式:C1C3C2和C1C2C3。它们的区别在于密文结构的排列顺序:
- C1C3C2:国密标准推荐模式
- C1C2C3:与某些国际标准兼容的模式
public static String encrypt(String publicKeyHex, String plainText) throws Exception { BCECPublicKey publicKey = getPublicKeyFromHex(publicKeyHex); SM2Engine engine = new SM2Engine(SM2Engine.Mode.C1C3C2); // 其余加密逻辑... }4.2 完整加密流程
下面是我封装的一个完整加密方法,包含了异常处理和编码转换:
public static String encrypt(BCECPublicKey publicKey, String plainText) { try { ECParameterSpec ecSpec = publicKey.getParameters(); ECDomainParameters ecDomain = new ECDomainParameters( ecSpec.getCurve(), ecSpec.getG(), ecSpec.getN()); ECPublicKeyParameters pubKeyParams = new ECPublicKeyParameters( publicKey.getQ(), ecDomain); SM2Engine engine = new SM2Engine(SM2Engine.Mode.C1C3C2); engine.init(true, new ParametersWithRandom(pubKeyParams, new SecureRandom())); byte[] input = plainText.getBytes(StandardCharsets.UTF_8); byte[] encrypted = engine.processBlock(input, 0, input.length); return Hex.toHexString(encrypted); } catch (Exception e) { throw new RuntimeException("SM2加密失败", e); } }5. 数据解密过程解析
5.1 解密核心逻辑
解密是加密的逆过程,但有几个关键点需要注意:
- 必须使用加密时相同的模式(C1C3C2或C1C2C3)
- 密文需要先进行Hex解码
- 要处理可能的填充异常
public static String decrypt(String privateKeyHex, String cipherText) { try { BCECPrivateKey privateKey = getPrivateKeyFromHex(privateKeyHex); byte[] cipherData = Hex.decode(cipherText); ECParameterSpec ecSpec = privateKey.getParameters(); ECDomainParameters ecDomain = new ECDomainParameters( ecSpec.getCurve(), ecSpec.getG(), ecSpec.getN()); ECPrivateKeyParameters privKeyParams = new ECPrivateKeyParameters( privateKey.getD(), ecDomain); SM2Engine engine = new SM2Engine(SM2Engine.Mode.C1C3C2); engine.init(false, privKeyParams); byte[] decrypted = engine.processBlock(cipherData, 0, cipherData.length); return new String(decrypted, StandardCharsets.UTF_8); } catch (Exception e) { throw new RuntimeException("SM2解密失败", e); } }5.2 常见解密异常处理
在实际项目中,我遇到过最多的解密问题包括:
- 密文被篡改导致的验签失败
- 使用了错误的私钥
- 加密/解密模式不匹配
建议在解密逻辑中加入更详细的错误日志,方便问题排查:
catch (InvalidCipherTextException e) { logger.error("密文格式异常,可能被篡改", e); throw new BusinessException("解密失败:无效的密文"); } catch (ArrayIndexOutOfBoundsException e) { logger.error("密文长度异常", e); throw new BusinessException("解密失败:密文长度不正确"); }6. 完整示例与性能优化
6.1 端到端示例代码
下面是一个可以直接运行的完整示例:
public class SM2FullDemo { public static void main(String[] args) throws Exception { // 1. 生成密钥对 KeyPair keyPair = generateSM2KeyPair(); String pubKeyHex = getPublicKeyHex(keyPair.getPublic()); String privKeyHex = getPrivateKeyHex(keyPair.getPrivate()); // 2. 加密测试 String originalText = "这是一段需要加密的敏感数据"; String encrypted = encrypt(pubKeyHex, originalText); System.out.println("加密结果:" + encrypted); // 3. 解密测试 String decrypted = decrypt(privKeyHex, encrypted); System.out.println("解密结果:" + decrypted); } // 这里插入前面介绍的所有工具方法... }6.2 性能优化建议
在高并发场景下,SM2算法仍有优化空间:
- 密钥对生成可以预先生成并缓存
- SM2Engine实例可以线程复用
- 考虑使用原生库加速(如通过JNI调用GMSSL)
我在一个百万级用户的项目中,通过以下优化使TPS提升了3倍:
- 使用ThreadLocal缓存SM2Engine实例
- 预先生成一批密钥对备用
- 对短数据采用内存池管理
7. 生产环境注意事项
7.1 密钥安全管理
千万不能像示例代码这样直接打印密钥!在实际项目中:
- 私钥必须加密存储
- 推荐使用HSM硬件加密机
- 实现密钥轮换机制
7.2 兼容性问题
不同平台的SM2实现可能有细微差异,特别是在:
- 曲线参数的定义
- 密文结构的编码
- 签名算法的细节
建议在系统对接时,先进行加密/解密的交叉测试。我曾经遇到过一个坑:某厂商实现的SM2在密文前额外加了4字节长度头,导致解密失败。