提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
前言
最近公司的一些项目的安全认证算法在逐渐转为使用国密实现,所以最近学习了一些比如SM2、SM4算法的相关知识,以及代码实现。
因为工作之后忘了很多,而且本身对数学也不是很好(比较菜)所以做的时候找了很多资料,过程中也是踩了很多坑,所以整理出来,希望对一些初学者有所帮助,也作为自己的一个学习资料记录下来。
一、SM2是什么?
SM2是国家密码管理局于2010年12月17日发布的椭圆曲线公钥密码算法。形式类似于RSA这种非对称加密,有公钥和私钥两组密钥。
SM2算法和RSA算法都是公钥密码算法,SM2算法与RSA算法不同的是,SM2算法是基于椭圆曲线上点群离散对数难题,相对于RSA算法,256位的SM2密码强度已经比2048位的RSA密码强度要高。
国密 SM2 算法是一种 ECC 算法。
二、Java实现
最开始的时候在网上找了相当多的实现代码,但是因为JAVA的代码实际对加密上的封装是比较好的,如果只是简单使用也比较简单,所以第一次写的时候并没有发现什么太大的问题。但是最后与公司另一个部门写C的同事做了互通测试,结果加密结果是不通用的,对了之后发现很多不了解的点。
1.实现
先说实现,先看看代码,之后再解释里边的一些参数,如果只是想跑起来,那么看完代码就可以了,但是我还是建议大家往下看完,不然真的会有很多坑。
1.1 引入依赖
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
<version>1.70</version>
</dependency>
1.2 生成密钥
private final static String CRYPTO_NAME_SM2 = "sm2p256v1";
/**
* 生成国密公私钥对
*
* @return Sm2国密对
* @throws Exception
*/
public static Sm2KeyEntity generateSmKey() throws Exception {
KeyPairGenerator keyPairGenerator = null;
SecureRandom secureRandom = new SecureRandom();
ECGenParameterSpec sm2Spec = new ECGenParameterSpec(CRYPTO_NAME_SM2);
keyPairGenerator = KeyPairGenerator.getInstance("EC", new BouncyCastleProvider());
keyPairGenerator.initialize(sm2Spec);
keyPairGenerator.initialize(sm2Spec, secureRandom);
KeyPair keyPair = keyPairGenerator.generateKeyPair();
PrivateKey privateKey = keyPair.getPrivate();
PublicKey publicKey = keyPair.getPublic();
return new Sm2KeyEntity(privateKey, publicKey);
}
1.3 将各种字符串保存的公钥转为公钥对象
/**
* 将Base64转码的公钥串,转化为公钥对象(全量密钥)
*
* @param base64PublicKey 公钥对象base64字符串
* @return 公钥对象 {@link BCECPublicKey}
*/
public static PublicKey createPublicKeyByFullBase64Str(String base64PublicKey) throws NoSuchAlgorithmException, InvalidKeySpecException {
X509EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(Base64.getDecoder().decode(base64PublicKey));
KeyFactory keyFactory = KeyFactory.getInstance("EC", new BouncyCastleProvider());
return keyFactory.generatePublic(publicKeySpec);
}
/**
* 根据Q点数值的数组创建一个公钥对象(Hex)
*
* @param publicKeyQPointBase64 公钥Q点值(Hex)
* @return 公钥对象
* @throws NoSuchAlgorithmException 没有对应点算法
* @throws InvalidKeySpecException 非法密钥
*/
private static PublicKey createPublicKeyByQPointBase64(String publicKeyQPointBase64) throws NoSuchAlgorithmException, InvalidKeySpecException {
return createPublicKeyByQPoint(Base64.getDecoder().decode(publicKeyQPointBase64));
}
/**
* 根据Q点数值的数组创建一个公钥对象(Hex)
*
* @param publicKeyQPointHex 公钥Q点值(Hex)
* @return 公钥对象
* @throws NoSuchAlgorithmException 没有对应点算法
* @throws InvalidKeySpecException 非法密钥
*/
private static PublicKey createPublicKeyByQPoint(String publicKeyQPointHex) throws NoSuchAlgorithmException, InvalidKeySpecException {
return createPublicKeyByQPoint(Hex.decode(publicKeyQPointHex));
}
/**
* 根据Q点数值的数组创建一个公钥对象
*
* @param publicKeyQPointBytes 公钥Q点值(数组)
* @return 公钥对象
* @throws NoSuchAlgorithmException 没有对应点算法
* @throws InvalidKeySpecException 非法密钥
*/
public static PublicKey createPublicKeyByQPoint(byte[] publicKeyQPointBytes) throws NoSuchAlgorithmException, InvalidKeySpecException {
// 获取SM2相关参数
X9ECParameters parameters = GMNamedCurves.getByName(CRYPTO_NAME_SM2);
// 椭圆曲线参数规格
ECParameterSpec ecParameterSpec = new ECParameterSpec(parameters.getCurve(), parameters.getG(), parameters.getN(), parameters.getH());
// 将公钥HEX字符串转换为椭圆曲线对应的点
ECPoint ecPoint = parameters.getCurve().decodePoint(publicKeyQPointBytes);
// 获取椭圆曲线KEY生成器
KeyFactory keyFactory = KeyFactory.getInstance("EC", new BouncyCastleProvider());
return keyFactory.generatePublic(new ECPublicKeySpec(ecPoint, ecParameterSpec));
}
1.4 将各种字符串保存的私钥转为私钥对象
/**
* 将Base64转码的私钥串,转化为私钥对象(全量密钥)
*
* @param base64PrivateKey 私钥对象base64字符串
* @return 私钥对象 {@link BCECPrivateKey}
*/
public static PrivateKey createPrivateKeyByFullBase64Str(String base64PrivateKey) throws NoSuchAlgorithmException, InvalidKeySpecException {
PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(Base64.getDecoder().decode(base64PrivateKey));
KeyFactory keyFactory = KeyFactory.getInstance("EC", new BouncyCastleProvider());
return keyFactory.generatePrivate(pkcs8EncodedKeySpec);
}
/**
* 根据D值(Base64格式的 {@link String })创建一个私钥对象
*
* @param dBase64 D值(Base64字符串)
* @return 私钥对象
* @throws NoSuchAlgorithmException 没有对应的算法
* @throws InvalidKeySpecException 非法密钥
*/
public static PrivateKey createPrivateKeyByDBase64(String dBase64) throws NoSuchAlgorithmException, InvalidKeySpecException {
return createPrivateKeyByD(Base64.getDecoder().decode(dBase64));
}
/**
* 根据D值({@link Byte} 数组)创建一个私钥对象
*
* @param dBytes D值(Byte数组)
* @return 私钥对象
* @throws NoSuchAlgorithmException 没有对应的算法
* @throws InvalidKeySpecException 非法密钥
*/
public static PrivateKey createPrivateKeyByD(byte[] dBytes) throws NoSuchAlgorithmException, InvalidKeySpecException {
String dHex = Hex.toHexString(dBytes);
return createPrivateKeyByD(dHex);
}
/**
* 根据D值(HEX格式的 {@link String })创建一个私钥对象
*
* @param dHex D值(HEX字符串)
* @return 私钥对象
* @throws NoSuchAlgorithmException 没有对应的算法
* @throws InvalidKeySpecException 非法密钥
*/
public static PrivateKey createPrivateKeyByD(String dHex) throws NoSuchAlgorithmException, InvalidKeySpecException {
return createPrivateKeyByD(new BigInteger(dHex, 16));
}
/**
* 根据D值({@link BigInteger})创建一个私钥对象
*
* @param d D值
* @return 私钥对象
* @throws NoSuchAlgorithmException 没有对应的算法
* @throws InvalidKeySpecException 非法密钥
*/
public static PrivateKey createPrivateKeyByD(BigInteger d) throws NoSuchAlgorithmException, InvalidKeySpecException {
X9ECParameters parameters = GMNamedCurves.getByName(CRYPTO_NAME_SM2);
ECParameterSpec ecParameterSpec = new ECParameterSpec(parameters.getCurve(),
parameters.getG(), parameters.getN(), parameters.getH());
KeyFactory keyFactory = KeyFactory.getInstance("EC", new BouncyCastleProvider());
return keyFactory.generatePrivate(new ECPrivateKeySpec(d, ecParameterSpec));
}
1.5 加密、解密、签名、验签
import org.bouncycastle.crypto.CipherParameters;
import org.bouncycastle.crypto.InvalidCipherTextException;
import org.bouncycastle.crypto.digests.SM3Digest;
import org.bouncycastle.crypto.engines.SM2Engine;
import org.bouncycastle.crypto.params.*;
import org.bouncycastle.crypto.signers.SM2Signer;
import org.bouncycastle.crypto.signers.StandardDSAEncoding;
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey;
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey;
import org.bouncycastle.jcajce.provider.asymmetric.util.ECUtil;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.jce.spec.ECParameterSpec;
import org.bouncycastle.util.encoders.Hex;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.Security;
public class BcSm2Util {
private final static Logger LOGGER = LoggerFactory.getLogger(BcSm2Util.class);
static {
Security.addProvider(new BouncyCastleProvider());
}
/**
* 根据publicKey对原始数据data,使用SM2加密
*/
public static byte[] encrypt(byte[] data, PublicKey publicKey) throws InvalidCipherTextException {
BCECPublicKey localECPublicKey = (BCECPublicKey) publicKey;
ECParameterSpec localECParameterSpec = localECPublicKey.getParameters();
ECDomainParameters localECDomainParameters = new ECDomainParameters(
localECParameterSpec.getCurve(), localECParameterSpec.getG(), localECParameterSpec.getN()
);
ECPublicKeyParameters localECPublicKeyParameters = new ECPublicKeyParameters(localECPublicKey.getQ(), localECDomainParameters);
SM2Engine localSM2Engine = new SM2Engine(new SM3Digest(), SM2Engine.Mode.C1C3C2);
localSM2Engine.init(true, new ParametersWithRandom(localECPublicKeyParameters, new SecureRandom()));
byte[] arrayOfByte2;
arrayOfByte2 = localSM2Engine.processBlock(data, 0, data.length);
return arrayOfByte2;
}
/**
* 根据privateKey对加密数据encodedata,使用SM2解密
*/
public static byte[] decrypt(byte[] encodedata, PrivateKey privateKey) throws InvalidCipherTextException {
SM2Engine localSM2Engine = new SM2Engine(new SM3Digest(), SM2Engine.Mode.C1C3C2);
BCECPrivateKey sm2PriK = (BCECPrivateKey) privateKey;
ECParameterSpec localECParameterSpec = sm2PriK.getParameters();
ECDomainParameters localECDomainParameters = new ECDomainParameters(
localECParameterSpec.getCurve(), localECParameterSpec.getG(), localECParameterSpec.getN()
);
ECPrivateKeyParameters localECPrivateKeyParameters = new ECPrivateKeyParameters(sm2PriK.getD(),
localECDomainParameters);
localSM2Engine.init(false, localECPrivateKeyParameters);
return localSM2Engine.processBlock(encodedata, 0, encodedata.length);
}
/**
* 私钥签名
*/
public static byte[] signByPrivateKey(byte[] data, PrivateKey privateKey) throws Exception {
SM2Signer signer = new SM2Signer(StandardDSAEncoding.INSTANCE, new SM3Digest());
CipherParameters param = new ParametersWithRandom(ECUtil.generatePrivateKeyParameter(privateKey));
ParametersWithID parametersWithID = new ParametersWithID(
param, Hex.decodeStrict("31323334353637383132333435363738")
);
signer.init(true, parametersWithID);
signer.update(data, 0, data.length);
return signer.generateSignature();
}
/**
* 公钥验签
*/
public static boolean verifyByPublicKey(byte[] data, PublicKey publicKey, byte[] signature) throws Exception {
SM2Signer signer = new SM2Signer(StandardDSAEncoding.INSTANCE, new SM3Digest());
ParametersWithID parametersWithID = new ParametersWithID(
ECUtil.generatePublicKeyParameter(publicKey), Hex.decodeStrict("31323334353637383132333435363738")
);
signer.init(false, parametersWithID);
signer.update(data, 0, data.length);
return signer.verifySignature(signature);
}
/**
* 计算签名(将签名的结果转为R和S值返回)
*
* @param data 原始数据
* @param privateKey 私钥
* @return 签名结果
* @throws Exception 异常
*/
public static Sm2SignRS signByPrivateKeyToRS(byte[] data, PrivateKey privateKey) throws Exception {
byte[] signResultBytes = signByPrivateKey(data, privateKey);
StandardDSAEncoding standardDSAEncoding = new StandardDSAEncoding();
ParametersWithRandom param = new ParametersWithRandom(ECUtil.generatePrivateKeyParameter(privateKey));
BigInteger[] decode = standardDSAEncoding.decode(
((ECKeyParameters) param.getParameters()).getParameters().getN(), signResultBytes
);
byte[] resultR;
byte[] sumR = decode[0].toByteArray();
if (sumR[0] == 0x00 && sumR.length == 33) {
resultR = new byte[32];
System.arraycopy(sumR, 1, resultR, 0, 32);
} else {
resultR = sumR;
}
byte[] resultS;
byte[] sumS = decode[1].toByteArray();
if (sumS[0] == 0x00 && sumS.length == 33) {
resultS = new byte[32];
System.arraycopy(sumS, 1, resultS, 0, 32);
} else {
resultS = sumS;
}
return new Sm2SignRS(
resultR,
resultS
);
}
/**
* 根据R和S的值验证签名
*
* @param data 原始数据
* @param publicKey 公钥
* @param sm2SignRS 签名数据对象
* @return 成功返回true
* @throws Exception 异常
*/
public static boolean verifyByPublicKeyByRS(byte[] data, PublicKey publicKey, Sm2SignRS sm2SignRS) throws Exception {
StandardDSAEncoding standardDSAEncoding = new StandardDSAEncoding();
ECKeyParameters ecKeyParameters = (ECKeyParameters) ECUtil.generatePublicKeyParameter(publicKey);
byte[] encode = standardDSAEncoding.encode(
ecKeyParameters.getParameters().getN(),
byteConvertInteger(sm2SignRS.getR()),
byteConvertInteger(sm2SignRS.getS())
);
return verifyByPublicKey(data, publicKey, encode);
}
/**
* 换字节流(字节数组)型数据转大数字(主要是做了在首个数组为负数的时候将数组前边拼接为 0x00)
*
* @param b 字节数组
* @return 大数字
*/
public static BigInteger byteConvertInteger(byte[] b) {
if (b[0] < 0) {
byte[] temp = new byte[b.length + 1];
temp[0] = 0;
System.arraycopy(b, 0, temp, 1, b.length);
return new BigInteger(temp);
}
return new BigInteger(b);
}
}
2.坑
2.1 加密中的坑
2.1.1 加密方式
加密方式里边,有两种 一种是 C1C3C2另一种是C1C2C3,这两种加密方式不同,当时找的资料说是,旧版本的标准上,使用的是C1C2C3,但是后续应该是更新过,使用的是C1C3C2,其他语言,比如Python或者Golang或者C的实现大多直接就是C1C3C2的,但是如果java中使用的bouncycastle的包,默认使用的是C1C2C3,就会发生与其他语言的加密结果不能相互解密的情况,但是可能你跟其他人的Java系统加解密又没有问题。
2.1.2 与其他语言的密钥传输编码问题
Java的密钥一般直接使用 PrivateKey.getEncoded() 或者 PublicKey.getEncoded() 获得密钥,然后直接使用Base64或者Hex将密钥转成可见字符串,但是Sm2算法这样保存的密钥其他语言的工具大多数是解析不了的。
2.1.2.1 导出
所以,Java生成的密钥,在导出的时候,最好使用 PrivateKey.getD().toByteArray() 和 PublicKey.getQ().getEncoded() 导出密钥,然后再转成Base64或者Hex给其他系统;
2.1.2.2 导入
导入的时候,请使用我上边的代码转换成对应的公钥或者私钥的对象,这里有另一个容易出错的地方,在私钥byte[]转私钥对象的时候,有些人会使用new BigInteger(byte[])这个方法将byte[]转为BigInteger,然后调用 keyFactory.generatePrivate(new ECPrivateKeySpec(BigInteger, ECParameterSpec)),但是实测这样在一些情况下会报错,这个时间长了记不清具体原因了,好像是因为第一位为负数的情况下会报错,我的方法是把byte[]转为Hex,然后再使用new BigInteger(hexStr, int)这种方式转为BigInteger,这个需要注意。
2.1.3 公钥压缩
publicKey.getQ().getEncoded()这个方法,有一个boolean参数,控制输出的密钥是否为压缩后的密钥,输出内容转为Hex之后,02和03开头的为压缩后的密钥,04表示未经压缩的密钥。Java里边压缩为压缩的调用同一个方法就能转为公钥对象,但是其他语言的目前不清楚,所以导出的时候,最好标注一下压缩或者未压缩。
2.2 签名验签中的坑
2.2.1 签名
Sm2签名时,有一个userId的概念,这个东西一般直接用CipherParameters对象是带不进去的,如果没有,默认是 Hex.decodeStrict(“31323334353637383132333435363738”) ,也就是1234567812345678的Ascii值,如果要使用自定义的userId,则需要使用ParametersWithID这个对象调用Signer的init,这个对象可以传入一个CipherParameters,然后再传入一个userId,可以把制定的userId带进去。
2.2.2 签名验签RS
我们平常接触的算法,一般我们调用加解密算法只返回一个值,但是Sm2算法,签名其实是有两个值,一个R,一个S,两个值构成一个签名结果,Java中bouncycastle的返回虽然也是一个值,但是大概看了一下算法的实现代码,其实得出的结果也是两个值,一个R,一个S,然后通过一个方法拼接成一个值(不确定这个方法的转换方式是不是有标准的)。在我们与C程序一块测试的时候,他们反馈了这个问题,然后我对着bouncycastle包内的org.bouncycastle.crypto.signers.SM2Signer类,摘出来了R、S和单一返回值的转换代码,已经提取到上边的代码里,可以直接使用。
总结
总之,可能是之前对加密解密以及签名验签的实际实现并不十分了解,导致现在调试的时候发生了很多问题,借助SM2的学习,补充一下相关知识,也做一个记录,避免之后忘记。这段时间也调了SM4的对称加密算法,等有时间再写一篇总结。