一、起因
最近在工作中,突然一个交易异常缓慢,调用时间高达40wms,严重影响了整个集群的生产效率。得知这个情况,我开始对整个交易进行剖析,最终发现原来是RSA非对称加密算法的锅。
RSA作为一种非对称的加密算法,其中很重要的一特点是当数据在网络中传输时,用来加密数据的密钥并不需要也和数据一起传送。因此,这就减少了密钥泄露的可能性。RSA在不允许加密方解密数据时也很有用,加密的一方使用一个密钥,称为公钥,解密的一方使用另一个密钥,称为私钥,私钥需要保持其私有性。
RSA非常安全,但是效率却很低,本身的秘钥长度就是1024bit,随着大数解析破解技术的发展,秘钥的长度还要进一步增加。同时,因为RSA生成随机数解密的时候会对线程加锁,会出现线程拥堵的问题。当然,因为使用RSA加解密的交易并不多,出现拥堵估计是特殊情况。
总而言之,RSA是一种低效的加密方法,用在加密大量数据上面是不合适的,即使是签名之类的地方,能尽量少用也要少用,否则对性能影响很大。
可是能之前的同事写这些代码时候存在局限,在一个对安全性要求不是特别高的地方用RSA进行加密,同时没有考虑到这个模块会有那么多的调用。
于是乎,经过测试和思考,最终决定对加密算法进行升级,用SM4算法。
二、SM4国密算法介绍
SMS4算法是在国内广泛使用的WAPI无线网络标准中使用的加密算法,是一种32轮的迭代非平衡Feistel结构的分组加密算法,其密钥长度和分组长度均为128。SMS4算法的加解密过程中使用的算法是完全相同的,唯一不同点在于该算法的解密密钥是由它的加密密钥进行逆序变换后得到的。
我准备研究SM4算法的时候,中国互联网络信息中心里已经不提供SM4算法的标准文档了,所以只能去网上查阅资料。感谢一文带你学会国产加密算法SM4的java实现方案这篇文章。
1、SM4算法的原理
结构图:
我们能看出来,明文加上128bit的秘钥,进过多轮变换,最后输出是加密的反序,解密时只是将轮密钥的使用顺序进行逆向进行。
2、SM4加密算法应用场景
SM4常用于政府系统的数据传输加密,我们使用前端向后端传递信息,或者分布式场景下不同模块之间的调用,可以使用此算法。对参数的数据进行加密,然后后台对加密的数据进行解密再存储到数据库中,保证数据传输过程中,不受泄露。
3、SM4算法java的实现
现在我就来简单的实现一下SM4算法的加解密功能
首先是一些常数值,包括编码,算法名,秘钥长度等,如果可以的写成参数值最好。
private static final String ENCODING = "UTF-8";
public static final String ALGORIGTHM_NAME = "SM4";
public static final String ALGORITHM_NAME_ECB_PADDING = "SM4/ECB/PKCS7Padding";
public static final int DEFAULT_KEY_SIZE = 128;
接下来就是生成ECB的加解密了,加解密是通过后台自己掌握的秘钥进行的,因为秘钥生成的算法是一致的,而且是作为jar包来使用,所以可以写在一起。
这个函数的作用是使用ECB模式(ECB(Electronic Codebook,电码本),ECB模式是分组密码的一种最基本的工作模式。根据length的长度来进行循环,到底是加密还是解密,主要是根据第二个参数data来进行决定。
这里放张图片简单介绍一下几种模式。
/**
* @Description:生成ecb秘钥
*/
private static Cipher generateEcbCipher(String algorithmName, int mode, byte[] key) throws Exception {
Cipher cipher = Cipher.getInstance(algorithmName,BouncyCastleProvider.PROVIDER_NAME);
Key sm4Key = new SecretKeySpec(key, ALGORIGTHM_NAME);
cipher.init(mode, sm4Key);
return cipher;
}
/**
* @Description:自动生成密钥
*/
public static byte[] generateKey() throws Exception {
return generateKey(DEFAULT_KEY_SIZE);
}
public static byte[] generateKey(int keySize) throws Exception {
KeyGenerator kg = KeyGenerator.getInstance(ALGORIGTHM_NAME, BouncyCastleProvider.PROVIDER_NAME);
kg.init(keySize, new SecureRandom());
return kg.generateKey().getEncoded();
}
具体加密解密类的实现如下:
/**
* @Description:加密
*/
public static String encryptEcb(String hexKey, String paramStr, String charset) throws Exception {
String cipherText = "";
if (null != paramStr && !"".equals(paramStr)) {
byte[] keyData = ByteUtils.fromHexString(hexKey);
charset = charset.trim();
if (charset.length() <= 0) {
charset = ENCODING;
}
byte[] srcData = paramStr.getBytes(charset);
byte[] cipherArray = encrypt_Ecb_Padding(keyData, srcData);
cipherText = ByteUtils.toHexString(cipherArray);
}
return cipherText;
}
/**
* @Description:加密模式之ecb
*/
public static byte[] encrypt_Ecb_Padding(byte[] key, byte[] data) throws Exception {
Cipher cipher = generateEcbCipher(ALGORITHM_NAME_ECB_PADDING, Cipher.ENCRYPT_MODE, key);
byte[] bs = cipher.doFinal(data);
return bs;
/**
* @Description:sm4解密
*/
public static String decryptEcb(String hexKey, String cipherText, String charset) throws Exception {
String decryptStr = "";
byte[] keyData = ByteUtils.fromHexString(hexKey);
byte[] cipherData = ByteUtils.fromHexString(cipherText);
byte[] srcData = decrypt_Ecb_Padding(keyData, cipherData);
charset = charset.trim();
if (charset.length() <= 0) {
charset = ENCODING;
}
decryptStr = new String(srcData, charset);
return decryptStr;
}
/**
* @Description:ecb解密
*/
public static byte[] decrypt_Ecb_Padding(byte[] key, byte[] cipherText) throws Exception {
Cipher cipher = generateEcbCipher(ALGORITHM_NAME_ECB_PADDING, Cipher.DECRYPT_MODE, key);
return cipher.doFinal(cipherText);
}
}
当然,在做测试的时候,还可以加上验证类
/**
* @Description:密码校验
*/
public static boolean verifyEcb(String hexKey,String cipherText,String paramStr) throws Exception {
boolean flag = false;
byte[] keyData = ByteUtils.fromHexString(hexKey);
byte[] cipherData = ByteUtils.fromHexString(cipherText);
byte[] decryptData = decrypt_Ecb_Padding(keyData,cipherData);
byte[] srcData = paramStr.getBytes(ENCODING);
flag = Arrays.equals(decryptData,srcData);
return flag;
}
最后加上简单的测试类,就大功告成了,这里为了验证并发性,用上了线程池,大家自己也可以试一下。
/**
* @Description:测试类
*/
public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(20,100,10, TimeUnit.SECONDS,new LinkedBlockingQueue());
Map map=new HashMap<>();
for (int i=0;i<1000;i++){
int finalI = i;
executor.submit(()->{
System.out.println(Thread.currentThread().getName());
int number = new Random().nextInt(100);
try {
long start,end;
start = System.currentTimeMillis();
String json = "{\"name\":\"color\",\"sex\":\"man\"}"+number;
// 自定义的32位16进制密钥
String key = "888368581322491ace9q79348a2757d1";
String cipher = Sm4Utils.encryptEcb(key, json,ENCODING);
System.out.println("加密之后:"+cipher);
Thread.sleep(100);
json = Sm4Utils.decryptEcb(key, cipher,ENCODING);
System.out.println("解密:"+json);
System.out.println(Sm4Utils.verifyEcb(key, cipher, json));
System.out.println("密码校验:"+json);
end = System.currentTimeMillis();
System.out.println(finalI+"Run Time:" + (end - start) + "(ms)");
map.put(finalI,json);
} catch (Exception e) {
e.printStackTrace();
}
});
}
}
结果一部分截图如下,大家可以自己尝试一下
有时候我在使用jar包时可能会出现错误,我顺便把jar包粘出来方便大家使用
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.pqc.math.linearalgebra.ByteUtils;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.spec.SecretKeySpec;
import java.security.Key;
import java.security.SecureRandom;
import java.security.Security;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;