前期内容导读:
- 开源加解密RSA/AES/SHA1/PGP/SM2/SM3/SM4介绍
- 开源AES/SM4/3DES对称加密算法介绍及其实现
- 开源AES/SM4/3DES对称加密算法的验证实现
- 开源非对称加密算法RSA/SM2实现及其应用
<dependency>
<groupId>com.biuqugroupId>
<artifactId>bq-encryptorartifactId>
<version>1.0.1version>
dependency>
加解密核心逻辑
public byte[] doCipher(byte[] data, byte[] key, int cipherMode)
{
try
{
//1.获取秘钥对象
Key algKey = toKey(key);
//2.根据填充类型获取加密对象
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding", BouncyCastleProvider.PROVIDER_NAME);
//3.初始化加密对象
cipher.init(cipherMode, algKey);
byte[] partData = cipher.doFinal(data, 0, data.length);
return partData;
}
catch (Exception e)
{
throw new EncryptionException("do rsa encrypt/decrypt error.", e);
}
}
说明:
- 上面的代码阐述了加解密的核心流程:根据二进制生成秘钥,再基于加密对象填充数据获得结果;
- 通过上述核心代码逻辑验证每种填充算法最大可加密多少明文byte;
- 通过秘钥二进制反向生成秘钥对象是一个有意思且有点复杂的事情,后面再单独说明;
受RSA算法的加密长度、填充算法、明文长度、BouncyCastle
不支持加分段加密的影响(在开源非对称加密算法RSA/SM2实现及其应用 中有介绍),上述核心逻辑是无法商用的,可商用的逻辑如下:
public byte[] doCipher(byte[] data, byte[] key, int cipherMode)
{
ByteArrayOutputStream out = new ByteArrayOutputStream();
try
{
//1.获取秘钥对象
Key algKey = toKey(key);
//2.根据填充类型获取加密对象
Cipher cipher;
if (null == this.getPaddingMode())
{
cipher = Cipher.getInstance(this.getAlgorithm());
}
else
{
cipher = Cipher.getInstance(this.getPaddingMode(), this.getProvider());
}
//3.初始化加密对象
cipher.init(cipherMode, algKey);
//4.根据RSA类型获取每次处理报文的最大字节数
int maxLen = this.rsaType.getDecryptLen(this.getPaddingMode());
if (cipherMode == Cipher.DECRYPT_MODE)
{
maxLen = this.rsaType.getEncryptLen();
}
//5.分段加解密
int start = 0;
while (start < data.length)
{
//5.1获取每次的起始位置
int limit = start + maxLen;
limit = Math.min(limit, data.length);
//5.2分段加解密后,把该段报文写入缓存
byte[] partData = cipher.doFinal(data, start, limit - start);
out.write(partData, 0, partData.length);
//5.3把分段的起始位置挪至上一次的结束位置
start = limit;
}
return out.toByteArray();
}
catch (Exception e)
{
throw new EncryptionException("do rsa encrypt/decrypt error.", e);
}
finally
{
IOUtils.closeQuietly(out);
}
}
说明:
- 在加密的核心逻辑上,加了秘钥长度和填充长度的关系处理;
- 在单次加密正常后,还迭代对超长的明文做了循环截取加密;
public KeyPair createKey(byte[] initKey)
{
try
{
KeyPairGenerator keyGenerator = KeyPairGenerator.getInstance(this.getAlgorithm(), this.getProvider());
if (null != initKey)
{
SecureRandom random = this.createRandom(initKey);
keyGenerator.initialize(this.getEncryptLen(), random);
}
else
{
keyGenerator.initialize(this.getEncryptLen());
}
return keyGenerator.generateKeyPair();
}
catch (Exception e)
{
throw new EncryptionException("create rsa key pair error.", e);
}
}
public PublicKey toPubKey(byte[] pubKey)
{
try
{
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(pubKey);
KeyFactory keyFactory = KeyFactory.getInstance(this.getAlgorithm());
return keyFactory.generatePublic(keySpec);
}
catch (Exception e)
{
throw new EncryptionException("get rsa public key error.", e);
}
}
public PrivateKey toPriKey(byte[] priKey)
{
try
{
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(priKey);
KeyFactory keyFactory = KeyFactory.getInstance(this.getAlgorithm());
return keyFactory.generatePrivate(keySpec);
}
catch (Exception e)
{
throw new EncryptionException("get rsa private key error.", e);
}
}
说明:
- 上述几段秘钥相关的代码可以把秘钥转成二进制,也可以把秘钥二进制反向转成秘钥对象,但是是怎么知道秘钥二进制是私钥或是公钥呢?
private Key toKey(byte[] key)
{
Key rsaKey;
if (this.rsaType.isPriKey(key))
{
rsaKey = toPriKey(key);
}
else
{
rsaKey = toPubKey(key);
}
return rsaKey;
}
/**
* 是否是私钥
*
* 经统计,规则如下:
* 1.私钥长度介于加密算法长度的(1/2-1)
* 2.公钥介于加密算法长度的(1/8-1/2)
*
* @param key 秘钥二进制
* @return true表示私钥
*/
public boolean isPriKey(byte[] key)
{
if (null != key && key.length > 0)
{
int keyLen = key.length;
int maxKeyLen = this.getLen();
int minKeyLen = maxKeyLen / PRI_RATIO;
return (keyLen < maxKeyLen && keyLen > minKeyLen);
}
return false;
}
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);
}
}
@Test
public void encrypt()
{
int[] encLengths = {1024, 2048, 3072, 4096};
List<String> paddings = new ArrayList<>();
paddings.add("RSA/NONE/NoPadding");
paddings.add("RSA/ECB/OAEPPadding");
paddings.add("RSA/ECB/PKCS1Padding");
paddings.add("RSA/ECB/NoPadding");
//公钥加密
super.encrypt(encLengths, paddings);
//私钥加密
super.encrypt(encLengths, paddings, false);
}
@Test
public void testEncryptAndSign()
{
String initKey = UUID.randomUUID() + new String(RandomUtils.nextBytes(5000), StandardCharsets.UTF_8);
int[] encLengths = {1024, 2048, 3072, 4096};
List<String> paddings = new ArrayList<>();
paddings.add("RSA/ECB/OAEPPadding");
paddings.add("RSA/ECB/PKCS1Padding");
BaseSingleSignature encryption = new RsaEncryption();
for (String padding : paddings)
{
encryption.setPaddingMode(padding);
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());
}
}
}
说明:
- 上述验证代码中,一旦设置成
RSA/NONE/NoPadding
或者RSA/ECB/NoPadding
,就有大概率会报错,排除掉NoPadding
则一切正常;
BouncyCastle
代码整体设计比较优雅,非常容易做到RSA的多种加密长度的兼容。本开源加密组件初期仅支持1024/2048,后面很快就扩展支持了3072/4096加密长度、OAEPPadding填充模式;NoPadding
在较长数据加密时,基本上都会出现异常,初步怀疑是BouncyCastle
的bug,但是该模式不安全、也没人使用,就不去跟进解决了;3072
/4096
生成秘钥非常慢;但是各种加密长度下,整体加密耗时约在100ms+(以1000byte字节为例),解密在5ms以内;