Java实现加密(六)国密SM2算法
* 博客文章部分截图及内容来自于学习的书本及相应培训课程以及网络其他博客,仅做学习讨论之用,不做商业用途。
* 如有侵权,马上联系我,我立马删除对应链接。
* @author Alan
* @Email no008@foxmail.com
正文
原文:https://blog.csdn.net/qq_33204709/category_12150019.html
Java实现 GitHub: https://github.com/ZZMarquis/gmhelper
一、SM2 简介
1.1 概述
SM2 算法是基于 ECC(Elliptic Curve Cryptography)椭圆曲线密码 的 非对称加密 算法,其密钥长度为 256bit。该算法由 国家密码管理局 于 2010年12月17号发布
国密算法,即 国家商用密码算法。是由 国家密码管理局 认定和公布的密码算法标准及其应用规范,其中部分密码算法已经称为国际标准。如:SM系列 密码,SM 代表 商密,即商业密码,是指用于商业的、不涉及国家秘密的密码技术。
国密算法包括:SM1(SCB2)、SM2、SM3、SM4、SM7、SM9,以及 ZUC(祖冲之密码)等。
其中:
1) SM1、SM4、SM7、ZUC(祖冲之密码)属于 对称算法;
2) SM2、SM9 属于 非对称算法。
3) SM3 属于 杂凑算法。
1.2 国密与国际密的对应关系
加密方式 国密 国际密
对称加密 SM1 AES(Advanced Encryption Standard)
非对称加密 SM2 RSA(Ron Rivest、Adi Shamir、Leonard Adleman)三人姓氏首字母拼在一起
摘要算法(杂凑) SM3 MD5(Message-Digest Algorithm)
SHA系列(Secure Hash Algorithm)
对称加密 SM4 DES(Data Encryption Standard)
1.3 优势
SM2 算法作为一种自主创新的密码算法,具有以下优势:
安全性高: 基于 椭圆曲线离散对数难题,安全性比较高,能够有效地防止黑客攻击。
效率高: 具有较高地运算效率,能够满足大量数据加密、解密和数字签名的需求。
灵活性好: 支持多种密钥长度,可根据实际需求灵活选择密钥长度,适用于不同的应用场景。
自主创新: SM2算法是我国自主创新的密码算法,具有独立的只是产权,能够保障国家关键信息系统的信息安全。
1.4 ECC加密算法 vs RSA加密算法
SM2算法 是基于 ECC 椭圆曲线算法 实现的,采用 256位 密钥长度,它的安全强度相对较高,在工程应用中难以实现,破译或求解难度基本上是指数级的。因此,SM2 算法可以用较少的计算能力提供比 RSA算法 更高的安全强度,而所需的密钥长度却远比 RSA算法低。
对比项目 ECC加密算法 RSA加密算法
密钥长度 246位 2048位
CPU占用 较少 较高
内存占用 较少 较高
网络小号 较低 较高
加密效率 较高 一般
破解难度 具有数据特性,破解难度大 相对ECC理论上容易些
抗攻击性 强 一般
可扩展性 强 一般
兼容范围 支持新版浏览器和操作系统,单存在少数不支持平台,例如:cPanel 广泛支持
二、SM2 应用场景
2.1 数据加密
在非对称加密算法中,可对外公布的密钥称为 公钥,只有持有者所知的密钥称为 私钥。发送者使用接收者的公钥来加密信息,接收者用自己的私钥解密和读取该信息。
使用 SM2 非对称加密算法加解密数据的过程:
2.2 密钥协商
利用 SM2 算法进行密钥协商的过程:
1) 会话双方生成自己的私钥(随机数);
2) 会话双方由私钥、ECC椭圆曲线参数 G 各自计算出公钥;
3) 会话双方将自己的公钥传递给对方,传递过程公开;
(由于椭圆曲线的计算复杂性高,破解难度大,因此攻击者难以通过公钥和椭圆曲线参数 G 反推出私钥)
4) 双方将自己的 私钥 与对方的 公钥 进行运算,最终得到相同的会话密钥,该会话密钥可作为共享密钥用于对称加密(例如:SM4算法)通信。
2.3 数字签名
数字签名 是一种用于 验证信息完整性、真实性和来源的技术手段。它通常用于确保数据在传输或存储过程中没有被篡改,并且可以追溯到特定的发送方。
发送方使用 自己的私钥 对信息进行 加密,生成数字签名。
接收方使用 发送方的公钥 对签名进行 解密 和 验证,以验证消息的完整性和真实性。
2.4 密文顺序
SM2 加密算法中包含两种密文顺序:
C1C2C3:这是 老版 的 SM2 加密密文顺序。在这种情况下,密文由三部分组成,依次是:C1、C2、C3。
C1C3C2:这是 新版 的 SM2 加密密文顺序。在这种情况下,密文由三部分组成,依次是:C1、C3、C2。
不同的密文顺序,加密出来的内容是不一样的,因此 在使用 SM2 算法时,加密和解密需要保证密文顺序一致,才能正常进行加解密。
三、Java 实现 SM2 的两种方式
3.1 Maven 依赖
在 bouncycastle - 1.57 版本之后,加入了对我国的 SM2、SM3、SM4算法的支持。
<!-- SM2加密 --> <dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcprov-jdk15on</artifactId> <version>1.64</version> </dependency>
3.2 实现方式一
1)SM2Utils.java
import lombok.extern.slf4j.Slf4j; import org.bouncycastle.crypto.CipherParameters; import org.bouncycastle.crypto.engines.SM2Engine; import org.bouncycastle.crypto.params.ECPrivateKeyParameters; import org.bouncycastle.crypto.params.ECPublicKeyParameters; import org.bouncycastle.crypto.params.ParametersWithRandom; import org.bouncycastle.crypto.signers.SM2Signer; import org.bouncycastle.jcajce.provider.asymmetric.util.ECUtil; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.util.encoders.Base64; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.security.*; import java.security.spec.*; /** * 国密SM2算法工具类 **/ @Slf4j public class SM2Utils { private static final Charset CHARSETS = StandardCharsets.UTF_8; private static final SM2Engine.Mode DIGEST = SM2Engine.Mode.C1C3C2; /** * 生成公钥和私钥 */ public static KeyPair generateSm2KeyPair() { //使用标准名称创建EC参数生成的参数规范 final ECGenParameterSpec sm2Spec = new ECGenParameterSpec("sm2p256v1"); // 获取一个椭圆曲线类型的密钥对生成器 final KeyPairGenerator kpg; try { kpg = KeyPairGenerator.getInstance("EC", new BouncyCastleProvider()); // 使用SM2算法域参数集初始化密钥生成器(默认使用以最高优先级安装的提供者的 SecureRandom 的实现作为随机源) // kpg.initialize(sm2Spec); // 使用SM2的算法域参数集和指定的随机源初始化密钥生成器 kpg.initialize(sm2Spec, new SecureRandom()); // 通过密钥生成器生成密钥对 return kpg.generateKeyPair(); } catch (Exception e) { log.error(">>>>>>>>>>【ERROR】SM2密钥对生成失败,原因:{}", e.getMessage(), e); throw new RuntimeException("SM2密钥对生成失败"); } } /** * 私钥转换为 {@link ECPrivateKeyParameters} * * @param key key * @return * @throws InvalidKeyException */ public static ECPrivateKeyParameters privateKeyToParams(byte[] key) throws InvalidKeyException, InvalidKeySpecException, NoSuchAlgorithmException { if (key == null) { throw new RuntimeException("key must be not null !"); } PrivateKey privateKey = generatePrivateKey(key); return (ECPrivateKeyParameters) ECUtil.generatePrivateKeyParameter(privateKey); } /** * 生成私钥 * * @param key key * @return */ public static PrivateKey generatePrivateKey(byte[] key) throws NoSuchAlgorithmException, InvalidKeySpecException { if (key == null) { throw new RuntimeException("key must be not null !"); } KeySpec keySpec = new PKCS8EncodedKeySpec(key); return getKeyFactory().generatePrivate(keySpec); } /** * 公钥转换为 {@link ECPublicKeyParameters} * * @param key key * @return * @throws InvalidKeyException */ public static ECPublicKeyParameters publicKeyToParams(byte[] key) throws InvalidKeyException, InvalidKeySpecException, NoSuchAlgorithmException { if (key == null) { throw new RuntimeException("key must be not null !"); } // 生成公钥 KeySpec keySpec = new X509EncodedKeySpec(key); PublicKey publicKey = getKeyFactory().generatePublic(keySpec); return (ECPublicKeyParameters) ECUtil.generatePublicKeyParameter(publicKey); } /** * 获取{@link KeyFactory} * * @return {@link KeyFactory} */ private static KeyFactory getKeyFactory() throws NoSuchAlgorithmException { final Provider provider = new BouncyCastleProvider(); return KeyFactory.getInstance("EC", provider); } /** * 加密 * * @param dataStr 数据 * @param publicKey 公钥 * @return 加密之后的数据 */ public static String encrypt(String dataStr, String publicKey) { try { return Base64.toBase64String(encrypt(dataStr.getBytes(StandardCharsets.UTF_8), Base64.decode(publicKey))); } catch (Exception e) { throw new RuntimeException("参数加密异常"); } } public static byte[] encrypt(byte[] data, byte[] publicKey) throws Exception { CipherParameters pubKeyParameters = new ParametersWithRandom(publicKeyToParams(publicKey)); SM2Engine engine = new SM2Engine(DIGEST); engine.init(true, pubKeyParameters); return engine.processBlock(data, 0, data.length); } /** * 解密 * * @param base64Data 数据 * @param base64PrivateKey 私钥 * @return 解密之后的数据 */ public static String decrypt(String base64Data, String base64PrivateKey) throws Exception { byte[] data = Base64.decode(base64Data); byte[] privateKey = Base64.decode(base64PrivateKey); return new String(decrypt(data, privateKey), CHARSETS); } public static byte[] decrypt(byte[] data, byte[] privateKey) throws Exception { CipherParameters privateKeyParameters = privateKeyToParams(privateKey); SM2Engine engine = new SM2Engine(DIGEST); engine.init(false, privateKeyParameters); return engine.processBlock(data, 0, data.length); } /** * 签名 * * @param data 数据 * @return 签名 */ public static byte[] sign(byte[] data, byte[] privateKey) throws Exception { SM2Signer signer = new SM2Signer(); CipherParameters param = new ParametersWithRandom(privateKeyToParams(privateKey)); signer.init(true, param); signer.update(data, 0, data.length); return signer.generateSignature(); } /** * 用公钥检验数字签名的合法性 * * @param data 数据 * @param sign 签名 * @param publicKey 公钥 * @return 是否验证通过 */ public static boolean verify(byte[] data, byte[] sign, byte[] publicKey) throws Exception { SM2Signer signer = new SM2Signer(); CipherParameters param = publicKeyToParams(publicKey); signer.init(false, param); signer.update(data, 0, data.length); return signer.verifySignature(sign); } }
2)SignatureSM2Util.java
import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson.serializer.SerializerFeature; import lombok.extern.slf4j.Slf4j; import org.bouncycastle.util.encoders.Base64; /** * 国密SM2工具类 */ @Slf4j public class SignatureSM2Util { private SignatureSM2Util() { } private final static String CHARSETTING = "utf-8"; public static final SerializerFeature SERIALIZER_FEATURE = SerializerFeature.MapSortField; public static final String COMMON_SIGNATURE_NAME = "signature"; /** * 签名 * @param plain 数据 * @param privateKey 私钥 */ public static String sign(String plain, String privateKey) throws Exception { if (isBlank(plain) || isBlank(privateKey)) { return null; } byte[] buffer = Base64.decode(privateKey); return Base64.toBase64String(SM2Utils.sign(plain.getBytes(CHARSETTING), buffer)); } /** * 验签 * @param plain 数据 * @param sign 签名 * @param publicKey 公钥 */ public static boolean verify(String plain, String sign, String publicKey) throws Exception { if (isBlank(plain) || isBlank(sign) || isBlank(publicKey)) { return false; } byte[] buffer = Base64.decode(publicKey); return SM2Utils.verify(plain.getBytes(CHARSETTING), Base64.decode(sign), buffer); } public static JSONObject formatAndSign(Object request, String privateKey, String signParamName) throws Exception { return formatAndSign(request, privateKey, signParamName, SERIALIZER_FEATURE); } public static String formatAndSignString(Object request, String privateKey) { JSONObject reqJsonObj = JSON.parseObject(JSONObject.toJSONString(request)); if (reqJsonObj.containsKey(COMMON_SIGNATURE_NAME)) { reqJsonObj.remove(COMMON_SIGNATURE_NAME); } String preSign = getJsonString(reqJsonObj, SERIALIZER_FEATURE); String signature = null; try { signature = sign(preSign, privateKey); } catch (Exception e) { log.error("添加签名异常了,req:{},error:{}", request, e); } return signature; } public static JSONObject formatAndSign(Object request, String privateKey, String signParamName, SerializerFeature serializerFeature) throws Exception { JSONObject reqJsonObj = JSON.parseObject(JSONObject.toJSONString(request)); if (reqJsonObj.containsKey(signParamName)) { reqJsonObj.remove(signParamName); } String preSign = getJsonString(reqJsonObj, serializerFeature); String signature = sign(preSign, privateKey); reqJsonObj.put(signParamName, signature); return reqJsonObj; } private static String getJsonString(JSONObject request, SerializerFeature serializerFeature) { return JSON.toJSONString(request, serializerFeature); } public static JSONObject formatAndSign(JSONObject request, String privateKey, SerializerFeature serializerFeature) throws Exception { return formatAndSign(request, privateKey, COMMON_SIGNATURE_NAME, serializerFeature); } public static JSONObject formatAndSign(Object request, String privateKey) { try { return formatAndSign(request, privateKey, COMMON_SIGNATURE_NAME); } catch (Exception e) { log.error("添加签名异常了,req:{},error:{}", request, e); return JSON.parseObject(JSON.toJSONString(request)); } } public static boolean formatAndVerify(JSONObject request, String publicKey, String signParamName) throws Exception { String sign = (String) request.remove(signParamName); String preSign = getJsonString(request, SERIALIZER_FEATURE); return verify(preSign, sign, publicKey); } public static boolean formatAndVerify(JSONObject request, String publicKey) throws Exception { return formatAndVerify(request, publicKey, COMMON_SIGNATURE_NAME); } private static boolean isBlank(CharSequence cs) { int strLen; if (cs != null && (strLen = cs.length()) != 0) { for(int i = 0; i < strLen; ++i) { if (!Character.isWhitespace(cs.charAt(i))) { return false; } } return true; } else { return true; } } }
3)Param.java
import lombok.Data; /** * 测试入参 */ @Data public class Param { /** * 应用ID */ private String appId; /** * 任务ID */ private String msisdn; /** * 事务ID */ private String transactionId; /** * 签名校验 */ private String signature; }
4)测试示例
SM2TestMain.java
import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import org.bouncycastle.util.encoders.Base64; import java.security.KeyPair; /** * 国密SM2测试类 */ public class SM2TestMain { public static void main(String[] args) { Param param = new Param(); param.setSignature(null); param.setAppId("111"); param.setMsisdn("222"); param.setTransactionId("123123123"); // 生成公私钥对示例 KeyPair keyPair = SM2Utils.generateSm2KeyPair(); String privateKey = Base64.toBase64String(keyPair.getPrivate().getEncoded()); String publicKey = Base64.toBase64String(keyPair.getPublic().getEncoded()); System.out.println("SM2 key pair:"); System.out.println("--private key--"); System.out.println(privateKey); System.out.println("--public key--"); System.out.println(publicKey); // SM2密钥对示例: // --private key-- // MIGTAgEAMBMGByqGSM49AgEGCCqBHM9VAYItBHkwdwIBAQQgoD8nD0oto4kjAYcGoBvVwX1qWFeOi+Gm2Zl679VKJzugCgYIKoEcz1UBgi2hRANCAAQiZHQqz9TtkP0R1nVnAwrkuvBHu8j3UvS7O9crUhgG6B2OSplEclY0CQtciSFeeDMq4ZboBnAlYTj0Npi+QVy0 // --public key-- // MFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAEImR0Ks/U7ZD9EdZ1ZwMK5LrwR7vI91L0uzvXK1IYBugdjkqZRHJWNAkLXIkhXngzKuGW6AZwJWE49DaYvkFctA== // 加密示例 String dataToEncrypt = "123"; String encryptData = SM2Utils.encrypt(dataToEncrypt, publicKey); System.out.println("明文内容:123"); System.out.println("加密内容:" + encryptData); // 解密示例 try { String decryptData = SM2Utils.decrypt(encryptData, privateKey); System.out.println("解密内容:" + decryptData); } catch (Exception e) { throw new RuntimeException(e); } // 签名示例 String data = JSON.toJSONString(param); JSONObject toSign = JSONObject.parseObject(data); JSONObject result; try { // 业务方私钥 result = SignatureSM2Util.formatAndSign(toSign, privateKey); } catch (Exception e) { e.printStackTrace(); throw e; } System.out.println("签名内容:" + result); // 验签示例 try { // 业务方公钥 boolean b = SignatureSM2Util.formatAndVerify(result, publicKey); System.out.println("验签结果:" + b); } catch (Exception e) { throw new RuntimeException(e); } } }
5)测试结果
SM2 key pair:
--private key--
MIGTAgEAMBMGByqGSM49AgEGCCqBHM9VAYItBHkwdwIBAQQgeZXmhOiiBtMhUIEhfP5rkasKrhP/ljSq1F/cd1596hOgCgYIKoEcz1UBgi2hRANCAAT9KLZa/dvBNRjdrfesiKRUfKdYnHOxD5HNIyfNMa8O6UtxtbAJsj4FeYzaEES3tr+rNwVmIAaMVzZJEvQpZtZY
--public key--
MFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAE/Si2Wv3bwTUY3a33rIikVHynWJxzsQ+RzSMnzTGvDulLcbWwCbI+BXmM2hBEt7a/qzcFZiAGjFc2SRL0KWbWWA==
明文内容:123
加密内容:BEBGZCSPGYVZoFHLcfa2FPCSK+Pt76esiaWzwjexOvEiEb3N39lccHct+Rh6yxGMFYRWdlGGg9hbTRJFr4TB5DeUg6YcslM10nxVYBRVdzAPrqt/2c0pRPKpvZ1glyhNWBzAMg==
解密内容:123
签名内容:{"signature":"MEQCIAb8zYzZc+6jxVAZgjsBn8gg5U14w92p7UMP436+Gdc/AiBcw5l0d57iHdAjQry08gd7+BDEKeKUVsmC3+RRG5ZP4w==","appId":"111","msisdn":"222","transactionId":"123123123"}
验签结果:true
3.3 实现方式二
1)SM2Utils.java
import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.bouncycastle.asn1.ASN1ObjectIdentifier; import org.bouncycastle.asn1.gm.GMNamedCurves; import org.bouncycastle.asn1.gm.GMObjectIdentifiers; import org.bouncycastle.asn1.x9.X9ECParameters; import org.bouncycastle.crypto.engines.SM2Engine; import org.bouncycastle.crypto.params.ECNamedDomainParameters; import org.bouncycastle.crypto.params.ECPrivateKeyParameters; import org.bouncycastle.crypto.params.ECPublicKeyParameters; import org.bouncycastle.crypto.params.ParametersWithRandom; import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey; import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey; import org.bouncycastle.jcajce.provider.asymmetric.ec.KeyPairGeneratorSpi; import org.bouncycastle.jcajce.provider.asymmetric.x509.CertificateFactory; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.jce.spec.ECParameterSpec; import org.bouncycastle.jce.spec.ECPrivateKeySpec; import org.bouncycastle.jce.spec.ECPublicKeySpec; import org.bouncycastle.math.ec.ECPoint; import org.bouncycastle.util.encoders.Hex; import java.io.ByteArrayInputStream; import java.math.BigInteger; import java.nio.charset.StandardCharsets; import java.security.*; import java.security.cert.X509Certificate; import java.util.Base64; import static java.util.Objects.isNull; @Slf4j public class SM2Utils { private static final String EC = "EC"; private static final Base64.Encoder BASE64_ENCODER = Base64.getEncoder(); private static final Base64.Decoder BASE64_DECODER = Base64.getDecoder(); private static final BouncyCastleProvider PROVIDER = new BouncyCastleProvider(); static { if (isNull(Security.getProvider(BouncyCastleProvider.PROVIDER_NAME))) { Security.addProvider(PROVIDER); } } // region generateKeyPair /** * 获取sm2密钥对 * BC库使用的公钥=64个字节+1个字节(04标志位),BC库使用的私钥=32个字节 * SM2秘钥的组成部分有 私钥D 、公钥X 、 公钥Y , 他们都可以用长度为64的16进制的HEX串表示, * <br/>SM2公钥并不是直接由X+Y表示 , 而是额外添加了一个头 * * @return */ public static SM2KeyPair<byte[], BigInteger> genKeyPair() { return genKeyPair(false); } /** * 获取sm2密钥对 * BC库使用的公钥=64个字节+1个字节(04标志位),BC库使用的私钥=32个字节 * SM2秘钥的组成部分有 私钥D 、公钥X 、 公钥Y , 他们都可以用长度为64的16进制的HEX串表示, * <br/>SM2公钥并不是直接由X+Y表示 , 而是额外添加了一个头,当启用压缩时:公钥=有头+公钥X ,即省略了公钥Y的部分 * * @param compressed 是否压缩公钥(加密解密都使用BC库才能使用压缩) * @return */ @SneakyThrows public static SM2KeyPair<byte[], BigInteger> genKeyPair(boolean compressed) { //1.创建密钥生成器 KeyPairGeneratorSpi.EC spi = new KeyPairGeneratorSpi.EC(); //获取一条SM2曲线参数 X9ECParameters parameters = GMNamedCurves.getByOID(GMObjectIdentifiers.sm2p256v1); //构造spec参数 ECParameterSpec parameterSpec = new ECParameterSpec(parameters.getCurve(), parameters.getG(), parameters.getN()); // SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG", "SUN"); SecureRandom secureRandom = new SecureRandom(); //2.初始化生成器,带上随机数 spi.initialize(parameterSpec, secureRandom); //3.生成密钥对 KeyPair asymmetricCipherKeyPair = spi.generateKeyPair(); // 把公钥放入map中,默认压缩公钥 // 公钥前面的02或者03表示是压缩公钥,04表示未压缩公钥,04的时候,可以去掉前面的04 BCECPublicKey publicKeyParameters = (BCECPublicKey) asymmetricCipherKeyPair.getPublic(); ECPoint ecPoint = publicKeyParameters.getQ(); byte[] publicKey = ecPoint.getEncoded(compressed); // 把私钥放入map中 BCECPrivateKey privateKeyParameters = (BCECPrivateKey) asymmetricCipherKeyPair.getPrivate(); BigInteger intPrivateKey = privateKeyParameters.getD(); return new SM2KeyPair<>(publicKey, intPrivateKey); } public static SM2KeyPair<String, String> genKeyPairAsHex() { return genKeyPairAsHex(false); } public static SM2KeyPair<String, String> genKeyPairAsHex(boolean compressed) { final SM2KeyPair<byte[], BigInteger> pair = genKeyPair(compressed); return new SM2KeyPair<>( Hex.toHexString(pair.getPublic()), pair.getPrivate().toString(16) ); } public static SM2KeyPair<String, String> genKeyPairAsBase64() { return genKeyPairAsBase64(false); } public static SM2KeyPair<String, String> genKeyPairAsBase64(boolean compressed) { final SM2KeyPair<byte[], BigInteger> pair = genKeyPair(compressed); return new SM2KeyPair<>( BASE64_ENCODER.encodeToString(pair.getPublic()), BASE64_ENCODER.encodeToString(pair.getPrivate().toByteArray()) ); } // endregion generateKeyPair // region encrypt /** * SM2加密算法 * * @param publicKey 公钥 * @param data 待加密的数据 * @return 密文,BC库产生的密文带由04标识符,与非BC库对接时需要去掉开头的04 */ public static byte[] encrypt(byte[] publicKey, byte[] data) { // 按国密排序标准加密 return encrypt(publicKey, data, SM2Engine.Mode.C1C3C2); } /** * SM2加密算法 * * @param publicKey 公钥 * @param data 待加密的数据 * @param mode 密文排列方式 * @return 密文,BC库产生的密文带由04标识符,与非BC库对接时需要去掉开头的04 */ @SneakyThrows public static byte[] encrypt(byte[] publicKey, byte[] data, SM2Engine.Mode mode) { final ASN1ObjectIdentifier sm2p256v1 = GMObjectIdentifiers.sm2p256v1; // 获取一条SM2曲线参数 X9ECParameters parameters = GMNamedCurves.getByOID(sm2p256v1); // 构造ECC算法参数,曲线方程、椭圆曲线G点、大整数N ECNamedDomainParameters namedDomainParameters = new ECNamedDomainParameters( sm2p256v1, parameters.getCurve(), parameters.getG(), parameters.getN()); //提取公钥点 ECPoint pukPoint = parameters.getCurve().decodePoint(publicKey); // 公钥前面的02或者03表示是压缩公钥,04表示未压缩公钥, 04的时候,可以去掉前面的04 ECPublicKeyParameters publicKeyParameters = new ECPublicKeyParameters(pukPoint, namedDomainParameters); SM2Engine sm2Engine = new SM2Engine(mode); SecureRandom secureRandom = new SecureRandom(); // 设置sm2为加密模式 sm2Engine.init(true, new ParametersWithRandom(publicKeyParameters, secureRandom)); final byte[] encrypt = sm2Engine.processBlock(data, 0, data.length); // if (encrypt[0] == 0x04) { // return Arrays.copyOfRange(encrypt, 1, encrypt.length); // } return encrypt; } public static String encryptHex(String publicKey, String data) { return encryptHex(publicKey, data, SM2Engine.Mode.C1C3C2); } public static String encryptHex(String publicKey, String data, SM2Engine.Mode mode) { final byte[] key = Hex.decode(publicKey); byte[] bytes = data.getBytes(StandardCharsets.UTF_8); final byte[] encrypt = encrypt(key, bytes, mode); return Hex.toHexString(encrypt); } public static String encryptBase64(String publicKey, String data) { return encryptBase64(publicKey, data, SM2Engine.Mode.C1C3C2); } public static String encryptBase64(String publicKey, String data, SM2Engine.Mode mode) { final byte[] key = BASE64_DECODER.decode(publicKey); byte[] bytes = data.getBytes(StandardCharsets.UTF_8); final byte[] encrypt = encrypt(key, bytes, mode); return BASE64_ENCODER.encodeToString(encrypt); } // endregion encrypt // region decrypt /** * SM2解密算法 * * @param privateKey 私钥 * @param cipherData 密文数据 * @return */ public static byte[] decrypt(BigInteger privateKey, byte[] cipherData) { // 按国密排序标准解密 return decrypt(privateKey, cipherData, SM2Engine.Mode.C1C3C2); } /** * SM2解密算法 * * @param privateKey 私钥 * @param cipherData 密文数据 * @param mode 密文排列方式 * @return */ @SneakyThrows public static byte[] decrypt(BigInteger privateKey, byte[] cipherData, SM2Engine.Mode mode) { final ASN1ObjectIdentifier sm2p256v1 = GMObjectIdentifiers.sm2p256v1; //获取一条SM2曲线参数 X9ECParameters parameters = GMNamedCurves.getByOID(sm2p256v1); // 构造ECC算法参数,曲线方程、椭圆曲线G点、大整数N ECNamedDomainParameters namedDomainParameters = new ECNamedDomainParameters( sm2p256v1, parameters.getCurve(), parameters.getG(), parameters.getN()); ECPrivateKeyParameters privateKeyParameters = new ECPrivateKeyParameters(privateKey, namedDomainParameters); SM2Engine sm2Engine = new SM2Engine(mode); // 设置sm2为解密模式 sm2Engine.init(false, privateKeyParameters); // 使用BC库加解密时密文以04开头,传入的密文前面没有04则补上 if (cipherData[0] == 0x04) { return sm2Engine.processBlock(cipherData, 0, cipherData.length); } else { byte[] bytes = new byte[cipherData.length + 1]; bytes[0] = 0x04; System.arraycopy(cipherData, 0, bytes, 1, cipherData.length); return sm2Engine.processBlock(bytes, 0, bytes.length); } } public static String decryptHex(String privateKey, String cipherData) { return decryptHex(privateKey, cipherData, SM2Engine.Mode.C1C3C2); } public static String decryptHex(String privateKey, String cipherData, SM2Engine.Mode mode) { final BigInteger key = new BigInteger(privateKey, 16); final byte[] decrypt = decrypt(key, Hex.decode(cipherData), mode); return new String(decrypt, StandardCharsets.UTF_8); } public static String decryptBase64(String privateKey, String cipherData) { return decryptBase64(privateKey, cipherData, SM2Engine.Mode.C1C3C2); } public static String decryptBase64(String privateKey, String cipherData, SM2Engine.Mode mode) { final BigInteger key = new BigInteger(BASE64_DECODER.decode(privateKey)); final byte[] decrypt = decrypt(key, BASE64_DECODER.decode(cipherData), mode); return new String(decrypt, StandardCharsets.UTF_8); } // endregion decrypt // region sign & cert /** * 签名 * * @param plainText 待签名文本 * @param privateKey 私钥 * @return * @throws GeneralSecurityException */ public static String sign(String plainText, BigInteger privateKey) throws GeneralSecurityException { X9ECParameters parameters = GMNamedCurves.getByOID(GMObjectIdentifiers.sm2p256v1); ECParameterSpec parameterSpec = new ECParameterSpec(parameters.getCurve(), parameters.getG(), parameters.getN()); ECPrivateKeySpec privateKeySpec = new ECPrivateKeySpec(privateKey, parameterSpec); PrivateKey bcecPrivateKey = new BCECPrivateKey(EC, privateKeySpec, BouncyCastleProvider.CONFIGURATION); // 创建签名对象 Signature signature = Signature.getInstance(GMObjectIdentifiers.sm2sign_with_sm3.toString(), PROVIDER); // 初始化为签名状态 signature.initSign(bcecPrivateKey); // 传入签名字节 signature.update(plainText.getBytes(StandardCharsets.UTF_8)); // 签名 return BASE64_ENCODER.encodeToString(signature.sign()); } /** * 验签 * * @param plainText 待签名文本 * @param signText * @param publicKey 公钥 * @return * @throws GeneralSecurityException */ public static boolean verify(String plainText, String signText, byte[] publicKey) throws GeneralSecurityException { X9ECParameters parameters = GMNamedCurves.getByOID(GMObjectIdentifiers.sm2p256v1); ECParameterSpec parameterSpec = new ECParameterSpec(parameters.getCurve(), parameters.getG(), parameters.getN()); ECPoint ecPoint = parameters.getCurve().decodePoint(publicKey); ECPublicKeySpec publicKeySpec = new ECPublicKeySpec(ecPoint, parameterSpec); PublicKey bcecPublicKey = new BCECPublicKey(EC, publicKeySpec, BouncyCastleProvider.CONFIGURATION); // 创建签名对象 Signature signature = Signature.getInstance(GMObjectIdentifiers.sm2sign_with_sm3.toString(), PROVIDER); // 初始化为验签状态 signature.initVerify(bcecPublicKey); signature.update(plainText.getBytes(StandardCharsets.UTF_8)); return signature.verify(BASE64_DECODER.decode(signText)); } /** * 证书验签 * * @param certText 证书串 * @param plainText 签名原文 * @param signText 签名产生签名值 此处的签名值实际上就是 R和S的sequence * @return * @throws GeneralSecurityException */ public static boolean certVerify(String certText, String plainText, String signText) throws GeneralSecurityException { // 解析证书 CertificateFactory factory = new CertificateFactory(); X509Certificate certificate = (X509Certificate) factory.engineGenerateCertificate( new ByteArrayInputStream(BASE64_DECODER.decode(certText))); // 验证签名 Signature signature = Signature.getInstance(certificate.getSigAlgName(), PROVIDER); signature.initVerify(certificate); signature.update(plainText.getBytes(StandardCharsets.UTF_8)); return signature.verify(BASE64_DECODER.decode(signText)); } }
2)SM2KeyPair.java
/** * 国密SM2密钥对 */ public class SM2KeyPair<U, V> { protected V privateKey; protected U publicKey; public SM2KeyPair(U publicKey, V privateKey) { this.publicKey = publicKey; this.privateKey = privateKey; } public U getPublic() { return publicKey; } public V getPrivate() { return privateKey; } }
3)测试示例
public static void main(String[] args) { final String plainText = "123456"; final SM2KeyPair<String, String> keys = genKeyPairAsBase64(); log.debug("\n公钥 : {}\n私钥 : {}", keys.getPublic(), keys.getPrivate()); String pubKey = keys.getPublic(); String priKey = keys.getPrivate(); String encrypt = encryptBase64(pubKey, plainText); String decrypt = decryptBase64(priKey, encrypt); log.debug("\n加密 : {}\n解密 : {}", encrypt, decrypt); try { String sign = sign(plainText, new BigInteger(BASE64_DECODER.decode(priKey))); boolean verify = verify(plainText, sign, BASE64_DECODER.decode(pubKey)); log.debug("\n签名 : {}\n验签 : {}", sign, verify); } catch (Exception e) { e.printStackTrace(); } }
4)测试结果
07:47:16.172 [main] DEBUG com.demo.util.SM2Utils -
公钥 : BPaIW/Bdy1brZeCvaXU95SYRbvT8O/A3cC67Nm8v2ukSikG6ToBJ8yX3rDzg48+R0qimVnN3QVgiAhS2aPprHNA=
私钥 : T768XF7KJXwKdeHRetcmBwiDczSgxIDBj3ioP9ozWG4=
07:47:16.204 [main] DEBUG com.demo.util.SM2Utils -
加密 : BFdQWw4+KRpEWjx7lb8Vf7U56Sfxwx3/5J5xQjyxhkQ8p2WrE+lHeh9SOA13+/8KLtRApRKwApdkE4rnnQQQiqGHv59QjuFOXYznU0CO25qXgrylH3zf+LkC1PEeY7CIUJqZUfK6Og==
解密 : 123456
07:47:16.260 [main] DEBUG com.demo.util.SM2Utils -
签名 : MEQCIFofpslFH01v9Zs6mv44CYubujOQ0VeZsknd+nOikSxnAiBSC1insHS7DhE3eChq+d/WSfHUkMapUDya7ogj3U1Vyw==
验签 : true
3.4 两种实现方式比较
这两种生成SM2密钥对的方式有以下主要区别:
第一种实现方式:
依赖库:使用的是Java的KeyPairGenerator结合BouncyCastle安全提供者。
参数规范:创建了ECGenParameterSpec对象,并指定了SM2标准的曲线名称sm2p256v1。
初始化:使用了SecureRandom来增加密钥生成过程中的随机性。
返回类型:直接返回了标准的KeyPair对象,包含了PublicKey和PrivateKey。
第二种实现方式:
依赖库:直接使用了BouncyCastle库中的KeyPairGeneratorSpi.EC类来生成密钥。
参数规范:通过GMNamedCurves.getByOID获取了SM2标准曲线的参数,并构建了ECParameterSpec对象。
初始化:同样使用了SecureRandom来保证密钥生成过程中的随机性。
返回类型:返回了一个自定义的SM2KeyPair对象,其中包含了一个压缩(或未压缩)的公钥字节数组和私钥的一个BigInteger表示。
比较:
易用性:第一种方法更加简洁,直接使用标准API来生成密钥对,并且返回的是标准的KeyPair对象。第二种方法则需要更多步骤来提取公钥和私钥,并且返回的是自定义的对象类型。
灵活性:第二种方法提供了更多的灵活性,比如可以选择是否压缩公钥,这对于某些应用场景来说可能是必要的。而第一种方法直接返回标准的KeyPair对象,对于如何处理公钥(压缩或非压缩)没有提供选择。
依赖差异:第一种方法依赖于Java的标准库加上BouncyCastle作为安全提供者,而第二种方法直接依赖于BouncyCastle的内部实现,这可能意味着第二种方法可能需要特定版本的BouncyCastle库才能正常工作。
性能和安全性:两种方法都使用了SecureRandom来提高密钥生成的安全性,理论上来说,在安全性方面它们应该是相当的。不过,第二种方法因为涉及到了更多的手动处理步骤,所以可能存在更多的出错机会。
四、推荐:在线 SM2 加解密工具网址
4.1 四种在线工具网址推荐
经过小编亲测,加解密 结果准确、使用方便 的在线 SM2 加解密的工具网址一共有以下四个:
网站一【推荐】:the x,https://the-x.cn/cryptography/Sm2.aspx
网站二:toolhelper,https://www.toolhelper.cn/AsymmetricEncryption/SM2
网站三:pctools,https://www.pctools.cc/zh-cn/guomi
网站四:lzltool,https://lzltool.cn/SM2
其中小编最推荐 the x,里面不仅集成了Base64形式的 SM2 加解密,还可以自动 分析当前字符串为公钥还是私钥,而且还可以根据 标识 判断公钥和私钥是否为同一密钥对。
这里我们就对上面的两个示例进行测试:
4.2 实现方式一 的示例:
公钥:
MFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAE/Si2Wv3bwTUY3a33rIikVHynWJxzsQ+RzSMnzTGvDulLcbWwCbI+BXmM2hBEt7a/qzcFZiAGjFc2SRL0KWbWWA==
1
私钥:
MIGTAgEAMBMGByqGSM49AgEGCCqBHM9VAYItBHkwdwIBAQQgeZXmhOiiBtMhUIEhfP5rkasKrhP/ljSq1F/cd1596hOgCgYIKoEcz1UBgi2hRANCAAT9KLZa/dvBNRjdrfesiKRUfKdYnHOxD5HNIyfNMa8O6UtxtbAJsj4FeYzaEES3tr+rNwVmIAaMVzZJEvQpZtZY
1
明文: 123
密文:
BEBGZCSPGYVZoFHLcfa2FPCSK+Pt76esiaWzwjexOvEiEb3N39lccHct+Rh6yxGMFYRWdlGGg9hbTRJFr4TB5DeUg6YcslM10nxVYBRVdzAPrqt/2c0pRPKpvZ1glyhNWBzAMg==
1
验证结果:
众所周知,公钥加密,私钥解密,我们只需要将 私钥 和 密文 输入之后即可进行解密。结果如下,可以看到解密成功。
4.2 实现方式二 的示例:
公钥:
BPaIW/Bdy1brZeCvaXU95SYRbvT8O/A3cC67Nm8v2ukSikG6ToBJ8yX3rDzg48+R0qimVnN3QVgiAhS2aPprHNA=
1
私钥:
T768XF7KJXwKdeHRetcmBwiDczSgxIDBj3ioP9ozWG4=
1
明文: 123
密文:
BFdQWw4+KRpEWjx7lb8Vf7U56Sfxwx3/5J5xQjyxhkQ8p2WrE+lHeh9SOA13+/8KLtRApRKwApdkE4rnnQQQiqGHv59QjuFOXYznU0CO25qXgrylH3zf+LkC1PEeY7CIUJqZUfK6Og==
1
验证结果:
众所周知,公钥加密,私钥解密,我们只需要将 私钥 和 密文 输入之后即可进行解密。结果如下,可以看到解密成功。
整理完毕,完结撒花~
参考地址:
1.国密算法介绍,https://zhuanlan.zhihu.com/p/132352160
2.即时通讯安全篇(十三):信创必学,一文读懂什么是国密算法,https://cloud.tencent.com/developer/article/2370981
3.Java SM2 国密算法(最权威)!https://blog.csdn.net/bj_xuzhiqiang/article/details/135056192
4.SM2-Java-demo,https://gitee.com/songlu-cube/sm2-java-demo
学问:纸上得来终觉浅,绝知此事要躬行
为事:工欲善其事,必先利其器。
态度:道阻且长,行则将至;行而不辍,未来可期
.....................................................................
------- 桃之夭夭,灼灼其华。之子于归,宜其室家。 ---------------
------- 桃之夭夭,有蕡其实。之子于归,宜其家室。 ---------------
------- 桃之夭夭,其叶蓁蓁。之子于归,宜其家人。 ---------------
=====================================================================
* 博客文章部分截图及内容来自于学习的书本及相应培训课程以及网络其他博客,仅做学习讨论之用,不做商业用途。
* 如有侵权,马上联系我,我立马删除对应链接。 * @author Alan -liu * @Email no008@foxmail.com
转载请标注出处! ✧*꧁一品堂.技术学习笔记꧂*✧. ---> https://www.cnblogs.com/ios9/
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】博客园2025新款「AI繁忙」系列T恤上架,前往周边小店选购
【推荐】凌霞软件回馈社区,携手博客园推出1Panel与Halo联合会员
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步