简介
SM2是非对称加密算法,一提非对称加密算法,第一想到的是RSA,没错,这个就是替代RSA的。
它是基于椭圆曲线密码的公钥密码算法标准,其秘钥长度256bit,包含数字签名、密钥交换和公钥加密,用于替换RSA/DH/ECDSA/ECDH等国际算法。可以满足电子认证服务系统等应用需求,由国家密码管理局于2010年12月17号发布。
SM2采用的是ECC 256位的一种,其安全强度比RSA 2048位高,且运算速度快于RSA。随着密码技术和计算技术的发展,目前常用的1024位RSA算法面临严重的安全威胁,我们国家密码管理部门经过研究,决定采用SM2椭圆曲线算法替换RSA算法。SM2算法在安全性、性能上都具有优势。
用途
可以用于前后端传输数据加密解密
可以用于对数据加签验签,确保报文的安全性和完整性。
比如,生成一套前端公私钥密钥对,生成一套后端服务器公私钥密钥对。
前端把参数json字符串通过服务器公钥用sm2算法加密,服务器后端接收到请求后用服务器私钥解密,拿到原始参数,处理数据并生成响应数据,把响应数据用前端公钥加密,前端接收到响应加密后数据,用前端私钥解密,拿到响应json。这个过程是快速且安全的。
一般这个过程在网关上公共实现。
java使用
maven引入bcprov-jdk15on jar包,截止发文,最新版本是1.70
org.bouncycastle
bcprov-jdk15on
1.70
新建StandardSM2Engine实体类
package com.zhaohy.app.utils;
import java.io.IOException;
import java.math.BigInteger;
import java.security.SecureRandom;
import org.bouncycastle.asn1.ASN1EncodableVector;
import org.bouncycastle.asn1.ASN1InputStream;
import org.bouncycastle.asn1.ASN1Integer;
import org.bouncycastle.asn1.ASN1OctetString;
import org.bouncycastle.asn1.ASN1Sequence;
import org.bouncycastle.asn1.DEROctetString;
import org.bouncycastle.asn1.DERSequence;
import org.bouncycastle.crypto.CipherParameters;
import org.bouncycastle.crypto.Digest;
import org.bouncycastle.crypto.InvalidCipherTextException;
import org.bouncycastle.crypto.digests.SM3Digest;
import org.bouncycastle.crypto.engines.SM2Engine.Mode;
import org.bouncycastle.crypto.params.ECDomainParameters;
import org.bouncycastle.crypto.params.ECKeyParameters;
import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
import org.bouncycastle.crypto.params.ParametersWithRandom;
import org.bouncycastle.math.ec.ECFieldElement;
import org.bouncycastle.math.ec.ECMultiplier;
import org.bouncycastle.math.ec.ECPoint;
import org.bouncycastle.math.ec.FixedPointCombMultiplier;
import org.bouncycastle.util.Arrays;
import org.bouncycastle.util.BigIntegers;
import org.bouncycastle.util.Memoable;
import org.bouncycastle.util.Pack;
/**
* 自定义SM2Engine类,对加密解密数据进行了ASN.1编码
*/
public class StandardSM2Engine {
private final Digest digest;
private final Mode mode;
private boolean forEncryption;
private ECKeyParameters ecKey;
private ECDomainParameters ecParams;
private int curveLength;
private SecureRandom random;
public StandardSM2Engine() {
this(new SM3Digest());
}
public StandardSM2Engine(Mode mode) {
this(new SM3Digest(), mode);
}
public StandardSM2Engine(Digest digest) {
this(digest, Mode.C1C2C3);
}
public StandardSM2Engine(Digest digest, Mode mode) {
if (mode == null) {
throw new IllegalArgumentException("mode cannot be NULL");
}
this.digest = digest;
this.mode = mode;
}
public void init(boolean forEncryption, CipherParameters param) {
this.forEncryption = forEncryption;
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;
}
public byte[] processBlock(byte[] in, int inOff, int inLen) throws InvalidCipherTextException {
if (forEncryption) {
return encrypt(in, inOff, inLen);
} else {
return decrypt(in, inOff, inLen);
}
}
public int getOutputSize(int inputLen) {
return (1 + 2 * curveLength) + inputLen + digest.getDigestSize();
}
protected ECMultiplier createBasePointMultiplier() {
return new FixedPointCombMultiplier();
}
/**
* 加密
*
* @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);
ECMultiplier multiplier = createBasePointMultiplier();
ECPoint c1P;
ECPoint kPB;
do {
BigInteger k = nextK();
c1P = multiplier.multiply(ecParams.getG(), k).normalize();
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);
return convertToASN1(c1P, c2, c3);
}
/**
* 解密
*
* @param in
* @param inOff
* @param inLen
* @return
* @throws InvalidCipherTextException
*/
private byte[] decrypt(byte[] in, int inOff, int inLen) throws InvalidCipherTextException {
byte[] decryptData = new byte[inLen];
System.arraycopy(in, inOff, decryptData, 0, decryptData.length);
BigInteger x;
BigInteger y;
byte[] originC3;
byte[] c2;
ECPoint c1P;
byte[] c1;
try (ASN1InputStream aIn = new ASN1InputStream(decryptData)) {
ASN1Sequence seq;
try {
seq = (ASN1Sequence) aIn.readObject();
} catch (IOException e) {
throw new InvalidCipherTextException();
}
x = ASN1Integer.getInstance(seq.getObjectAt(0)).getValue();
y = ASN1Integer.getInstance(seq.getObjectAt(1)).getValue();
c1P = ecParams.getCurve().validatePoint(x, y);
c1 = c1P.getEncoded(false);
if (mode == Mode.C1C3C2) {
originC3 = ASN1OctetString.getInstance(seq.getObjectAt(2)).getOctets();
c2 = ASN1OctetString.getInstance(seq.getObjectAt(3)).getOctets();
} else {
c2 = ASN1OctetString.getInstance(seq.getObjectAt(2)).getOctets();
originC3 = ASN1OctetString.getInstance(seq.getObjectAt(3)).getOctets();
}
} catch (IOException e) {
throw new InvalidCipherTextException();
}
ECPoint s = c1P.multiply(ecParams.getH());
if (s.isInfinity()) {
throw new InvalidCipherTextException("[h]C1 at infinity");
}
c1P = c1P.multiply(((ECPrivateKeyParameters) ecKey).getD()).normalize();
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;
for (int i = 0; i != c3.length; i++) {
check |= c3[i] ^ originC3[i];
}
Arrays.fill(c1, (byte) 0);
Arrays.fill(c3, (byte) 0);
if (check != 0) {
Arrays.fill(c2, (byte) 0);
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 + i]) {
return false;
}
}
return true;
}
private void kdf(Digest digest, ECPoint c1, byte[] encData) {
int digestSize = digest.getDigestSize();
byte[] buf = new byte[Math.max(4, digestSize)];
int off = 0;
Memoable memo = null;
Memoable copy = null;
if (digest instanceof Memoable) {
addFieldElement(digest, c1.getAffineXCoord());
addFieldElement(digest, c1.getAffineYCoord());
memo = (Memoable) digest;
copy = memo.copy();
}
int ct = 0;
while (off < encData.length) {
if (memo != null) {
memo.reset(copy);
} else {
addFieldElement(digest, c1.getAffineXCoord());
addFieldElement(digest, c1.getAffineYCoord());
}
Pack.intToBigEndian(++ct, buf, 0);
digest.update(buf, 0, 4);
digest.doFinal(buf, 0);
int xorLen = Math.min(digestSize, encData.length - off);
xor(encData, buf, off, xorLen);
off += xorLen;
}
}
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 = BigIntegers.createRandomBigInteger(qBitLength, random);
} while (k.equals(BigIntegers.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);
}
private byte[] convertToASN1(ECPoint c1P, byte[] c2, byte[] c3) {
ASN1Integer x = new ASN1Integer(c1P.getXCoord().toBigInteger());
ASN1Integer y = new ASN1Integer(c1P.getYCoord().toBigInteger());
DEROctetString derDig = new DEROctetString(c3);
DEROctetString derEnc = new DEROctetString(c2);
ASN1EncodableVector v = new ASN1EncodableVector();
switch (mode) {
case C1C3C2:
v.add(x);
v.add(y);
v.add(derDig);
v.add(derEnc);
break;
default:
v.add(x);
v.add(y);
v.add(derEnc);
v.add(derDig);
}
DERSequence seq = new DERSequence(v);
try {
return seq.getEncoded();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
新建Sm2Utils工具类
package com.zhaohy.app.utils;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.Security;
import java.security.Signature;
import java.security.SignatureException;
import java.security.spec.ECGenParameterSpec;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import org.apache.commons.codec.DecoderException;
import org.apache.commons.codec.binary.Hex;
import org.bouncycastle.asn1.gm.GMObjectIdentifiers;
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.ParametersWithRandom;
import org.bouncycastle.jcajce.provider.asymmetric.util.ECUtil;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
/**
* SM2工具类
*/
public class Sm2Utils {
/**
* 加签
* @param plainText
* @return
*/
public static String sign(String plainText, String privateKeyStr) {
BouncyCastleProvider provider = new BouncyCastleProvider();
try {
// 获取椭圆曲线KEY生成器
KeyFactory keyFactory = KeyFactory.getInstance("EC", provider);
byte[] privateKeyData = Base64.getDecoder().decode(privateKeyStr);
PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(privateKeyData);
Signature rsaSignature = Signature.getInstance(GMObjectIdentifiers.sm2sign_with_sm3.toString(), provider);
rsaSignature.initSign(keyFactory.generatePrivate(privateKeySpec));
rsaSignature.update(plainText.getBytes());
byte[] signed = rsaSignature.sign();
return Base64.getEncoder().encodeToString(signed);
} catch (NoSuchAlgorithmException | InvalidKeySpecException | InvalidKeyException | SignatureException e) {
throw new RuntimeException(e);
}
}
/**
* 验签
* @param plainText
* @param signatureValue
* @return
*/
public static boolean verify(String plainText, String signatureValue, String publicKeyStr) {
BouncyCastleProvider provider = new BouncyCastleProvider();
try {
// 获取椭圆曲线KEY生成器
KeyFactory keyFactory = KeyFactory.getInstance("EC", provider);
byte[] publicKeyData = Base64.getDecoder().decode(publicKeyStr);
X509EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(publicKeyData);
// 初始化为验签状态
Signature signature = Signature.getInstance(GMObjectIdentifiers.sm2sign_with_sm3.toString(), provider);
signature.initVerify(keyFactory.generatePublic(publicKeySpec));
signature.update(Hex.decodeHex(plainText.toCharArray()));
return signature.verify(Hex.decodeHex(signatureValue.toCharArray()));
} catch (NoSuchAlgorithmException | InvalidKeySpecException | InvalidKeyException | SignatureException e) {
throw new RuntimeException(e);
} catch (IllegalArgumentException e) {
LogUtils.error("验签失败", e);
return false;
} catch (DecoderException e) {
LogUtils.error("验签失败", e);
return false;
}
}
/**
* 加密
* @param plainText
* @return
*/
public static byte[] encrypt(String plainText, String publicKeyStr) throws Exception {
Security.addProvider(new BouncyCastleProvider());
try {
// 获取椭圆曲线KEY生成器
KeyFactory keyFactory = KeyFactory.getInstance("EC");
byte[] publicKeyData = Base64.getDecoder().decode(publicKeyStr);
X509EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(publicKeyData);
PublicKey publicKey = keyFactory.generatePublic(publicKeySpec);
CipherParameters publicKeyParamerters = ECUtil.generatePublicKeyParameter(publicKey);
//数据加密
StandardSM2Engine engine = new StandardSM2Engine(new SM3Digest(), SM2Engine.Mode.C1C3C2);
engine.init(true, new ParametersWithRandom(publicKeyParamerters));
byte[] encryptData = engine.processBlock(plainText.getBytes(), 0, plainText.getBytes().length);
return encryptData;
} catch (NoSuchAlgorithmException | InvalidKeySpecException
| InvalidKeyException | InvalidCipherTextException e) {
throw new RuntimeException(e);
}
}
/**
* 解密
* @param encryptedText
* @return
*/
public static String decrypt(byte[] encryptedData, String privateKeyStr) {
Security.addProvider(new BouncyCastleProvider());
try {
// 获取椭圆曲线KEY生成器
KeyFactory keyFactory = KeyFactory.getInstance("EC");
byte[] privateKeyData = Base64.getDecoder().decode(privateKeyStr);
PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(privateKeyData);
PrivateKey privateKey = keyFactory.generatePrivate(privateKeySpec);
CipherParameters privateKeyParamerters = ECUtil.generatePrivateKeyParameter(privateKey);
//数据解密
StandardSM2Engine engine = new StandardSM2Engine(new SM3Digest(), SM2Engine.Mode.C1C3C2);
engine.init(false, privateKeyParamerters);
byte[] plainText = engine.processBlock(encryptedData, 0, encryptedData.length);
return new String(plainText);
} catch (NoSuchAlgorithmException | InvalidKeySpecException
| InvalidKeyException | InvalidCipherTextException e) {
throw new RuntimeException(e);
}
}
/**
* SM2算法生成密钥对
* @return 密钥对信息
*/
public static KeyPair generateSm2KeyPair() {
try {
final ECGenParameterSpec sm2Spec = new ECGenParameterSpec("sm2p256v1");
// 获取一个椭圆曲线类型的密钥对生成器
final KeyPairGenerator kpg = KeyPairGenerator.getInstance("EC", new BouncyCastleProvider());
SecureRandom random = new SecureRandom();
// 使用SM2的算法区域初始化密钥生成器
kpg.initialize(sm2Spec, random);
// 获取密钥对
KeyPair keyPair = kpg.generateKeyPair();
return keyPair;
} catch (Exception e) {
LogUtils.error("generate sm2 key pair failed:{}", e.getMessage(), e);
return null;
}
}
public static void main(String[] args) throws Exception {
KeyPair keyPair = generateSm2KeyPair();
String privateKey = Base64.getEncoder().encodeToString(keyPair.getPrivate().getEncoded());
String publicKey = Base64.getEncoder().encodeToString(keyPair.getPublic().getEncoded());
String data = "{\"daId\":\"123456\"}";
String encryptedJsonStr = Hex.encodeHexString(encrypt(data, publicKey)) + "";//16进制字符串
String decryptedJsonStr = decrypt(Hex.decodeHex(encryptedJsonStr), privateKey);
String sign = Hex.encodeHexString(Base64.getDecoder().decode(sign(data, privateKey)));
boolean flag = verify(Hex.encodeHexString(data.getBytes()), sign, publicKey);
System.out.println("base64后privateKey:" + privateKey);
System.out.println("base64后publicKey:" + publicKey);
System.out.println("加密前数据:" + data);
System.out.println("公钥加密后16进制字符串:" + encryptedJsonStr);
System.out.println("私钥解密后数据:" + decryptedJsonStr);
System.out.println("私钥加签后数据(16进制):" + sign);
System.out.println("公钥验签结果:" + flag);
}
}
可以看到工具类里已经写好了调用测试main方法,运行结果如下:
base64后privateKey:MIGTAgEAMBMGByqGSM49AgEGCCqBHM9VAYItBHkwdwIBAQQgmze2hmNoi3XpeMPmLNIQBOC7+P1iMbwGMMAdQXko0VWgCgYIKoEcz1UBgi2hRANCAATyF9jHsCvFaLkR4DS+viX9CShZEXc7Nc1OueDaIpzZx/DRTRejSpTOcFmb0B9TsyYutnRWAx46nfkQ289NVXjg
base64后publicKey:MFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAE8hfYx7ArxWi5EeA0vr4l/QkoWRF3OzXNTrng2iKc2cfw0U0Xo0qUznBZm9AfU7MmLrZ0VgMeOp35ENvPTVV44A==
加密前数据:{"daId":"123456"}
公钥加密后16进制字符串:307b022100f1ab8b4fc8755d2ab84bf530f15ab14f5250d060d52679fd38e822a85eeceb860221009eef85a579ef9e2f27f44461a89046b95f8c630773be7ced1f3f3f48057f4777042064d9b5fb396591e022791858e334fc40b1ee9c93412442d403385266a00dd78704117b30ed38afcb361f8176acbbfbabad71e9
私钥解密后数据:{"daId":"123456"}
私钥加签后数据(16进制):304402201505caa731d9b70987a6c19f40618593e9acd8cad880dc001e52ef4b48638e2302204bdb5c5dc881789e716de7360aa387e5e4caa9f9f2fd6d1fbce35f1227b34440
公钥验签结果:true
可以看到加密解密加签验签都是成功的。
上面加密后生成的是byte[],转换成16进制字符串处理的,也可以用base64实现byte[]和字符串的转换处理,但是要保证对应加解密都是用base64实现byte[]和字符串的转换,对应的上就行。
java实现byte[]和16进制字符串之间的转换是通过Apache Commons Codec包来实现的,一般springboot里有依赖这个,如果没有,则maven引入:
commons-codec
commons-codec
1.15