1 什么是SM2
RSA算法的危机在于其存在亚指数算法,对ECC算法而言一般没有亚指数攻击算法。
SM2椭圆曲线公钥密码算法:我国自主知识产权的商用密码算法,是ECC(Elliptic Curve Cryptosystem)算法的一种,基于椭圆曲线离散对数问题,计算复杂度是指数级,求解难度较大,同等安全程度要求下,椭圆曲线密码较其他公钥算法所需密钥长度小很多。
1.1 ECC算法简述
ECC的全称是Error Checking and Correction,是一种用于Nand的差错检测和修正算法。如果操作时序和电路稳定性不存在问题的话,NAND Flash出错的时候一般不会造成整个Block或是Page不能读取或是全部出错,而是整个Page(例如512Bytes)中只有一个或几个bit出错。ECC能纠正1个比特错误和检测2个比特错误,而且计算速度很快,但对1比特以上的错误无法纠正,对2比特以上的错误不保证能检测。
以上部分就是简单介绍一下什么是SM2,具体的理论请自行百度(太难了。。。。我这个小菜鸡也看不懂)。本篇主要是SM2方法的工具类以及简单的demo,工具代码已经经过实际测验可直接使用,涉及到业务相关的自行处理。
2 SM2工具类实现
首先定义一个公私钥对实体类,实体类很简单,就两个字段:
@Data
public class SM2KeyPair {
public SM2KeyPair(String publicKey, String privateKey) {
this.publicKey = publicKey;
this.privateKey = privateKey;
}
/**公钥*/
private String publicKey;
/**私钥*/
private String privateKey;
}
SM2算法处理类:
s(new SM3Digest());
}
public SM2EngineExtend(Digest digest) {
this.digest = digest;
}
/**
* 设置密文排序方式
* @param cipherMode
*/
public void setCipherMode(int cipherMode){
this.cipherMode = cipherMode;
}
/**
* 默认初始化方法,使用国密排序标准
* @param forEncryption - 是否以加密模式初始化
* @param param - 曲线参数
*/
public void init(boolean forEncryption, CipherParameters param) {
init(forEncryption, CIPHERMODE_NORM, param);
}
/**
* 默认初始化方法,使用国密排序标准
* @param forEncryption 是否以加密模式初始化
* @param cipherMode 加密数据排列模式:1-标准排序;0-BC默认排序
* @param param 曲线参数
*/
public void init(boolean forEncryption, int cipherMode, CipherParameters param) {
this.forEncryption = forEncryption;
this.cipherMode = cipherMode;
if (forEncryption) {
ParametersWithRandom rParam = (ParametersWithRandom) param;
ecKey = (ECKeyParameters) rParam.getParameters();
ecParams = ecKey.getParameters();
ECPoint s = ((ECPublicKeyParameters) ecKey).getQ().multiply(ecParams.getH());
if (s.isInfinity()) {
throw new IllegalArgumentException("invalid key: [h]Q at infinity");
}
random = rParam.getRandom();
} else {
ecKey = (ECKeyParameters) param;
ecParams = ecKey.getParameters();
}
curveLength = (ecParams.getCurve().getFieldSize() + 7) / 8;
}
/**
* 加密或解密输入数据
* @param in
* @param inOff
* @param inLen
* @return
* @throws InvalidCipherTextException
*/
public byte[] processBlock( byte[] in, int inOff, int inLen) throws InvalidCipherTextException {
if (forEncryption) {
// 加密
return encrypt(in, inOff, inLen);
} else {
return decrypt(in, inOff, inLen);
}
}
/**
* 加密实现,根据cipherMode输出指定排列的结果,默认按标准方式排列
* @param in
* @param inOff
* @param inLen
* @return
* @throws InvalidCipherTextException
*/
private byte[] encrypt(byte[] in, int inOff, int inLen)
throws InvalidCipherTextException {
byte[] c2 = new byte[inLen];
System.arraycopy(in, inOff, c2, 0, c2.length);
byte[] c1;
ECPoint kPB;
do {
BigInteger k = nextK();
ECPoint c1P = ecParams.getG().multiply(k).normalize();
c1 = c1P.getEncoded(false);
kPB = ((ECPublicKeyParameters) ecKey).getQ().multiply(k).normalize();
kdf(digest, kPB, c2);
}
while (notEncrypted(c2, in, inOff));
byte[] c3 = new byte[digest.getDigestSize()];
addFieldElement(digest, kPB.getAffineXCoord());
digest.update(in, inOff, inLen);
addFieldElement(digest, kPB.getAffineYCoord());
digest.doFinal(c3, 0);
if (cipherMode == CIPHERMODE_NORM){
return Arrays.concatenate(c1, c3, c2);
}
return Arrays.concatenate(c1, c2, c3);
}
/**
* 解密实现,默认按标准排列方式解密,解密时解出c2部分原文并校验c3部分
* @param in
* @param inOff
* @param inLen
* @return
* @throws InvalidCipherTextException
*/
private byte[] decrypt(byte[] in, int inOff, int inLen)
throws InvalidCipherTextException {
byte[] c1 = new byte[curveLength * 2 + 1];
System.arraycopy(in, inOff, c1, 0, c1.length);
ECPoint c1P = ecParams.getCurve().decodePoint(c1);
ECPoint s = c1P.multiply(ecParams.getH());
if (s.isInfinity()) {
throw new InvalidCipherTextException("[h]C1 at infinity");
}
c1P = c1P.multiply(((ECPrivateKeyParameters) ecKey).getD()).normalize();
byte[] c2 = new byte[inLen - c1.length - digest.getDigestSize()];
if (cipherMode == CIPHERMODE_BC) {
System.arraycopy(in, inOff + c1.length, c2, 0, c2.length);
}else{
// C1 C3 C2
System.arraycopy(in, inOff + c1.length + digest.getDigestSize(), c2, 0, c2.length);
}
kdf(digest, c1P, c2);
byte[] c3 = new byte[digest.getDigestSize()];
addFieldElement(digest, c1P.getAffineXCoord());
digest.update(c2, 0, c2.length);
addFieldElement(digest, c1P.getAffineYCoord());
digest.doFinal(c3, 0);
int check = 0;
// 检查密文输入值C3部分和由摘要生成的C3是否一致
if (cipherMode == CIPHERMODE_BC) {
for (int i = 0; i != c3.length; i++) {
check |= c3[i] ^ in[c1.length + c2.length + i];
}
}else{
for (int i = 0; i != c3.length; i++) {
check |= c3[i] ^ in[c1.length + i];
}
}
clearBlock(c1);
clearBlock(c3);
if (check != 0) {
clearBlock(c2);
throw new InvalidCipherTextException("invalid cipher text");
}
return c2;
}
private boolean notEncrypted(byte[] encData, byte[] in, int inOff) {
for (int i = 0; i != encData.length; i++) {
if (encData[i] != in[inOff]) {
return false;
}
}
return true;
}
private void kdf(Digest digest, ECPoint c1, byte[] encData) {
int ct = 1;
int v = digest.getDigestSize();
byte[] buf = new byte[digest.getDigestSize()];
int off = 0;
for (int i = 1; i <= ((encData.length + v - 1) / v); i++) {
addFieldElement(digest, c1.getAffineXCoord());
addFieldElement(digest, c1.getAffineYCoord());
digest.update((byte) (ct >> 24));
digest.update((byte) (ct >> 16));
digest.update((byte) (ct >> 8));
digest.update((byte) ct);
digest.doFinal(buf, 0);
if (off + buf.length < encData.length) {
xor(encData, buf, off, buf.length);
} else {
xor(encData, buf, off, encData.length - off);
}
off += buf.length;
ct++;
}
}
private void xor(byte[] data, byte[] kdfOut, int dOff, int dRemaining) {
for (int i = 0; i != dRemaining; i++) {
data[dOff + i] ^= kdfOut[i];
}
}
private BigInteger nextK() {
int qBitLength = ecParams.getN().bitLength();
BigInteger k;
do {
k = new BigInteger(qBitLength, random);
}
while (k.equals(ECConstants.ZERO) || k.compareTo(ecParams.getN()) >= 0);
return k;
}
private void addFieldElement(Digest digest, ECFieldElement v) {
byte[] p = BigIntegers.asUnsignedByteArray(curveLength, v.toBigInteger());
digest.update(p, 0, p.length);
}
/**
* clear possible sensitive data
*/
private void clearBlock(
byte[] block) {
for (int i = 0; i != block.length; i++) {
block[i] = 0;
}
}
}
最后就是SM2工具类,工具类中提供获取公私钥对,加密解密三个方法。
@Slf4j
public class SM2Utils {
/**
* SM2加密算法
* @param publicKey 公钥
* @param data 待加密的数据
* @return 密文,BC库产生的密文带由04标识符,与非BC库对接时需要去掉开头的04
*/
public static String encrypt(String publicKey, String data){
// 按国密排序标准加密
return encrypt(publicKey, data, SM2EngineExtend.CIPHERMODE_NORM);
}
/**
* SM2加密算法
* @param publicKey 公钥
* @param data 待加密的数据
* @param cipherMode 密文排列方式0-C1C2C3;1-C1C3C2;
* @return 密文,BC库产生的密文带由04标识符,与非BC库对接时需要去掉开头的04
*/
public static String encrypt(String publicKey, String data, int cipherMode){
// 获取一条SM2曲线参数
X9ECParameters sm2ECParameters = GMNamedCurves.getByName("sm2p256v1");
// 构造ECC算法参数,曲线方程、椭圆曲线G点、大整数N
ECDomainParameters domainParameters = new ECDomainParameters(sm2ECParameters.getCurve(), sm2ECParameters.getG(), sm2ECParameters.getN());
//提取公钥点
ECPoint pukPoint = sm2ECParameters.getCurve().decodePoint(Hex.decode(publicKey));
// 公钥前面的02或者03表示是压缩公钥,04表示未压缩公钥, 04的时候,可以去掉前面的04
ECPublicKeyParameters publicKeyParameters = new ECPublicKeyParameters(pukPoint, domainParameters);
SM2EngineExtend sm2Engine = new SM2EngineExtend();
// 设置sm2为加密模式
sm2Engine.init(true, cipherMode, new ParametersWithRandom(publicKeyParameters, new SecureRandom()));
byte[] arrayOfBytes = null;
try {
byte[] in = data.getBytes();
arrayOfBytes = sm2Engine.processBlock(in, 0, in.length);
} catch (Exception e) {
log.error("SM2加密时出现异常:{}", e.getMessage(), e);
}
return Hex.toHexString(arrayOfBytes);
}
/**
* 获取sm2密钥对
* BC库使用的公钥=64个字节+1个字节(04标志位),BC库使用的私钥=32个字节
* SM2秘钥的组成部分有 私钥D 、公钥X 、 公钥Y , 他们都可以用长度为64的16进制的HEX串表示,
* <br/>SM2公钥并不是直接由X+Y表示 , 而是额外添加了一个头,当启用压缩时:公钥=有头+公钥X ,即省略了公钥Y的部分
* @param compressed 是否压缩公钥(加密解密都使用BC库才能使用压缩)
* @return
*/
public static SM2KeyPair getSm2Keys(boolean compressed){
//获取一条SM2曲线参数
X9ECParameters sm2ECParameters = GMNamedCurves.getByName("sm2p256v1");
//构造domain参数
ECDomainParameters domainParameters = new ECDomainParameters(sm2ECParameters.getCurve(), sm2ECParameters.getG(), sm2ECParameters.getN());
//1.创建密钥生成器
ECKeyPairGenerator keyPairGenerator = new ECKeyPairGenerator();
//2.初始化生成器,带上随机数
try {
keyPairGenerator.init(new ECKeyGenerationParameters(domainParameters, SecureRandom.getInstance("SHA1PRNG")));
} catch (NoSuchAlgorithmException e) {
log.error("生成公私钥对时出现异常:", e);
}
//3.生成密钥对
AsymmetricCipherKeyPair asymmetricCipherKeyPair = keyPairGenerator.generateKeyPair();
ECPublicKeyParameters publicKeyParameters = (ECPublicKeyParameters)asymmetricCipherKeyPair.getPublic();
ECPoint ecPoint = publicKeyParameters.getQ();
// 把公钥放入map中,默认压缩公钥
// 公钥前面的02或者03表示是压缩公钥,04表示未压缩公钥,04的时候,可以去掉前面的04
String publicKey = Hex.toHexString(ecPoint.getEncoded(compressed));
ECPrivateKeyParameters privateKeyParameters = (ECPrivateKeyParameters) asymmetricCipherKeyPair.getPrivate();
BigInteger intPrivateKey = privateKeyParameters.getD();
// 把私钥放入map中
String privateKey = intPrivateKey.toString(16);
return new SM2KeyPair(publicKey, privateKey);
}
/**
* SM2解密算法
* @param privateKey 私钥
* @param cipherData 密文数据
* @return
*/
public static String decrypt(String privateKey, String cipherData) {
// // 按国密排序标准解密
return decrypt(privateKey, cipherData, SM2EngineExtend.CIPHERMODE_NORM);
}
/**
* SM2解密算法
* @param privateKey 私钥
* @param cipherData 密文数据
* @param cipherMode 密文排列方式0-C1C2C3;1-C1C3C2;
* @return
*/
public static String decrypt(String privateKey, String cipherData, int cipherMode) {
// 使用BC库加解密时密文以04开头,传入的密文前面没有04则补上
if (!cipherData.startsWith("04")){
cipherData = "04" + cipherData;
}
byte[] cipherDataByte = Hex.decode(cipherData);
//获取一条SM2曲线参数
X9ECParameters sm2ECParameters = GMNamedCurves.getByName("sm2p256v1");
//构造domain参数
ECDomainParameters domainParameters = new ECDomainParameters(sm2ECParameters.getCurve(), sm2ECParameters.getG(), sm2ECParameters.getN());
BigInteger privateKeyD = new BigInteger(privateKey, 16);
ECPrivateKeyParameters privateKeyParameters = new ECPrivateKeyParameters(privateKeyD, domainParameters);
SM2EngineExtend sm2Engine = new SM2EngineExtend();
// 设置sm2为解密模式
sm2Engine.init(false, cipherMode, privateKeyParameters);
String result = "";
try {
byte[] arrayOfBytes = sm2Engine.processBlock(cipherDataByte, 0, cipherDataByte.length);
return new String(arrayOfBytes);
} catch (Exception e) {
log.error("SM2解密时出现异常:{}", e.getMessage(), e);
}
return result;
}
}
2 SM2工具类测试
新建一个demo,对刚才的工具类进行测试
public static void main(String[] args) {
//获取公私钥
SM2KeyPair sm2Keys = SM2Utils.getSm2Keys(false);
System.out.println("公钥 :" + sm2Keys.getPublicKey());
System.out.println("私钥 :" + sm2Keys.getPrivateKey());
//需要加密的数据
String data = "9ol.0p;/?<>$^MJU&";
//公钥加密,获取密文
String encrypt = SM2Utils.encrypt(sm2Keys.getPublicKey(), data);
System.out.println("密文 :" + encrypt);
//私钥解密
String decrypt = SM2Utils.decrypt(sm2Keys.getPrivateKey(), encrypt);
System.out.println("解密数据 : " + decrypt);
System.out.println("明文密文是否相同 :" + data.equals(decrypt));
}
控制台输出:
公钥 :04088b8bc1191ad2bc72ef17db5e22f128ac4621442c060c5685512f6bb8b06594d7fd8027ef38d74351012a2c3794be811c2789fa13ae0c016b6f151c53706a9c
私钥 :76b8c2f70bef82029ebba73d02c0fcc53208948897a4e223869485ad36e12034
密文 :04cec3b6ed6c6d5ac30f7ed2b260add3e1c9f1406be18bc9c74b631756b67a98d86df8903a54e6f932a9138bbb1c4de6818ae2b5b1f75aea0c995ed341f0b0cd65359c3f00cc0865e640669f19cd21eb820faf13ba1c671bc883c1d30cb7bb556a6b3ec54c186032087a35fd01d63ecfec13
解密数据 : 9ol.0p;/?<>$^MJU&
明文密文是否相同 :true
到此SM2加解密实现就算完成,再实际应用中通常再前端先获取到公钥,然后通过公钥对请求数据进行加密,将加密后的密文串和公钥(加密)传递给后端,后端先通过公钥获取到对应私钥再将密文解密成明文,最后给到controller中处理。
后面还会增加签名加密和实际业务场景中前后端交互的加密解密处理。
我是王同学,喜欢这篇文章的伙伴可以点赞关注,我会经常分享技术相关博客,共同进步。