文章目录
- 前言
- 认识SM2
- 后端工具类实现
- 引入依赖
- 代码实现
- 工具类:SM2Util
- 单元测试
- 案例1:生成服务端公钥、私钥,前端js公钥、私钥
- 案例2:客户端加密,服务端完成解密
- 案例3:服务端进行加密(可用于后面前端测试解密操作)
- 前端vue2实现
- 工具类构建:sm2.js
- 页面vue测试
- 常见报错
- 1、前端加密出现:TypeError: Cannot read properties of null (reading ‘multiply
- 2、后端解密出现:InvalidCipherTextException: invalid cipher text
- 参考文章
前言
本章配套代码:Gitee仓库/demo-exer
- 说明:前端vue工具类和库在resources目录下。
本章节实现思路:后端基于Hutool开源工具提供的SmUtil来完成国密加解密,前端使用sm-crypto来实现加解密。
后端:
- 开源 sm工具类库:国密算法工具-SmUtil
前端:
- 开源库:sm-crypto
认识SM2
认识
SM2是国家密码管理局于2010年12月17日发布的椭圆曲线公钥密码算法。
SM2算法和RSA算法都是公钥密码算法,SM2算法是一种更先进安全的算法,在我们国家商用密码体系中被用来替换RSA算法。
随着密码技术和计算机技术的发展,目前常用的1024位RSA算法面临严重的安全威胁,我们国家密码管理部门经过研究,决定采用SM2椭圆曲线算法替换RSA算法。
对比RSA
SM2性能更优更安全:密码复杂度高、处理速度快、机器性能消耗更小
详细参考: https://www.ecaa.org.cn/667.html
SM2 | RSA | |
---|---|---|
算法结构 | 基本椭圆曲线(ECC) | 基于特殊的可逆模幂运算 |
计算复杂度 | 完全指数级 | 亚指数级 |
存储空间 | 192-256bit | 2048-4096bit |
秘钥生成速度 | 较RSA算法快百倍以上 | 慢 |
解密加密速度 | 较快 | 一般 |
加密长度限制 | 无 | 117 |
后端工具类实现
引入依赖
<!-- 引入Hutool依赖 -->
<dependency><groupId>cn.hutool</groupId><artifactId>hutool-crypto</artifactId><version>5.8.20</version>
</dependency>
<!-- 引入Bouncy Castle依赖 -->
<dependency><groupId>org.bouncycastle</groupId><artifactId>bcpkix-jdk18on</artifactId><version>1.78.1</version>
</dependency>
代码实现
工具类:SM2Util
.
package com.changlu.springboot.sm.util;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.crypto.asymmetric.KeyType;
import cn.hutool.crypto.asymmetric.SM2;
import org.bouncycastle.crypto.engines.SM2Engine;
import org.springframework.util.StringUtils;import java.security.KeyPair;public class SM2Util {public static KeyPair generateKeyPair() {return SecureUtil.generateKeyPair("SM2");}/*** 加密* @param str* @return*/public static String encrypt(String str, String privateKey, String publicKey) {try {if (checkKeyIsEmpty(privateKey, publicKey)) {SM2 sm2 = new SM2(privateKey, publicKey);sm2.setMode(SM2Engine.Mode.C1C3C2);return sm2.encryptHex(str, KeyType.PublicKey);}return str;} catch (Exception e) {throw new RuntimeException("sm2加密失败" + e);}}/*** 解密* @param str 加密之后的字符串* @param privateKey 私钥* @param publicKey 公钥* @return 原文密码*/public static String decrypt(String str, String privateKey, String publicKey) {try {if (checkKeyIsEmpty(privateKey, publicKey)) {SM2 sm2 = new SM2(privateKey, publicKey);sm2.setMode(SM2Engine.Mode.C1C3C2);return sm2.decryptStr(str, KeyType.PrivateKey);}return str;} catch (Exception e) {throw new RuntimeException("sm2解密失败" + e);}}private static boolean checkKeyIsEmpty(String privateKey, String publicKey) {if (StringUtils.isEmpty(privateKey) || StringUtils.isEmpty(publicKey)) {return false;}return true;}}
单元测试
案例1:生成服务端公钥、私钥,前端js公钥、私钥
package com.changlu.springboot.sm;
import cn.hutool.core.util.HexUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.BCUtil;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.crypto.SmUtil;
import cn.hutool.crypto.asymmetric.KeyType;
import cn.hutool.crypto.asymmetric.SM2;
import com.changlu.springboot.sm.util.SM2Util;
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey;
import org.junit.jupiter.api.Test;import java.security.KeyPair;public class SMUtilTest {// 案例1:生成服务端公钥、私钥,前端js公钥、私钥@Testpublic void testDiySM() {String text = "我是一段测试aaaa";// 生成密钥对KeyPair keyPair = SM2Util.generateKeyPair();// 服务器端使用// 生成私钥String privateKey = HexUtil.encodeHexStr(keyPair.getPrivate().getEncoded());// 生成公钥String publicKey = HexUtil.encodeHexStr(keyPair.getPublic().getEncoded());System.out.println("privateKey=>" + privateKey);System.out.println("publicKey=>" + publicKey);// 前端使用// 生成公钥 Q,以Q值做为js端的加密公钥String publicKeyQ = HexUtil.encodeHexStr(((BCECPublicKey) keyPair.getPublic()).getQ().getEncoded(false));System.out.println("公钥Q:"+ publicKeyQ);// 生成私钥 D,以D值做为js端的解密私钥String privateKeyD = HexUtil.encodeHexStr(BCUtil.encodeECPrivateKey(keyPair.getPrivate()));System.out.println("私钥D:"+ privateKeyD);// 服务端加解密String encodeStr = SM2Util.encrypt(text, privateKey, publicKey);String formatStr = SM2Util.decrypt(encodeStr, privateKey, publicKey);System.out.println("encodeStr=>" + encodeStr);System.out.println("formatStr=>" + formatStr);}}
效果:该单元测试得到的服务端公钥、私钥,前端js公钥、私钥,可用于服务器端、前端使用。
案例2:客户端加密,服务端完成解密
场景:前端客户端使用前端公钥加密后(可用vue中加密函数生成的加密内容),我们在服务器端进行解密。
package com.changlu.springboot.sm;
import cn.hutool.core.util.HexUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.BCUtil;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.crypto.SmUtil;
import cn.hutool.crypto.asymmetric.KeyType;
import cn.hutool.crypto.asymmetric.SM2;
import com.changlu.springboot.sm.util.SM2Util;
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey;
import org.junit.jupiter.api.Test;import java.security.KeyPair;public class SMUtilTest {// 生成的一组私钥、公钥进行测试private String privateKey = "308193020100301306072a8648ce3d020106082a811ccf5501822d047930770201010420057ab3e1e512e970023c16c545289ecf37dd2cb202daa24c42936f21daa061aca00a06082a811ccf5501822da14403420004981070e26f624917f2717bcaadc000c928c91b49c9c218df33260cafa1d2243c2427fd3486884a67d390751ff4956e35466fb4b925a666229b22d36c26267d67";private String publicKey = "3059301306072a8648ce3d020106082a811ccf5501822d03420004981070e26f624917f2717bcaadc000c928c91b49c9c218df33260cafa1d2243c2427fd3486884a67d390751ff4956e35466fb4b925a666229b22d36c26267d67";private String publicKeyQ = "04981070e26f624917f2717bcaadc000c928c91b49c9c218df33260cafa1d2243c2427fd3486884a67d390751ff4956e35466fb4b925a666229b22d36c26267d67";private String privateKeyD = "057ab3e1e512e970023c16c545289ecf37dd2cb202daa24c42936f21daa061ac";// 案例2:客户端加密,服务端完成解密@Testpublic void testDecrypt() {String encodeStr = "04badafbddce5f728fb11c2007f2230b2fcd0ecf019ac4536370c75dc2e222ca696d20033ab8f76965bd1a9e2691b7a6e4e62d71627874cedd6138453444e1868881e69dbcd3ca13818d6db061561fb87da14e061d9d1c82d550322b2e04c60bcca7998ac51059";String formatStr = SM2Util.decrypt(encodeStr, privateKey, publicKey);System.out.println("formatStr=>" + formatStr);}
}
案例3:服务端进行加密(可用于后面前端测试解密操作)
package com.changlu.springboot.sm;
import cn.hutool.core.util.HexUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.BCUtil;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.crypto.SmUtil;
import cn.hutool.crypto.asymmetric.KeyType;
import cn.hutool.crypto.asymmetric.SM2;
import com.changlu.springboot.sm.util.SM2Util;
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey;
import org.junit.jupiter.api.Test;
import java.security.KeyPair;public class SMUtilTest {// 生成的一组私钥、公钥进行测试private String privateKey = "308193020100301306072a8648ce3d020106082a811ccf5501822d047930770201010420057ab3e1e512e970023c16c545289ecf37dd2cb202daa24c42936f21daa061aca00a06082a811ccf5501822da14403420004981070e26f624917f2717bcaadc000c928c91b49c9c218df33260cafa1d2243c2427fd3486884a67d390751ff4956e35466fb4b925a666229b22d36c26267d67";private String publicKey = "3059301306072a8648ce3d020106082a811ccf5501822d03420004981070e26f624917f2717bcaadc000c928c91b49c9c218df33260cafa1d2243c2427fd3486884a67d390751ff4956e35466fb4b925a666229b22d36c26267d67";private String publicKeyQ = "04981070e26f624917f2717bcaadc000c928c91b49c9c218df33260cafa1d2243c2427fd3486884a67d390751ff4956e35466fb4b925a666229b22d36c26267d67";private String privateKeyD = "057ab3e1e512e970023c16c545289ecf37dd2cb202daa24c42936f21daa061ac";// 案例3:服务端进行加密@Testpublic void testEncrypt() {String str = "changlu test test";String encodeStr = SM2Util.encrypt(str, privateKey, publicKey);System.out.println("encodeStr=>" + encodeStr);}}
前端vue2实现
工具类构建:sm2.js
导入依赖:
cnpm i sm-crypto --save
工具类utils目录中创建sm2.js:
import { sm2 } from 'sm-crypto';// 公钥
const PUBLIC_KEY = '04981070e26f624917f2717bcaadc000c928c91b49c9c218df33260cafa1d2243c2427fd3486884a67d390751ff4956e35466fb4b925a666229b22d36c26267d67'
const PRIVATE_KEY = '057ab3e1e512e970023c16c545289ecf37dd2cb202daa24c42936f21daa061ac'// 可配置参数
// 1 - C1C3C2; 0 - C1C2C3; 默认为1
const cipherMode = 1//加密
export function doSM2Encrypt(str) {let msg = strif (typeof str !== 'string') {msg = JSON.stringify(str)}// console.log(msg,'加密前')let publicKey = PUBLIC_KEY// 加密结果let encryptData = sm2.doEncrypt(msg, publicKey, cipherMode)//Base64编码 自行选择是否使用//let baseEncode = Base64.encode(encryptData)// 加密后的密文前需要添加04,后端才能正常解密 (不添加04,后端处理也可以)let encrypt = '04' + encryptDatareturn encrypt
}// 解密
export function doSM2DecryptStr(enStr) {let msg = enStrif (typeof enStr !== 'string') {msg = JSON.stringify(enStr)}let privateKey = PRIVATE_KEYlet enval = enStr.substring(2)// 解密结果let doDecrypt = sm2.doDecrypt(enval , privateKey, cipherMode)console.log("doDecrypt=>", doDecrypt)// 解密后类型转换return doDecrypt;
}
页面vue测试
任意找一个vue页面来进行测试。
首先引入工具类:
import { doSM2Encrypt, doSM2DecryptStr } from '@/utils/sm2'
编写测试函数:
export default {created() {//测试编码this.testEncode()},methods: {testEncode() {// 案例1:前端进行加密let str = "123456"let encodeStr = doSM2Encrypt(str)console.log("前端明文加密之后=>", encodeStr)// 案例2:前端自己加密之后的内容进行解密let originStr = doSM2DecryptStr(encodeStr)console.log("前端自行加密,解密之后=>", originStr)// 案例3:服务器端内容加密后进行解密let testEncodeStr = '04c719fa9dff41a22b8119a3f1ba984303d19c295f1f6a6b8196c7a330cf0e9e1830cbd3cd949c49be0681fd2fa9abca3ed14f8f8d4111c552ef7603793c0a2ae344e9072b7dcaefaf5785634a624d4ca7addcf4ab9ff37abe4ec69847ee5e24a65ce74e8a5b1aecdc467d18b36fba22a8e8'originStr = doSM2DecryptStr(testEncodeStr)console.log("服务器端进行加密,解密之后=>", originStr)},
}
常见报错
1、前端加密出现:TypeError: Cannot read properties of null (reading ‘multiply
出现异常:
解决方式:
错误:一开始直接将服务端生成的公钥作为前端的公钥,该公钥的前缀没有04,此时就会报错。
3059301306072a8648ce3d020106082a811ccf5501822d03420004981070e26f624917f2717bcaadc000c928c91b49c9c218df33260cafa1d2243c2427fd3486884a67d390751ff4956e35466fb4b925a666229b22d36c26267d67
正确方式:
此时得到的公钥为:
04981070e26f624917f2717bcaadc000c928c91b49c9c218df33260cafa1d2243c2427fd3486884a67d390751ff4956e35466fb4b925a666229b22d36c26267d67
2、后端解密出现:InvalidCipherTextException: invalid cipher text
问题描述:
解决方案:
和问题1一致,一开始前端使用的公钥并不是D值作为js端的解密私钥,此时生成出来的自然在解密端无法解除。
参考文章
[1]. 使用sm2出现报错 “TypeError: Cannot read properties of null (reading ‘multiply‘)”:https://blog.csdn.net/ciwei0605/article/details/125844154
[2]. BC库实现SM2解密时InvalidCipherTextException:https://blog.csdn.net/weixin_43504369/article/details/132739118
[3]. 从零玩转前后端加解密之SM2-sm2:https://www.cnblogs.com/yby6/p/17414766.html
[4]. 适用于前后端的SM2国密加密解密:https://blog.csdn.net/weixin_53021967/article/details/131594733