1. 项目概述为什么要在Java里实现SM2如果你是一名Java开发者最近在接触金融、政务、物联网或者任何对数据安全有高要求的项目那么“国密算法”这个词你大概率绕不开。而SM2作为国密算法体系中非对称加密的“扛把子”其重要性不言而喻。最近我在重构一个涉及电子合同签章的后台服务甲方明确要求必须采用国密算法套件进行数据加密和签名验签于是把SM2在Java环境里跑通、跑稳就成了一个必须啃下来的硬骨头。网上关于SM2的资料不少但很多要么是零散的代码片段讲不清所以然要么是直接丢给你一个jar包黑盒操作出了问题两眼一抹黑。所以我决定把这次从调研、选型、实现到踩坑排雷的全过程记录下来。这不仅仅是一个“Hello World”式的演示更是一次深入算法原理、吃透工具库、并最终形成生产级可用方案的实战总结。无论你是正在应对类似需求的同行还是对密码学在Java中的应用感兴趣的学习者希望这篇长文能给你带来实实在在的参考价值。简单来说SM2是一种基于椭圆曲线密码学ECC的非对称加密算法。它和你们熟悉的RSA是同一类东西都有一对公钥和私钥但背后的数学原理不同在相同安全强度下SM2的密钥长度更短256位SM2约等于3072位RSA运算速度更快尤其适合移动互联网和物联网这些资源受限的场景。在Java里实现它核心就是找到靠谱的底层库然后正确地调用API处理好密钥、加密、解密、签名、验签这一整套流程。2. 核心原理与方案选型为什么是Bouncy Castle在动手写代码之前搞清楚“用什么”和“为什么用”至关重要。Java标准库JCE本身并不直接支持国密算法这就需要我们引入第三方密码学提供者Provider。2.1 主流Java国密实现方案对比目前社区里主要有几种选择我做了个简单的对比表格方案代表库/产品优点缺点适用场景国际开源密码库Bouncy Castle (BC)生态成熟文档丰富支持全面SM2/SM3/SM4活跃度高。纯Java实现在某些极端性能场景可能不如本地库。通用性最强学习、开发和大多数生产环境首选。国内商业/开源库如GMSSL的Java绑定、一些商业密码模块可能针对国密有专门优化或与特定硬件结合更好。开源版本可能文档不全、更新慢商业版有许可和成本问题。有特定硬件需求或深度定制化需求的场景。基于JNI的本地库封装调用C/C实现的国密库如GmSSL性能理论上限高可直接利用硬件加速。集成复杂跨平台部署麻烦需编译.so/.dll易引入native内存等问题。对性能有极致要求且有能力处理native层复杂性的团队。对于绝大多数项目尤其是快速启动、要求稳定和可维护性的场景Bouncy Castle几乎是毋庸置疑的首选。它是一个非常成熟、经过广泛审计的密码学库提供了“轻量级API”和“JCE Provider”两种使用方式。为了与现有的Java安全体系如KeyPairGenerator,Cipher,Signature无缝集成我们通常选择将其作为JCE Provider来使用。这意味着你可以用写RSA代码几乎一样的习惯来写SM2代码学习成本大大降低。注意Bouncy Castle库有两个主要的包名版本org.bouncycastle和较老的bouncycastle。请务必使用Maven中央仓库中GroupId为org.bouncycastle的版本这是官方维护的。2.2 SM2算法核心要点速览在编码前理解这几个关键点能避免很多低级错误椭圆曲线参数SM2使用的是定义在国标GB/T 32918.5-2017中的一条特定的256位素数域椭圆曲线其参数是固定的。Bouncy Castle已经内置了这条名为“sm2p256v1”的曲线。你不需要自己定义这些大数参数直接使用标识符即可。加密解密流程SM2加密并非直接使用公钥运算它包含一个关键的“密钥派生函数KDF”通常使用SM3哈希算法。流程是生成临时密钥对 - 计算共享秘密 - 用KDF派生会话密钥 - 对称加密数据。解密则是逆过程。签名与验签SM2的签名算法也整合了SM3哈希。其签名结果通常由两个大整数(r, s)连接而成并且为了兼容性有时会采用ASN.1 DER编码格式。在和其他系统如用C、Go写的服务对接时签名值的格式是第一个需要对齐的“暗坑”。公钥格式SM2公钥是一个椭圆曲线上的点(x, y)。在传输或存储时通常有两种格式压缩公钥1字节前缀 32字节x坐标和非压缩公钥1字节前缀 32字节x 32字节y。国内很多系统默认使用非压缩格式04开头。而Bouncy Castle默认生成和处理的往往是符合X.509标准的SubjectPublicKeyInfo结构里面就包含了这个非压缩公钥点。理清了这些我们就可以开始准备开发环境了。3. 环境准备与核心依赖配置这里我以最常用的Maven项目为例Gradle的配置也类似。3.1 引入Bouncy Castle依赖在你的pom.xml文件中需要添加两个依赖dependencies !-- Bouncy Castle Provider 核心包 -- dependency groupIdorg.bouncycastle/groupId artifactIdbcprov-jdk18on/artifactId version1.78/version !-- 请使用当时最新稳定版 -- /dependency !-- Bouncy Castle PKIX/证书相关支持用于处理密钥格式 -- dependency groupIdorg.bouncycastle/groupId artifactIdbcpkix-jdk18on/artifactId version1.78/version /dependency /dependenciesbcprov是提供密码学算法实现的Provider本身bcpkix则包含了处理证书、CRL、OCSP等公钥基础设施相关的工具我们在转换密钥格式时会用到它。3.2 安全提供者动态注册为了让JVM识别并使用Bouncy Castle的SM2实现我们需要在代码中动态注册这个Provider。通常这放在程序初始化阶段如Spring Boot的PostConstruct或静态块中。import org.bouncycastle.jce.provider.BouncyCastleProvider; import java.security.Security; public class Sm2Util { static { // 防止重复注册 if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) null) { Security.addProvider(new BouncyCastleProvider()); } } // ... 后续工具方法 }实操心得虽然也可以在JVM启动参数中通过-Djava.security.properties文件静态注册Provider但在容器化部署、动态环境里代码内动态注册的方式更灵活、更可控也是我更推荐的做法。4. 密钥对生成与管理密钥是加密体系的根基生成和保存必须谨慎。4.1 生成SM2密钥对使用标准的KeyPairGenerator类指定算法为“EC”并设置SM2特有的椭圆曲线参数。import java.security.*; import java.security.spec.ECGenParameterSpec; public static KeyPair generateSm2KeyPair() throws NoSuchAlgorithmException, InvalidAlgorithmParameterException, NoSuchProviderException { // 1. 获取密钥对生成器实例指定算法为椭圆曲线“EC”提供者为BC KeyPairGenerator keyPairGen KeyPairGenerator.getInstance(EC, BouncyCastleProvider.PROVIDER_NAME); // 2. 初始化使用SM2的标准曲线参数“sm2p256v1” ECGenParameterSpec sm2Spec new ECGenParameterSpec(sm2p256v1); keyPairGen.initialize(sm2Spec, new SecureRandom()); // 使用强随机数源 // 3. 生成密钥对 return keyPairGen.generateKeyPair(); }这段代码生成的KeyPair中PrivateKey是ECPrivateKey实例PublicKey是ECPublicKey实例它们内部包含了标准的椭圆曲线密钥参数。4.2 密钥的保存与加载PEM格式生成的密钥对需要持久化。PEM格式-----BEGIN XXX-----是常见且可读性较好的格式。我们可以使用Bouncy Castle的PEMParser和JcaPEMWriter来处理。将密钥对保存为PEM文件import org.bouncycastle.openssl.jcajce.JcaPEMWriter; import java.io.FileWriter; import java.security.PrivateKey; import java.security.PublicKey; public static void saveKeyToPem(Key key, String filePath) throws IOException { try (JcaPEMWriter pemWriter new JcaPEMWriter(new FileWriter(filePath))) { pemWriter.writeObject(key); pemWriter.flush(); } } // 使用saveKeyToPem(keyPair.getPrivate(), sm2_private.pem); // saveKeyToPem(keyPair.getPublic(), sm2_public.pem);从PEM文件加载密钥import org.bouncycastle.openssl.PEMParser; import org.bouncycastle.openssl.PEMKeyPair; import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; import java.io.FileReader; import java.security.KeyPair; import java.security.PrivateKey; import java.security.PublicKey; public static PrivateKey loadPrivateKeyFromPem(String filePath) throws IOException { try (PEMParser pemParser new PEMParser(new FileReader(filePath))) { Object object pemParser.readObject(); JcaPEMKeyConverter converter new JcaPEMKeyConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME); if (object instanceof PEMKeyPair) { return converter.getPrivateKey(((PEMKeyPair) object).getPrivateKeyInfo()); } else if (object instanceof PrivateKey) { return (PrivateKey) object; } else { throw new IOException(不支持的PEM格式: object.getClass()); } } } public static PublicKey loadPublicKeyFromPem(String filePath) throws IOException { try (PEMParser pemParser new PEMParser(new FileReader(filePath))) { Object object pemParser.readObject(); JcaPEMKeyConverter converter new JcaPEMKeyConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME); if (object instanceof PublicKey) { return (PublicKey) object; } else { // 也可能读到的是SubjectPublicKeyInfo return converter.getPublicKey((SubjectPublicKeyInfo) object); } } }注意事项PEM文件可能包含多种类型如加密的私钥、证书等。上述代码是基础示例生产环境需要更健壮的类型判断和异常处理。私钥文件务必妥善保管建议加密存储。4.3 获取原始公钥点十六进制字符串在与某些硬件设备或特定协议对接时对方可能要求直接提供非压缩公钥的十六进制字符串即04xy。我们可以从ECPublicKey中提取import java.security.interfaces.ECPublicKey; import org.bouncycastle.math.ec.ECPoint; import java.math.BigInteger; public static String getUncompressedPublicKeyHex(PublicKey publicKey) { if (!(publicKey instanceof ECPublicKey)) { throw new IllegalArgumentException(非EC公钥); } ECPublicKey ecPubKey (ECPublicKey) publicKey; // 获取椭圆曲线点 ECPoint point ecPubKey.getW(); // 获取x, y坐标BigInteger类型 BigInteger x point.getAffineXCoord().toBigInteger(); BigInteger y point.getAffineYCoord().toBigInteger(); // 转换为固定长度64字符的十六进制字符串不足补0 String xHex leftPad(x.toString(16), 64); String yHex leftPad(y.toString(16), 64); // 非压缩格式以04开头 return 04 xHex yHex; } private static String leftPad(String str, int size) { StringBuilder sb new StringBuilder(str); while (sb.length() size) { sb.insert(0, 0); } return sb.toString(); }这个64字节128字符十六进制的字符串就是很多国密SDK或文档里提到的“裸公钥”。5. SM2加密与解密实现SM2加密解密过程比RSA稍复杂因为涉及密钥派生。Bouncy Castle的“轻量级API”提供了更直接的接口。5.1 使用SM2Engine进行加密这里我们使用SM2Engine它封装了加密流程。import org.bouncycastle.crypto.engines.SM2Engine; import org.bouncycastle.crypto.params.ECPublicKeyParameters; import org.bouncycastle.crypto.params.ParametersWithRandom; import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey; import java.security.SecureRandom; public static byte[] encrypt(byte[] data, PublicKey publicKey) throws Exception { // 1. 转换公钥为BC内部参数格式 BCECPublicKey bcPubKey (BCECPublicKey) publicKey; ECPublicKeyParameters pubKeyParams new ECPublicKeyParameters( bcPubKey.getQ(), bcPubKey.getParameters() ); // 2. 创建SM2加密引擎使用C1C3C2模式这是国标推荐顺序 SM2Engine engine new SM2Engine(SM2Engine.Mode.C1C3C2); // 3. 初始化引擎加密模式传入公钥和随机数 engine.init(true, new ParametersWithRandom(pubKeyParams, new SecureRandom())); // 4. 执行加密 return engine.processBlock(data, 0, data.length); }关键点解析SM2Engine.Mode.C1C3C2这是加密后数据的排列顺序。C1是临时公钥点C3是SM3得到的摘要C2是实际加密后的密文。这是国标规定的标准顺序。另一种模式是C1C2C3多见于一些旧的实现或国际标准。对接时务必确认双方模式一致否则解密必然失败。ParametersWithRandom加密过程需要随机数来生成临时密钥对这里我们使用密码学安全的SecureRandom。5.2 使用SM2Engine进行解密解密是加密的逆过程需要私钥。import org.bouncycastle.crypto.params.ECPrivateKeyParameters; import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey; public static byte[] decrypt(byte[] sm2CipherText, PrivateKey privateKey) throws Exception { // 1. 转换私钥为BC内部参数格式 BCECPrivateKey bcPrivKey (BCECPrivateKey) privateKey; ECPrivateKeyParameters privKeyParams new ECPrivateKeyParameters( bcPrivKey.getD(), bcPrivKey.getParameters() ); // 2. 创建SM2解密引擎模式必须与加密时一致 SM2Engine engine new SM2Engine(SM2Engine.Mode.C1C3C2); // 3. 初始化引擎解密模式传入私钥 engine.init(false, privKeyParams); // 4. 执行解密 return engine.processBlock(sm2CipherText, 0, sm2CipherText.length); }踩坑实录我曾在与一个硬件加密机对接时耗费了大半天排查解密失败。最后发现硬件加密机默认输出是C1C2C3模式而我的代码使用的是C1C3C2。解决方法要么让对方切换模式要么在解密前自己重组密文顺序。所以“模式”是SM2加解密对接的第一道坎。6. SM2签名与验签实现签名验签是身份认证和完整性保护的核心。SM2的签名算法通常表示为SM3withSM2。6.1 使用标准JCE Signature接口签名最通用的方式是使用JCE的Signature类这样代码风格与RSA签名保持一致。public static byte[] sign(byte[] data, PrivateKey privateKey) throws Exception { // 1. 获取Signature实例指定算法为“SM3withSM2”提供者为BC Signature signature Signature.getInstance(SM3withSM2, BouncyCastleProvider.PROVIDER_NAME); // 2. 初始化签名对象签名模式传入私钥 signature.initSign(privateKey); // 3. 传入待签名数据 signature.update(data); // 4. 生成签名值 return signature.sign(); }这里有一个巨大的坑需要注意Signature.sign()返回的字节数组其编码格式是ASN.1 DER编码的。它并不是简单的r||sr和s拼接而是一个DER序列SEQUENCE里面包含了两个INTEGERr和s。这个格式在Java生态内流通没问题但很多其他平台如某些C库、硬件设备可能要求原始的、固定长度的r||s格式各32字节共64字节。6.2 处理签名值的两种格式因此处理签名时格式转换是常态。import org.bouncycastle.asn1.*; import java.math.BigInteger; /** * 将ASN.1 DER格式的签名转换为裸的 r|s 字节数组 (各32字节共64字节) */ public static byte[] convertDerSignToPlain(byte[] derSign) throws IOException { ASN1Sequence seq ASN1Sequence.getInstance(derSign); BigInteger r ASN1Integer.getInstance(seq.getObjectAt(0)).getValue(); BigInteger s ASN1Integer.getInstance(seq.getObjectAt(1)).getValue(); // 将r和s转换为固定32字节的字节数组 byte[] rBytes to32Bytes(r); byte[] sBytes to32Bytes(s); byte[] plainSign new byte[64]; System.arraycopy(rBytes, 0, plainSign, 0, 32); System.arraycopy(sBytes, 0, plainSign, 32, 32); return plainSign; } /** * 将裸的 r|s 字节数组转换为ASN.1 DER格式 */ public static byte[] convertPlainSignToDer(byte[] plainSign) throws IOException { if (plainSign.length ! 64) { throw new IllegalArgumentException(Plain signature must be 64 bytes); } byte[] rBytes new byte[32]; byte[] sBytes new byte[32]; System.arraycopy(plainSign, 0, rBytes, 0, 32); System.arraycopy(plainSign, 32, sBytes, 0, 32); BigInteger r new BigInteger(1, rBytes); // 正数 BigInteger s new BigInteger(1, sBytes); ASN1EncodableVector v new ASN1EncodableVector(); v.add(new ASN1Integer(r)); v.add(new ASN1Integer(s)); return new DERSequence(v).getEncoded(); } private static byte[] to32Bytes(BigInteger bi) { byte[] bytes bi.toByteArray(); if (bytes.length 32) { return bytes; } else if (bytes.length 32) { // 如果字节数组长度超过32比如因为符号位取后32位 byte[] result new byte[32]; System.arraycopy(bytes, bytes.length - 32, result, 0, 32); return result; } else { // 如果不足32字节前面补0 byte[] result new byte[32]; System.arraycopy(bytes, 0, result, 32 - bytes.length, bytes.length); return result; } }6.3 验签实现验签同样使用Signature类注意处理不同格式的签名值。public static boolean verify(byte[] data, byte[] signature, PublicKey publicKey) throws Exception { Signature verifier Signature.getInstance(SM3withSM2, BouncyCastleProvider.PROVIDER_NAME); verifier.initVerify(publicKey); verifier.update(data); // 先判断签名格式如果是64字节可能是裸签名需要尝试转换 byte[] sigToVerify signature; if (signature.length 64) { try { // 尝试将裸签名转换为DER格式进行验签 sigToVerify convertPlainSignToDer(signature); } catch (Exception e) { // 转换失败可能不是标准的r|s直接使用原字节验签虽然很可能失败 // 生产环境应有更明确的格式约定和错误处理 } } // 如果是其他长度如70字节假定已经是DER格式 return verifier.verify(sigToVerify); }核心经验在涉及多系统交互的签名验签场景中第一件要做的事就是明确签名值的格式。是ASN.1 DER还是裸的64字节最好在接口文档或协议中白纸黑字写清楚并编写对应的格式转换工具函数这能节省大量联调时间。7. 生产环境进阶考量与性能优化把功能跑通只是第一步要上生产环境还有不少细节需要打磨。7.1 算法提供者优先级与算法名称在注册了多个Provider的复杂环境里为了确保SM2算法一定由BC提供可以在获取实例时显式指定Provider。// 更稳妥的获取方式 KeyPairGenerator kpg KeyPairGenerator.getInstance(EC, BC); Signature signature Signature.getInstance(SM3withSM2, BC); Cipher cipher Cipher.getInstance(SM2, BC); // 注意标准JCE Cipher可能不支持SM2通常用SM2Engine有些情况下算法名称可能是“1.2.156.10197.1.301”SM2的OID或“SM2”。使用“SM3withSM2”和“EC”配合BC Provider是最通用的。7.2 大文件或数据流的处理上述示例都是对内存中的字节数组进行操作。对于大文件不能一次性读入内存。签名/验签Signature.update(byte[] b, int off, int len)方法支持分块更新可以配合BufferedInputStream循环读取文件并更新最后再sign()或verify。加密/解密SM2Engine本身是分组加密但通常用于加密对称密钥如SM4的密钥而非直接加密大文件。标准的“混合加密”模式是用SM2加密一个随机生成的对称密钥如SM4密钥再用这个对称密钥去加密实际的大文件数据。SM2Engine的processBlock方法一次处理一个“块”对于大数据需要自己管理循环调用虽然不常见。更常见的做法是使用Cipher类并指定“SM2”为密钥协商算法但BC的JCE支持度不一直接使用SM2Engine进行密钥加密更可靠。7.3 线程安全与对象复用KeyPairGenerator,Signature,Cipher这些JCE类通常不是线程安全的。在高并发场景下应该避免在多个线程间共享同一个实例。要么每次使用时创建新实例对于轻量操作如签名开销可接受要么使用ThreadLocal为每个线程缓存一个实例。SM2Engine对象在每次init之后用于一次加密或解密操作。操作完成后如果需要再次使用必须重新调用init进行初始化。不建议在并发环境下复用同一个SM2Engine实例。7.4 性能监控与调试在性能关键路径上可以关注密钥生成SM2密钥生成比RSA快很多一般不是瓶颈。加解密速度SM2加密解密速度也优于同等安全强度的RSA。如果发现性能不符合预期检查是否错误地用于加密大量数据应仅用于加密密钥或小块数据。签名验签速度SM2签名很快验签稍慢但整体依然优秀。可以使用Java Microbenchmark Harness (JMH) 进行基准测试对比不同场景下的性能表现。8. 常见问题排查与实战技巧这里汇总了我遇到的一些典型问题及解决方法。8.1 问题速查表问题现象可能原因排查步骤与解决方案NoSuchAlgorithmException: no such algorithm: SM3withSM2 for provider BC1. Bouncy Castle未正确注册。2. 依赖版本冲突或损坏。1. 检查Security.addProvider是否执行且成功。2. 检查BouncyCastleProvider.PROVIDER_NAME常量是否为“BC”。3. 清理Maven本地仓库重新下载依赖。解密失败或验签失败1. 加密/签名与解密/验签使用的**模式C1C3C2/C1C2C3**不一致。2. 公钥私钥不配对。3. 签名值格式DER/裸64字节不匹配。4. 待处理数据在传输过程中被篡改或编码如Base64处理有误。1.首先确认模式这是最高频问题。2. 重新生成密钥对测试排除密钥问题。3. 对签名值打印长度64字节尝试转换DER70字节尝试解析DER。4. 确保加解密/签名验签前后数据完全一致注意编码解码。与其他系统如OpenSSL、硬件加密机对接失败1. 椭圆曲线参数不一致虽都是sm2p256v1但实现微调可能有差异。2. 公钥格式不一致是否带04头是否X.509封装。3. 签名/加密结果格式不一致。1. 使用双方都认可的测试向量Test Vector进行交叉验证。2. 对比公钥的十六进制表示确认是否都是04开头的非压缩格式。3. 编写小规模、分步骤的测试程序逐个环节密钥生成-导出-加密-结果对比输出。InvalidKeyException传入的Key对象类型错误或不是有效的SM2密钥。确认publicKey是ECPublicKey实例privateKey是ECPrivateKey实例。检查密钥是否来自正确的PEM文件或生成过程。性能低下错误地用SM2加密大数据。回顾非对称加密原理SM2应用于加密会话密钥或小数据如几十到几百字节。大数据应使用SM4等对称算法加密。8.2 调试技巧开启Bouncy Castle的详细日志Bouncy Castle本身日志不多但可以通过JVM参数开启Java安全相关的日志有助于诊断Provider注册、算法查找等问题。-Djava.security.debugprovider在启动命令中加入上述参数可以在控制台看到Provider加载和算法查找的详细信息。8.3 一个完整的工具类示例骨架最后给出一个整合了上述关键功能的工具类骨架你可以在此基础上进行扩展和封装。import org.bouncycastle.asn1.*; import org.bouncycastle.crypto.engines.SM2Engine; import org.bouncycastle.crypto.params.*; import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey; import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey; import org.bouncycastle.jce.provider.BouncyCastleProvider; import javax.crypto.Cipher; import java.io.*; import java.math.BigInteger; import java.security.*; import java.security.spec.ECGenParameterSpec; public class Sm2CryptoUtil { public static final String PROVIDER_NAME BouncyCastleProvider.PROVIDER_NAME; public static final String SM2_CURVE_NAME sm2p256v1; public static final SM2Engine.Mode DEFAULT_MODE SM2Engine.Mode.C1C3C2; static { if (Security.getProvider(PROVIDER_NAME) null) { Security.addProvider(new BouncyCastleProvider()); } } // --- 密钥生成 --- public static KeyPair generateKeyPair() throws Exception { /* 见上文 */ } // --- 密钥PEM格式IO --- public static void saveKeyToPem(Key key, String path) throws IOException { /* 见上文 */ } public static PublicKey loadPublicKeyFromPem(String path) throws IOException { /* 见上文 */ } public static PrivateKey loadPrivateKeyFromPem(String path) throws IOException { /* 见上文 */ } // --- 公钥格式转换 --- public static String getUncompressedPubKeyHex(PublicKey publicKey) { /* 见上文 */ } // --- 加密解密 (使用SM2Engine) --- public static byte[] encrypt(byte[] data, PublicKey publicKey, SM2Engine.Mode mode) throws Exception { BCECPublicKey bcPubKey (BCECPublicKey) publicKey; ECPublicKeyParameters pubKeyParams new ECPublicKeyParameters( bcPubKey.getQ(), bcPubKey.getParameters() ); SM2Engine engine new SM2Engine(mode); engine.init(true, new ParametersWithRandom(pubKeyParams, new SecureRandom())); return engine.processBlock(data, 0, data.length); } public static byte[] decrypt(byte[] cipherText, PrivateKey privateKey, SM2Engine.Mode mode) throws Exception { BCECPrivateKey bcPrivKey (BCECPrivateKey) privateKey; ECPrivateKeyParameters privKeyParams new ECPrivateKeyParameters( bcPrivKey.getD(), bcPrivKey.getParameters() ); SM2Engine engine new SM2Engine(mode); engine.init(false, privKeyParams); return engine.processBlock(cipherText, 0, cipherText.length); } // --- 签名验签 (使用JCE Signature) --- public static byte[] sign(byte[] data, PrivateKey privateKey) throws Exception { Signature signature Signature.getInstance(SM3withSM2, PROVIDER_NAME); signature.initSign(privateKey); signature.update(data); return signature.sign(); // DER格式 } public static boolean verify(byte[] data, byte[] signature, PublicKey publicKey) throws Exception { Signature verifier Signature.getInstance(SM3withSM2, PROVIDER_NAME); verifier.initVerify(publicKey); verifier.update(data); return verifier.verify(signature); } // --- 签名格式转换 --- public static byte[] convertDerSignToPlain(byte[] derSign) throws IOException { /* 见上文 */ } public static byte[] convertPlainSignToDer(byte[] plainSign) throws IOException { /* 见上文 */ } // --- 便捷方法使用默认模式 --- public static byte[] encrypt(byte[] data, PublicKey publicKey) throws Exception { return encrypt(data, publicKey, DEFAULT_MODE); } public static byte[] decrypt(byte[] cipherText, PrivateKey privateKey) throws Exception { return decrypt(cipherText, privateKey, DEFAULT_MODE); } }这个工具类覆盖了SM2在Java中的核心操作。在实际项目中你可能还需要将其与Spring Boot的ConfigurationProperties结合进行密钥路径配置或者集成到你的服务框架中。记住密码学无小事尤其是在生产环境务必做好密钥安全管理、操作日志记录和充分的异常处理。
Java国密SM2算法实战:基于Bouncy Castle的加密、签名与密钥管理
发布时间:2026/7/2 23:31:30
1. 项目概述为什么要在Java里实现SM2如果你是一名Java开发者最近在接触金融、政务、物联网或者任何对数据安全有高要求的项目那么“国密算法”这个词你大概率绕不开。而SM2作为国密算法体系中非对称加密的“扛把子”其重要性不言而喻。最近我在重构一个涉及电子合同签章的后台服务甲方明确要求必须采用国密算法套件进行数据加密和签名验签于是把SM2在Java环境里跑通、跑稳就成了一个必须啃下来的硬骨头。网上关于SM2的资料不少但很多要么是零散的代码片段讲不清所以然要么是直接丢给你一个jar包黑盒操作出了问题两眼一抹黑。所以我决定把这次从调研、选型、实现到踩坑排雷的全过程记录下来。这不仅仅是一个“Hello World”式的演示更是一次深入算法原理、吃透工具库、并最终形成生产级可用方案的实战总结。无论你是正在应对类似需求的同行还是对密码学在Java中的应用感兴趣的学习者希望这篇长文能给你带来实实在在的参考价值。简单来说SM2是一种基于椭圆曲线密码学ECC的非对称加密算法。它和你们熟悉的RSA是同一类东西都有一对公钥和私钥但背后的数学原理不同在相同安全强度下SM2的密钥长度更短256位SM2约等于3072位RSA运算速度更快尤其适合移动互联网和物联网这些资源受限的场景。在Java里实现它核心就是找到靠谱的底层库然后正确地调用API处理好密钥、加密、解密、签名、验签这一整套流程。2. 核心原理与方案选型为什么是Bouncy Castle在动手写代码之前搞清楚“用什么”和“为什么用”至关重要。Java标准库JCE本身并不直接支持国密算法这就需要我们引入第三方密码学提供者Provider。2.1 主流Java国密实现方案对比目前社区里主要有几种选择我做了个简单的对比表格方案代表库/产品优点缺点适用场景国际开源密码库Bouncy Castle (BC)生态成熟文档丰富支持全面SM2/SM3/SM4活跃度高。纯Java实现在某些极端性能场景可能不如本地库。通用性最强学习、开发和大多数生产环境首选。国内商业/开源库如GMSSL的Java绑定、一些商业密码模块可能针对国密有专门优化或与特定硬件结合更好。开源版本可能文档不全、更新慢商业版有许可和成本问题。有特定硬件需求或深度定制化需求的场景。基于JNI的本地库封装调用C/C实现的国密库如GmSSL性能理论上限高可直接利用硬件加速。集成复杂跨平台部署麻烦需编译.so/.dll易引入native内存等问题。对性能有极致要求且有能力处理native层复杂性的团队。对于绝大多数项目尤其是快速启动、要求稳定和可维护性的场景Bouncy Castle几乎是毋庸置疑的首选。它是一个非常成熟、经过广泛审计的密码学库提供了“轻量级API”和“JCE Provider”两种使用方式。为了与现有的Java安全体系如KeyPairGenerator,Cipher,Signature无缝集成我们通常选择将其作为JCE Provider来使用。这意味着你可以用写RSA代码几乎一样的习惯来写SM2代码学习成本大大降低。注意Bouncy Castle库有两个主要的包名版本org.bouncycastle和较老的bouncycastle。请务必使用Maven中央仓库中GroupId为org.bouncycastle的版本这是官方维护的。2.2 SM2算法核心要点速览在编码前理解这几个关键点能避免很多低级错误椭圆曲线参数SM2使用的是定义在国标GB/T 32918.5-2017中的一条特定的256位素数域椭圆曲线其参数是固定的。Bouncy Castle已经内置了这条名为“sm2p256v1”的曲线。你不需要自己定义这些大数参数直接使用标识符即可。加密解密流程SM2加密并非直接使用公钥运算它包含一个关键的“密钥派生函数KDF”通常使用SM3哈希算法。流程是生成临时密钥对 - 计算共享秘密 - 用KDF派生会话密钥 - 对称加密数据。解密则是逆过程。签名与验签SM2的签名算法也整合了SM3哈希。其签名结果通常由两个大整数(r, s)连接而成并且为了兼容性有时会采用ASN.1 DER编码格式。在和其他系统如用C、Go写的服务对接时签名值的格式是第一个需要对齐的“暗坑”。公钥格式SM2公钥是一个椭圆曲线上的点(x, y)。在传输或存储时通常有两种格式压缩公钥1字节前缀 32字节x坐标和非压缩公钥1字节前缀 32字节x 32字节y。国内很多系统默认使用非压缩格式04开头。而Bouncy Castle默认生成和处理的往往是符合X.509标准的SubjectPublicKeyInfo结构里面就包含了这个非压缩公钥点。理清了这些我们就可以开始准备开发环境了。3. 环境准备与核心依赖配置这里我以最常用的Maven项目为例Gradle的配置也类似。3.1 引入Bouncy Castle依赖在你的pom.xml文件中需要添加两个依赖dependencies !-- Bouncy Castle Provider 核心包 -- dependency groupIdorg.bouncycastle/groupId artifactIdbcprov-jdk18on/artifactId version1.78/version !-- 请使用当时最新稳定版 -- /dependency !-- Bouncy Castle PKIX/证书相关支持用于处理密钥格式 -- dependency groupIdorg.bouncycastle/groupId artifactIdbcpkix-jdk18on/artifactId version1.78/version /dependency /dependenciesbcprov是提供密码学算法实现的Provider本身bcpkix则包含了处理证书、CRL、OCSP等公钥基础设施相关的工具我们在转换密钥格式时会用到它。3.2 安全提供者动态注册为了让JVM识别并使用Bouncy Castle的SM2实现我们需要在代码中动态注册这个Provider。通常这放在程序初始化阶段如Spring Boot的PostConstruct或静态块中。import org.bouncycastle.jce.provider.BouncyCastleProvider; import java.security.Security; public class Sm2Util { static { // 防止重复注册 if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) null) { Security.addProvider(new BouncyCastleProvider()); } } // ... 后续工具方法 }实操心得虽然也可以在JVM启动参数中通过-Djava.security.properties文件静态注册Provider但在容器化部署、动态环境里代码内动态注册的方式更灵活、更可控也是我更推荐的做法。4. 密钥对生成与管理密钥是加密体系的根基生成和保存必须谨慎。4.1 生成SM2密钥对使用标准的KeyPairGenerator类指定算法为“EC”并设置SM2特有的椭圆曲线参数。import java.security.*; import java.security.spec.ECGenParameterSpec; public static KeyPair generateSm2KeyPair() throws NoSuchAlgorithmException, InvalidAlgorithmParameterException, NoSuchProviderException { // 1. 获取密钥对生成器实例指定算法为椭圆曲线“EC”提供者为BC KeyPairGenerator keyPairGen KeyPairGenerator.getInstance(EC, BouncyCastleProvider.PROVIDER_NAME); // 2. 初始化使用SM2的标准曲线参数“sm2p256v1” ECGenParameterSpec sm2Spec new ECGenParameterSpec(sm2p256v1); keyPairGen.initialize(sm2Spec, new SecureRandom()); // 使用强随机数源 // 3. 生成密钥对 return keyPairGen.generateKeyPair(); }这段代码生成的KeyPair中PrivateKey是ECPrivateKey实例PublicKey是ECPublicKey实例它们内部包含了标准的椭圆曲线密钥参数。4.2 密钥的保存与加载PEM格式生成的密钥对需要持久化。PEM格式-----BEGIN XXX-----是常见且可读性较好的格式。我们可以使用Bouncy Castle的PEMParser和JcaPEMWriter来处理。将密钥对保存为PEM文件import org.bouncycastle.openssl.jcajce.JcaPEMWriter; import java.io.FileWriter; import java.security.PrivateKey; import java.security.PublicKey; public static void saveKeyToPem(Key key, String filePath) throws IOException { try (JcaPEMWriter pemWriter new JcaPEMWriter(new FileWriter(filePath))) { pemWriter.writeObject(key); pemWriter.flush(); } } // 使用saveKeyToPem(keyPair.getPrivate(), sm2_private.pem); // saveKeyToPem(keyPair.getPublic(), sm2_public.pem);从PEM文件加载密钥import org.bouncycastle.openssl.PEMParser; import org.bouncycastle.openssl.PEMKeyPair; import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; import java.io.FileReader; import java.security.KeyPair; import java.security.PrivateKey; import java.security.PublicKey; public static PrivateKey loadPrivateKeyFromPem(String filePath) throws IOException { try (PEMParser pemParser new PEMParser(new FileReader(filePath))) { Object object pemParser.readObject(); JcaPEMKeyConverter converter new JcaPEMKeyConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME); if (object instanceof PEMKeyPair) { return converter.getPrivateKey(((PEMKeyPair) object).getPrivateKeyInfo()); } else if (object instanceof PrivateKey) { return (PrivateKey) object; } else { throw new IOException(不支持的PEM格式: object.getClass()); } } } public static PublicKey loadPublicKeyFromPem(String filePath) throws IOException { try (PEMParser pemParser new PEMParser(new FileReader(filePath))) { Object object pemParser.readObject(); JcaPEMKeyConverter converter new JcaPEMKeyConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME); if (object instanceof PublicKey) { return (PublicKey) object; } else { // 也可能读到的是SubjectPublicKeyInfo return converter.getPublicKey((SubjectPublicKeyInfo) object); } } }注意事项PEM文件可能包含多种类型如加密的私钥、证书等。上述代码是基础示例生产环境需要更健壮的类型判断和异常处理。私钥文件务必妥善保管建议加密存储。4.3 获取原始公钥点十六进制字符串在与某些硬件设备或特定协议对接时对方可能要求直接提供非压缩公钥的十六进制字符串即04xy。我们可以从ECPublicKey中提取import java.security.interfaces.ECPublicKey; import org.bouncycastle.math.ec.ECPoint; import java.math.BigInteger; public static String getUncompressedPublicKeyHex(PublicKey publicKey) { if (!(publicKey instanceof ECPublicKey)) { throw new IllegalArgumentException(非EC公钥); } ECPublicKey ecPubKey (ECPublicKey) publicKey; // 获取椭圆曲线点 ECPoint point ecPubKey.getW(); // 获取x, y坐标BigInteger类型 BigInteger x point.getAffineXCoord().toBigInteger(); BigInteger y point.getAffineYCoord().toBigInteger(); // 转换为固定长度64字符的十六进制字符串不足补0 String xHex leftPad(x.toString(16), 64); String yHex leftPad(y.toString(16), 64); // 非压缩格式以04开头 return 04 xHex yHex; } private static String leftPad(String str, int size) { StringBuilder sb new StringBuilder(str); while (sb.length() size) { sb.insert(0, 0); } return sb.toString(); }这个64字节128字符十六进制的字符串就是很多国密SDK或文档里提到的“裸公钥”。5. SM2加密与解密实现SM2加密解密过程比RSA稍复杂因为涉及密钥派生。Bouncy Castle的“轻量级API”提供了更直接的接口。5.1 使用SM2Engine进行加密这里我们使用SM2Engine它封装了加密流程。import org.bouncycastle.crypto.engines.SM2Engine; import org.bouncycastle.crypto.params.ECPublicKeyParameters; import org.bouncycastle.crypto.params.ParametersWithRandom; import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey; import java.security.SecureRandom; public static byte[] encrypt(byte[] data, PublicKey publicKey) throws Exception { // 1. 转换公钥为BC内部参数格式 BCECPublicKey bcPubKey (BCECPublicKey) publicKey; ECPublicKeyParameters pubKeyParams new ECPublicKeyParameters( bcPubKey.getQ(), bcPubKey.getParameters() ); // 2. 创建SM2加密引擎使用C1C3C2模式这是国标推荐顺序 SM2Engine engine new SM2Engine(SM2Engine.Mode.C1C3C2); // 3. 初始化引擎加密模式传入公钥和随机数 engine.init(true, new ParametersWithRandom(pubKeyParams, new SecureRandom())); // 4. 执行加密 return engine.processBlock(data, 0, data.length); }关键点解析SM2Engine.Mode.C1C3C2这是加密后数据的排列顺序。C1是临时公钥点C3是SM3得到的摘要C2是实际加密后的密文。这是国标规定的标准顺序。另一种模式是C1C2C3多见于一些旧的实现或国际标准。对接时务必确认双方模式一致否则解密必然失败。ParametersWithRandom加密过程需要随机数来生成临时密钥对这里我们使用密码学安全的SecureRandom。5.2 使用SM2Engine进行解密解密是加密的逆过程需要私钥。import org.bouncycastle.crypto.params.ECPrivateKeyParameters; import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey; public static byte[] decrypt(byte[] sm2CipherText, PrivateKey privateKey) throws Exception { // 1. 转换私钥为BC内部参数格式 BCECPrivateKey bcPrivKey (BCECPrivateKey) privateKey; ECPrivateKeyParameters privKeyParams new ECPrivateKeyParameters( bcPrivKey.getD(), bcPrivKey.getParameters() ); // 2. 创建SM2解密引擎模式必须与加密时一致 SM2Engine engine new SM2Engine(SM2Engine.Mode.C1C3C2); // 3. 初始化引擎解密模式传入私钥 engine.init(false, privKeyParams); // 4. 执行解密 return engine.processBlock(sm2CipherText, 0, sm2CipherText.length); }踩坑实录我曾在与一个硬件加密机对接时耗费了大半天排查解密失败。最后发现硬件加密机默认输出是C1C2C3模式而我的代码使用的是C1C3C2。解决方法要么让对方切换模式要么在解密前自己重组密文顺序。所以“模式”是SM2加解密对接的第一道坎。6. SM2签名与验签实现签名验签是身份认证和完整性保护的核心。SM2的签名算法通常表示为SM3withSM2。6.1 使用标准JCE Signature接口签名最通用的方式是使用JCE的Signature类这样代码风格与RSA签名保持一致。public static byte[] sign(byte[] data, PrivateKey privateKey) throws Exception { // 1. 获取Signature实例指定算法为“SM3withSM2”提供者为BC Signature signature Signature.getInstance(SM3withSM2, BouncyCastleProvider.PROVIDER_NAME); // 2. 初始化签名对象签名模式传入私钥 signature.initSign(privateKey); // 3. 传入待签名数据 signature.update(data); // 4. 生成签名值 return signature.sign(); }这里有一个巨大的坑需要注意Signature.sign()返回的字节数组其编码格式是ASN.1 DER编码的。它并不是简单的r||sr和s拼接而是一个DER序列SEQUENCE里面包含了两个INTEGERr和s。这个格式在Java生态内流通没问题但很多其他平台如某些C库、硬件设备可能要求原始的、固定长度的r||s格式各32字节共64字节。6.2 处理签名值的两种格式因此处理签名时格式转换是常态。import org.bouncycastle.asn1.*; import java.math.BigInteger; /** * 将ASN.1 DER格式的签名转换为裸的 r|s 字节数组 (各32字节共64字节) */ public static byte[] convertDerSignToPlain(byte[] derSign) throws IOException { ASN1Sequence seq ASN1Sequence.getInstance(derSign); BigInteger r ASN1Integer.getInstance(seq.getObjectAt(0)).getValue(); BigInteger s ASN1Integer.getInstance(seq.getObjectAt(1)).getValue(); // 将r和s转换为固定32字节的字节数组 byte[] rBytes to32Bytes(r); byte[] sBytes to32Bytes(s); byte[] plainSign new byte[64]; System.arraycopy(rBytes, 0, plainSign, 0, 32); System.arraycopy(sBytes, 0, plainSign, 32, 32); return plainSign; } /** * 将裸的 r|s 字节数组转换为ASN.1 DER格式 */ public static byte[] convertPlainSignToDer(byte[] plainSign) throws IOException { if (plainSign.length ! 64) { throw new IllegalArgumentException(Plain signature must be 64 bytes); } byte[] rBytes new byte[32]; byte[] sBytes new byte[32]; System.arraycopy(plainSign, 0, rBytes, 0, 32); System.arraycopy(plainSign, 32, sBytes, 0, 32); BigInteger r new BigInteger(1, rBytes); // 正数 BigInteger s new BigInteger(1, sBytes); ASN1EncodableVector v new ASN1EncodableVector(); v.add(new ASN1Integer(r)); v.add(new ASN1Integer(s)); return new DERSequence(v).getEncoded(); } private static byte[] to32Bytes(BigInteger bi) { byte[] bytes bi.toByteArray(); if (bytes.length 32) { return bytes; } else if (bytes.length 32) { // 如果字节数组长度超过32比如因为符号位取后32位 byte[] result new byte[32]; System.arraycopy(bytes, bytes.length - 32, result, 0, 32); return result; } else { // 如果不足32字节前面补0 byte[] result new byte[32]; System.arraycopy(bytes, 0, result, 32 - bytes.length, bytes.length); return result; } }6.3 验签实现验签同样使用Signature类注意处理不同格式的签名值。public static boolean verify(byte[] data, byte[] signature, PublicKey publicKey) throws Exception { Signature verifier Signature.getInstance(SM3withSM2, BouncyCastleProvider.PROVIDER_NAME); verifier.initVerify(publicKey); verifier.update(data); // 先判断签名格式如果是64字节可能是裸签名需要尝试转换 byte[] sigToVerify signature; if (signature.length 64) { try { // 尝试将裸签名转换为DER格式进行验签 sigToVerify convertPlainSignToDer(signature); } catch (Exception e) { // 转换失败可能不是标准的r|s直接使用原字节验签虽然很可能失败 // 生产环境应有更明确的格式约定和错误处理 } } // 如果是其他长度如70字节假定已经是DER格式 return verifier.verify(sigToVerify); }核心经验在涉及多系统交互的签名验签场景中第一件要做的事就是明确签名值的格式。是ASN.1 DER还是裸的64字节最好在接口文档或协议中白纸黑字写清楚并编写对应的格式转换工具函数这能节省大量联调时间。7. 生产环境进阶考量与性能优化把功能跑通只是第一步要上生产环境还有不少细节需要打磨。7.1 算法提供者优先级与算法名称在注册了多个Provider的复杂环境里为了确保SM2算法一定由BC提供可以在获取实例时显式指定Provider。// 更稳妥的获取方式 KeyPairGenerator kpg KeyPairGenerator.getInstance(EC, BC); Signature signature Signature.getInstance(SM3withSM2, BC); Cipher cipher Cipher.getInstance(SM2, BC); // 注意标准JCE Cipher可能不支持SM2通常用SM2Engine有些情况下算法名称可能是“1.2.156.10197.1.301”SM2的OID或“SM2”。使用“SM3withSM2”和“EC”配合BC Provider是最通用的。7.2 大文件或数据流的处理上述示例都是对内存中的字节数组进行操作。对于大文件不能一次性读入内存。签名/验签Signature.update(byte[] b, int off, int len)方法支持分块更新可以配合BufferedInputStream循环读取文件并更新最后再sign()或verify。加密/解密SM2Engine本身是分组加密但通常用于加密对称密钥如SM4的密钥而非直接加密大文件。标准的“混合加密”模式是用SM2加密一个随机生成的对称密钥如SM4密钥再用这个对称密钥去加密实际的大文件数据。SM2Engine的processBlock方法一次处理一个“块”对于大数据需要自己管理循环调用虽然不常见。更常见的做法是使用Cipher类并指定“SM2”为密钥协商算法但BC的JCE支持度不一直接使用SM2Engine进行密钥加密更可靠。7.3 线程安全与对象复用KeyPairGenerator,Signature,Cipher这些JCE类通常不是线程安全的。在高并发场景下应该避免在多个线程间共享同一个实例。要么每次使用时创建新实例对于轻量操作如签名开销可接受要么使用ThreadLocal为每个线程缓存一个实例。SM2Engine对象在每次init之后用于一次加密或解密操作。操作完成后如果需要再次使用必须重新调用init进行初始化。不建议在并发环境下复用同一个SM2Engine实例。7.4 性能监控与调试在性能关键路径上可以关注密钥生成SM2密钥生成比RSA快很多一般不是瓶颈。加解密速度SM2加密解密速度也优于同等安全强度的RSA。如果发现性能不符合预期检查是否错误地用于加密大量数据应仅用于加密密钥或小块数据。签名验签速度SM2签名很快验签稍慢但整体依然优秀。可以使用Java Microbenchmark Harness (JMH) 进行基准测试对比不同场景下的性能表现。8. 常见问题排查与实战技巧这里汇总了我遇到的一些典型问题及解决方法。8.1 问题速查表问题现象可能原因排查步骤与解决方案NoSuchAlgorithmException: no such algorithm: SM3withSM2 for provider BC1. Bouncy Castle未正确注册。2. 依赖版本冲突或损坏。1. 检查Security.addProvider是否执行且成功。2. 检查BouncyCastleProvider.PROVIDER_NAME常量是否为“BC”。3. 清理Maven本地仓库重新下载依赖。解密失败或验签失败1. 加密/签名与解密/验签使用的**模式C1C3C2/C1C2C3**不一致。2. 公钥私钥不配对。3. 签名值格式DER/裸64字节不匹配。4. 待处理数据在传输过程中被篡改或编码如Base64处理有误。1.首先确认模式这是最高频问题。2. 重新生成密钥对测试排除密钥问题。3. 对签名值打印长度64字节尝试转换DER70字节尝试解析DER。4. 确保加解密/签名验签前后数据完全一致注意编码解码。与其他系统如OpenSSL、硬件加密机对接失败1. 椭圆曲线参数不一致虽都是sm2p256v1但实现微调可能有差异。2. 公钥格式不一致是否带04头是否X.509封装。3. 签名/加密结果格式不一致。1. 使用双方都认可的测试向量Test Vector进行交叉验证。2. 对比公钥的十六进制表示确认是否都是04开头的非压缩格式。3. 编写小规模、分步骤的测试程序逐个环节密钥生成-导出-加密-结果对比输出。InvalidKeyException传入的Key对象类型错误或不是有效的SM2密钥。确认publicKey是ECPublicKey实例privateKey是ECPrivateKey实例。检查密钥是否来自正确的PEM文件或生成过程。性能低下错误地用SM2加密大数据。回顾非对称加密原理SM2应用于加密会话密钥或小数据如几十到几百字节。大数据应使用SM4等对称算法加密。8.2 调试技巧开启Bouncy Castle的详细日志Bouncy Castle本身日志不多但可以通过JVM参数开启Java安全相关的日志有助于诊断Provider注册、算法查找等问题。-Djava.security.debugprovider在启动命令中加入上述参数可以在控制台看到Provider加载和算法查找的详细信息。8.3 一个完整的工具类示例骨架最后给出一个整合了上述关键功能的工具类骨架你可以在此基础上进行扩展和封装。import org.bouncycastle.asn1.*; import org.bouncycastle.crypto.engines.SM2Engine; import org.bouncycastle.crypto.params.*; import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey; import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey; import org.bouncycastle.jce.provider.BouncyCastleProvider; import javax.crypto.Cipher; import java.io.*; import java.math.BigInteger; import java.security.*; import java.security.spec.ECGenParameterSpec; public class Sm2CryptoUtil { public static final String PROVIDER_NAME BouncyCastleProvider.PROVIDER_NAME; public static final String SM2_CURVE_NAME sm2p256v1; public static final SM2Engine.Mode DEFAULT_MODE SM2Engine.Mode.C1C3C2; static { if (Security.getProvider(PROVIDER_NAME) null) { Security.addProvider(new BouncyCastleProvider()); } } // --- 密钥生成 --- public static KeyPair generateKeyPair() throws Exception { /* 见上文 */ } // --- 密钥PEM格式IO --- public static void saveKeyToPem(Key key, String path) throws IOException { /* 见上文 */ } public static PublicKey loadPublicKeyFromPem(String path) throws IOException { /* 见上文 */ } public static PrivateKey loadPrivateKeyFromPem(String path) throws IOException { /* 见上文 */ } // --- 公钥格式转换 --- public static String getUncompressedPubKeyHex(PublicKey publicKey) { /* 见上文 */ } // --- 加密解密 (使用SM2Engine) --- public static byte[] encrypt(byte[] data, PublicKey publicKey, SM2Engine.Mode mode) throws Exception { BCECPublicKey bcPubKey (BCECPublicKey) publicKey; ECPublicKeyParameters pubKeyParams new ECPublicKeyParameters( bcPubKey.getQ(), bcPubKey.getParameters() ); SM2Engine engine new SM2Engine(mode); engine.init(true, new ParametersWithRandom(pubKeyParams, new SecureRandom())); return engine.processBlock(data, 0, data.length); } public static byte[] decrypt(byte[] cipherText, PrivateKey privateKey, SM2Engine.Mode mode) throws Exception { BCECPrivateKey bcPrivKey (BCECPrivateKey) privateKey; ECPrivateKeyParameters privKeyParams new ECPrivateKeyParameters( bcPrivKey.getD(), bcPrivKey.getParameters() ); SM2Engine engine new SM2Engine(mode); engine.init(false, privKeyParams); return engine.processBlock(cipherText, 0, cipherText.length); } // --- 签名验签 (使用JCE Signature) --- public static byte[] sign(byte[] data, PrivateKey privateKey) throws Exception { Signature signature Signature.getInstance(SM3withSM2, PROVIDER_NAME); signature.initSign(privateKey); signature.update(data); return signature.sign(); // DER格式 } public static boolean verify(byte[] data, byte[] signature, PublicKey publicKey) throws Exception { Signature verifier Signature.getInstance(SM3withSM2, PROVIDER_NAME); verifier.initVerify(publicKey); verifier.update(data); return verifier.verify(signature); } // --- 签名格式转换 --- public static byte[] convertDerSignToPlain(byte[] derSign) throws IOException { /* 见上文 */ } public static byte[] convertPlainSignToDer(byte[] plainSign) throws IOException { /* 见上文 */ } // --- 便捷方法使用默认模式 --- public static byte[] encrypt(byte[] data, PublicKey publicKey) throws Exception { return encrypt(data, publicKey, DEFAULT_MODE); } public static byte[] decrypt(byte[] cipherText, PrivateKey privateKey) throws Exception { return decrypt(cipherText, privateKey, DEFAULT_MODE); } }这个工具类覆盖了SM2在Java中的核心操作。在实际项目中你可能还需要将其与Spring Boot的ConfigurationProperties结合进行密钥路径配置或者集成到你的服务框架中。记住密码学无小事尤其是在生产环境务必做好密钥安全管理、操作日志记录和充分的异常处理。