结合Hutool实现SM2算法文件加解密 & 签名验签
欢迎浏览本文章
该文属于个人经验分享类,结合csdn多篇文章整合重制而成,可能存在不足和缺点,欢迎提出指导&意见。
0 . 背景
业务中涉及SM2算法,特抽象出为工具类。
0.1 SM2 算法背景
SM2密码算法是一种椭圆(非对称)密码算法,具有以下重要特性:
- 加密强度:256位(私钥长度);
- 公私钥长度:公钥长度为64字节(512位),私钥32字节(256位);
- 支持签名最大数据量及签名结果长度:最大签名数据量长度无限制;签名结果为64字节(但由于签名后会做ASN.1编码,实际输出长度为70-72字节);
- 支持加密最大数据量及加密后结果长度:支持最大近128G字节数据长度;加密结果(C=C1C3C2)增加96字节【C1(64字节) + C3(32字节)】(如果首个字节为0x04则增加97字节,实际有效96字节)。
0.2 本文背景
本文并非100%原创,只是做了必要的封装,提供给同样需求的朋友。参考了以下文章,在此衷心感谢他们的研究和分享:
- 【SM2 SM3 SM4简介】https://blog.csdn.net/cqwei1987/article/details/107329600;
- 【国密算法—SM2介绍及基于BC的实现】https://blog.csdn.net/zcmain/article/details/114099664;
- 【hutool国密sm2算法使用, 正确的秘钥生成签名及验签,签名为64字节】https://blog.csdn.net/qq_33140565/article/details/113818235;
- 【国密SM2算法加解密文件】https://blog.csdn.net/A_Lonely_Smile/article/details/118366945
1 . 依赖引入
<!--开始-国密SM2 算法依赖:bouncycastle & hutool -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15to18</artifactId>
<version>1.66</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.3.10</version>
</dependency>
<!--结束-国密SM2 算法依赖:bouncycastle & hutool -->
2 .创建秘钥对象
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.experimental.Accessors;
/**
* SM2 国密算法生成秘钥对 - 对象
*/
@Data
@Accessors(chain = true)
@ApiModel(value = "SM2KeyPairs", description = "SM2用户秘钥对对象")
public class SM2KeyPairs {
@ApiModelProperty(value = "公钥")
private String publicKeyBase64;
@ApiModelProperty(value = "私钥")
private String privateKeyBase64;
}
3 .核心:工具类
import cn.hutool.core.util.HexUtil;
import cn.hutool.crypto.BCUtil;
import cn.hutool.crypto.SmUtil;
import cn.hutool.crypto.asymmetric.KeyType;
import cn.hutool.crypto.asymmetric.SM2;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.bouncycastle.crypto.engines.SM2Engine;
import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey;
import org.springframework.web.multipart.MultipartFile;
/**
* SM2 国密算法 工具类
*/
@Slf4j
public class SM2FileUtil {
/**
* 生成私钥 & 公钥
*
*
* @return SM2KeyPairs对象
*/
public static SM2KeyPairs getKeyParis() {
SM2KeyPairs sm2KeyPairs = new SM2KeyPairs();
//随机生成秘钥
SM2 sm2 = new SM2();
//获取秘钥对象
PrivateKey privateKeyObject = sm2.getPrivateKey();
PublicKey publicKeyObject = sm2.getPublicKey();
//生成私钥
String privateKeyBase64 = Base64.encode(privateKeyObject.getEncoded());
//生成公钥
String publicKeyBase64 = Base64.encode(publicKeyObject.getEncoded());
sm2KeyPairs.setPrivateKeyBase64(privateKeyBase64);
sm2KeyPairs.setPublicKeyBase64(publicKeyBase64);
return sm2KeyPairs;
}
/**
* SM2 文件加密
*
* @param publicKey 公钥
* @param dataBytes 提交的原始文件以流的形式
* @param outputPath 输出的加密文件路径
* @param fileName 输出的加密文件名称
*/
public static Boolean lockFile(String publicKey, byte[] dataBytes, String outputPath, String fileName) throws Exception {
Boolean flag = false;
if (StringUtils.isEmpty(publicKey) || null == dataBytes ||
StringUtils.isEmpty(outputPath) || StringUtils.isEmpty(fileName)) {
throw new BusinessException("缺少必要参数!");
} else {
long startTime = System.currentTimeMillis();
// 初始化SM2对象
try {
SM2 SM_2 = new SM2(null, publicKey);
byte[] data;
data = SM_2.encrypt(dataBytes, KeyType.PublicKey);
FileUtils.byteToFile(data, outputPath, fileName);
flag = true;
} catch (Exception e) {
flag = false;
log.error("Exception | " + e);
}
long endTime = System.currentTimeMillis();
log.error("本次加密操作,所耗时间为:" + (endTime - startTime));
return flag;
}
}
/**
* SM2 文件解密
*
* @param privateKey 私钥
* @param lockFilePath 加密文件路径
* @param outputPath 输出的解密文件路径
* @param fileName 输出的解密文件名称
*/
public static Boolean unlockFile(String privateKey, String lockFilePath, String outputPath, String fileName) {
Boolean flag = false;
if (StringUtils.isEmpty(privateKey) || StringUtils.isEmpty(lockFilePath) || StringUtils.isEmpty(fileName)) {
throw new BusinessException("缺少必要参数!");
} else {
long startTime = System.currentTimeMillis();
try {
// 初始化SM2对象
SM2 SM_2 = new SM2(privateKey, null);
byte[] bytes = FileUtils.fileToByte(lockFilePath);
byte[] data;
data = SM_2.decrypt(bytes, KeyType.PrivateKey);
FileUtils.byteToFile(data, outputPath, fileName);
flag = true;
} catch (Exception e) {
log.error("Exception | " + e);
}
long endTime = System.currentTimeMillis();
log.error("本次解密操作,所耗时间为:" + (endTime - startTime));
return flag;
}
}
/**
* 通过私钥进行文件签名
*
* @param privateKey 私钥
* @param dataBytes 需要签名的文件以流的形式
* @throws Exception
*/
public static String generateFileSignByPrivateKey(String privateKey, byte[] dataBytes) throws Exception {
String signature = "";
if (StringUtils.isEmpty(privateKey) || null == dataBytes) {
throw new BusinessException("缺少必要参数!");
} else {
long startTime = System.currentTimeMillis();
String signs = "";
try {
//----------------------20210830优化:私钥HEX处理---------------------------------
byte[] decode = Base64.decode(privateKey);
SM2 sm3 = new SM2(decode,null);
byte[] bytes = BCUtil.encodeECPrivateKey(sm3.getPrivateKey());
String privateKeyHex = HexUtil.encodeHexStr(bytes);
//------@End----------------20210830优化:私钥HEX处理------------------------------
//需要加密的明文,得到明文对应的字节数组
ECPrivateKeyParameters privateKeyParameters = BCUtil.toSm2Params(privateKeyHex);
//创建sm2 对象
SM2 sm2 = new SM2(privateKeyParameters, null);
//这里需要手动设置,sm2 对象的默认值与我们期望的不一致 , 使用明文编码
sm2.usePlainEncoding();
sm2.setMode(SM2Engine.Mode.C1C2C3);
byte[] sign = sm2.sign(dataBytes, null);
//change encoding : hex to base64
signs = Base64.encode(sign);
signature = signs;
} catch (Exception e) {
log.error("Exception | " + e);
}
long endTime = System.currentTimeMillis();
log.error("本次签名操作,所得签名为:" + signs + ",所耗时间为:" + (endTime - startTime));
return signature;
}
}
/**
* 通过公钥进行文件验签
*
* @param publicKey 公钥
* @param sign 签名(原先为hex处理后的16位,现在改为base处理后的64位)
* @param dataBytes 需要验签的文件数据以流的形式
* @return
* @throws Exception
*/
public static Boolean verifyFileSignByPublicKey(String publicKey, String sign, byte[] dataBytes) throws Exception {
if (StringUtils.isEmpty(publicKey) || StringUtils.isEmpty(sign) || null == dataBytes) {
throw new BusinessException("缺少必要参数!");
} else {
long startTime = System.currentTimeMillis();
Boolean verify = false;
try {
//-----------------------------20210830修改公钥HEX处理----------------------------
byte[] decode = Base64.decode(publicKey);
SM2 sm3 = new SM2(null,decode);
byte[] bytes = ((BCECPublicKey) sm3.getPublicKey()).getQ().getEncoded(false);
String publicKeyHex = HexUtil.encodeHexStr(bytes);
//--------@End---------------------公钥HEX处理------------------------------------
//需要加密的明文,得到明文对应的字节数组
//这里需要根据公钥的长度进行加工
if (publicKeyHex.length() == 130) {
//这里需要去掉开始第一个字节 第一个字节表示标记
publicKeyHex = publicKeyHex.substring(2);
}
String xhex = publicKeyHex.substring(0, 64);
String yhex = publicKeyHex.substring(64, 128);
ECPublicKeyParameters ecPublicKeyParameters = BCUtil.toSm2Params(xhex, yhex);
//创建sm2 对象
SM2 sm2 = new SM2(null, ecPublicKeyParameters);
//这里需要手动设置,sm2 对象的默认值与我们期望的不一致 , 使用明文编码
sm2.usePlainEncoding();
sm2.setMode(SM2Engine.Mode.C1C2C3);
verify = sm2.verify(dataBytes, Base64.decode(sign));
} catch (Exception e) {
log.error("Exception | " + e);
}
long endTime = System.currentTimeMillis();
log.error("本次验签操作,所得结果为:" + verify + ",所耗时间为:" + (endTime - startTime));
return verify;
}
}
4 .其他辅助类
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.springframework.web.multipart.MultipartFile;
import java.io.*;
/**
* 文件处理工具类
*
* @author wujie
*/
@Slf4j
public class FileUtils {
/**
* 获得指定文件的byte数组
*
* @param filePath 文件路径
* @return 字节数组
*/
public static byte[] fileToByte(String filePath) {
byte[] buffer = null;
try {
File file = new File(filePath);
FileInputStream fis = new FileInputStream(file);
ByteArrayOutputStream bos = new ByteArrayOutputStream(1000);
byte[] b = new byte[1024];
int n;
while ((n = fis.read(b)) != -1) {
bos.write(b, 0, n);
}
fis.close();
bos.close();
buffer = bos.toByteArray();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return buffer;
}
/**
*
* @param multipartFile 真实文件
* @return byte[] 数组
*/
public static byte[] multipartFileToByte(MultipartFile multipartFile) throws Exception {
byte[] buffer = null;
File file = multipartFileToFile(multipartFile);
try {
FileInputStream fis = new FileInputStream(file);
ByteArrayOutputStream bos = new ByteArrayOutputStream(1000);
byte[] b = new byte[1024];
int n;
while ((n = fis.read(b)) != -1) {
bos.write(b, 0, n);
}
fis.close();
bos.close();
buffer = bos.toByteArray();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}finally {
//清除缓存文件
delteTempFile(file);
}
return buffer;
}
/**
* 根据byte数组,生成文件
* @param bfile 字节数组
* @param filePath 文件路径
* @param fileName 文件名
*/
public static void byteToFile(byte[] bfile, String filePath, String fileName) {
BufferedOutputStream bos = null;
FileOutputStream fos = null;
File file = null;
try {
File dir = new File(filePath);
if (!dir.exists() && dir.isDirectory()) {
dir.mkdirs();
}
file = new File(filePath + "\\" + fileName);
fos = new FileOutputStream(file);
bos = new BufferedOutputStream(fos);
bos.write(bfile);
} catch (Exception e) {
e.printStackTrace();
} finally {
IOUtils.closeQuietly(bos);
IOUtils.closeQuietly(fos);
}
}
public static File multipartFileToFile(MultipartFile file) throws Exception {
File toFile = null;
if (file.equals("") || file.getSize() <= 0) {
file = null;
} else {
InputStream ins = null;
ins = file.getInputStream();
toFile = new File(file.getOriginalFilename());
inputStreamToFile(ins, toFile);
ins.close();
}
return toFile;
}
private static void inputStreamToFile(InputStream ins, File file) {
try {
OutputStream os = new FileOutputStream(file);
int bytesRead = 0;
byte[] buffer = new byte[8192];
while ((bytesRead = ins.read(buffer, 0, 8192)) != -1) {
os.write(buffer, 0, bytesRead);
}
os.close();
ins.close();
} catch (Exception e) {
e.printStackTrace();
}
}
public static void delteTempFile(File file) {
if (file != null) {
File del = new File(file.toURI());
del.delete();
}
}
}