前期内容导读:
- 开源加解密RSA/AES/SHA1/PGP/SM2/SM3/SM4介绍
- 开源AES/SM4/3DES对称加密算法介绍及其实现
- 开源AES/SM4/3DES对称加密算法的验证实现
- 开源非对称加密算法RSA/SM2实现及其应用
- 开源非对称加密算法RSA实现
<dependency>
<groupId>com.biuqugroupId>
<artifactId>bq-encryptorartifactId>
<version>1.0.1version>
dependency>
public byte[] doCipher(byte[] data, byte[] key, int cipherMode)
{
SM2Engine.Mode mode = SM2Engine.Mode.C1C2C3;
if (!this.getPaddingMode().equalsIgnoreCase(String.valueOf(DEFAULT_MODE)))
{
mode = SM2Engine.Mode.C1C3C2;
}
SM2Engine sm2Engine = new SM2Engine(mode);
this.initSm2Engine(sm2Engine, key, cipherMode);
try
{
return sm2Engine.processBlock(data, 0, data.length);
}
catch (Exception e)
{
throw new EncryptionException("failed to do sm2 cipher.", e);
}
}
private void initSm2Engine(SM2Engine sm2Engine, byte[] key, int cipherMode)
{
if (Cipher.ENCRYPT_MODE == cipherMode)
{
ECPublicKey keyObj = (ECPublicKey)this.toPubKey(key);
ECDomainParameters domainParam = this.getDomainParam(keyObj);
ECKeyParameters keyParam = new ECPublicKeyParameters(keyObj.getQ(), domainParam);
byte[] initKey = UUID.randomUUID().toString().getBytes(StandardCharsets.UTF_8);
sm2Engine.init(true, new ParametersWithRandom(keyParam, this.createRandom(initKey)));
}
else
{
ECPrivateKey keyObj = (ECPrivateKey)this.toPriKey(key);
ECDomainParameters domainParam = this.getDomainParam(keyObj);
ECKeyParameters keyParam = new ECPrivateKeyParameters(keyObj.getD(), domainParam);
sm2Engine.init(false, keyParam);
}
}
说明:
- 上面的代码阐述了加解密的核心流程:根据二进制生成秘钥,再基于单独的API计算得到加解密结果,该计算逻辑完全不同于以往的加解密API;
- 通过上述核心代码逻辑,再对比上篇5.非对称加密算法RSA实现,可知SM2本身是支持分段的;
- 通过秘钥二进制反向生成秘钥对象是一个有意思且有点复杂的事情,后面再单独说明;
秘钥生成逻辑
public KeyPair createKey(byte[] initKey)
{
try
{
ECGenParameterSpec paramSpec = new ECGenParameterSpec(SM2_VERSION);
KeyPairGenerator keyGen = KeyPairGenerator.getInstance(ALGORITHM, this.getProvider());
if (null == initKey)
{
initKey = UUID.randomUUID().toString().getBytes(StandardCharsets.UTF_8);
}
SecureRandom random = this.createRandom(initKey);
keyGen.initialize(paramSpec, random);
return keyGen.generateKeyPair();
}
catch (Exception e)
{
throw new EncryptionException("failed to get sm2 key.", e);
}
}
公钥、私钥反向生成逻辑
public PublicKey toPubKey(byte[] pubKey)
{
try
{
String hexKey = Hex.toHexString(pubKey);
KeyFactory kf = KeyFactory.getInstance(ALGORITHM, this.getProvider());
if (hexKey.startsWith(STANDARD_HEX_KEY_PREFIX))
{
return kf.generatePublic(new X509EncodedKeySpec(pubKey));
}
else
{
// 获取SM2相关参数
X9ECParameters ecParam = GMNamedCurves.getByName(SM2_VERSION);
// 将公钥HEX字符串转换为椭圆曲线对应的点
ECCurve ecCurve = ecParam.getCurve();
ECPoint ecPoint = ecCurve.decodePoint(pubKey);
// 椭圆曲线参数规格
ECParameterSpec ecSpec = new ECParameterSpec(ecCurve, ecParam.getG(), ecParam.getN(), ecParam.getH());
// 将椭圆曲线点转为公钥KEY对象
return kf.generatePublic(new ECPublicKeySpec(ecPoint, ecSpec));
}
}
catch (Exception e)
{
throw new EncryptionException("failed to get sm2 pub key.", e);
}
}
public PrivateKey toPriKey(byte[] priKey)
{
try
{
String hexKey = Hex.toHexString(priKey);
KeyFactory kf = KeyFactory.getInstance(ALGORITHM, this.getProvider());
if (hexKey.startsWith(STANDARD_HEX_KEY_PREFIX))
{
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(priKey);
return kf.generatePrivate(keySpec);
}
else
{
// 获取SM2相关参数
X9ECParameters ecParam = GMNamedCurves.getByName(SM2_VERSION);
ECCurve ecCurve = ecParam.getCurve();
// 椭圆曲线参数规格
ECParameterSpec ecSpec = new ECParameterSpec(ecCurve, ecParam.getG(), ecParam.getN(), ecParam.getH());
// 将私钥HEX字符串转换为16进制的数字值
BigInteger bigInteger = new BigInteger(Hex.toHexString(priKey), EncryptionConst.HEX_UNIT);
// 将X值转为私钥KEY对象
return kf.generatePrivate(new ECPrivateKeySpec(bigInteger, ecSpec));
}
}
catch (Exception e)
{
throw new EncryptionException("failed to get sm2 pri key.", e);
}
}
说明:SM2基于椭圆的原理来加解密,其秘钥生成和解析方式也与其它方式不同。
- SM2支持标准的秘钥生成方式:
BaseSingleSignature sm2 = new Sm2Encryption(); KeyPair keyPair = sm2.createKey(UUID.randomUUID().toString().getBytes(StandardCharsets.UTF_8)); byte[] priKey0 = keyPair.getPrivate().getEncoded(); byte[] pubKey0 = keyPair.getPublic().getEncoded();
- SM2支持非标准的秘钥生成方式:
BaseSingleSignature sm2 = new Sm2Encryption(); KeyPair keyPair = sm2.createKey(UUID.randomUUID().toString().getBytes(StandardCharsets.UTF_8)); byte[] priKey1 = ((BCECPrivateKey)keyPair.getPrivate()).getD().toByteArray(); byte[] pubKey1 = ((BCECPublicKey)keyPair.getPublic()).getQ().getEncoded(false); byte[] pubKey2 = ((BCECPublicKey)keyPair.getPublic()).getQ().getEncoded(true);
- 上述正文部分的秘钥转换逻辑可以无感兼容上述各种秘钥场景。有兴趣可以看看此算法的单元测试类。PS:网上资料通常只描述了其中一种秘钥生成场景,但是相互间是不兼容的。
签名和验签判定逻辑:
public byte[] sign(byte[] data, byte[] key)
{
try
{
PrivateKey priKey = this.toPriKey(key);
Signature signature = Signature.getInstance(this.getSignatureAlg(), this.getProvider());
signature.initSign(priKey);
signature.update(data);
return signature.sign();
}
catch (Exception e)
{
throw new EncryptionException("failed to signature.", e);
}
}
public boolean verify(byte[] data, byte[] key, byte[] sign)
{
try
{
PublicKey pubKey = this.toPubKey(key);
Signature signature = Signature.getInstance(this.getSignatureAlg(), this.getProvider());
signature.initVerify(pubKey);
signature.update(data);
return signature.verify(sign);
}
catch (Exception e)
{
throw new EncryptionException("failed to verify signature.", e);
}
}
SM2加密批量验证逻辑
@Test
public void encrypt()
{
int[] encLengths = {256};
super.encrypt(encLengths);
}
@Test
public void testEncryptAndSign()
{
String initKey = UUID.randomUUID() + new String(RandomUtils.nextBytes(5000), StandardCharsets.UTF_8);
int[] encLengths = {256};
BaseSingleSignature encryption = new Sm2Encryption();
for (int encLen : encLengths)
{
encryption.setEncryptLen(encLen);
KeyPair keyPair = encryption.createKey(initKey.getBytes(StandardCharsets.UTF_8));
super.testEncryptAndSign(encryption, keyPair.getPrivate().getEncoded(), keyPair.getPublic().getEncoded());
}
}
说明:
- SM2可以不用设置加密长度,因为默认只有一个,同理我们也无需关心其填充算法;
- 通过单元测试对比RSA算法可知,SM2由于秘钥非常短,其秘钥生成和加解密效率明显高于RSA;
BouncyCastle
的SM2
由于其算法独特性,与其它的算法实现差异加大,但是在实际使用时,由于其秘钥非常短,在核心的加解密执行效率上是有一定优势的;秘改
(国密改造)场景时,还无法做到。因为大部分开源组件只支持标准协议,一般支持p256v1
非对称加密,但是不支持sm2p256v1
,国人仍需努力。