文章目录
- 前言
- 认识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公钥、私钥
@Test
public 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:客户端加密,服务端完成解密
@Test
public 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:服务端进行加密
@Test
public 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 = str
if (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' + encryptData
return encrypt
}
// 解密
export function doSM2DecryptStr(enStr) {
let msg = enStr
if (typeof enStr !== 'string') {
msg = JSON.stringify(enStr)
}
let privateKey = PRIVATE_KEY
let 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