diff --git a/docs/不加密、SM2+AES加密和SSL加密效率测试.MD b/docs/不加密、SM2+AES加密和SSL加密效率测试.MD new file mode 100644 index 00000000..316482e4 --- /dev/null +++ b/docs/不加密、SM2+AES加密和SSL加密效率测试.MD @@ -0,0 +1,88 @@ +# 客户端与服务器端采用不加密、SM2+AES加密、SSL加密方式进行的性能测试比较 + +* 本测试不作为性能测试参考,仅作为三种数据加密方式的性能比较使用 +* 本测试使用的操作系统为windows10, +* 本测试使用的测试环境配置:内存:16G,CPU:i716核 + +## 1、测试程序准备情况 +* 将程序分别打包为`server`和`client`的`jar`包,在本地运行一个`server`端 +* 拷贝三个客户端配置文件,配置文件名称为`app.yml`、`app-sm2-aes.yml`、`app-ssl.yml`,并修改相应配置,适配不加密、SM2+AES加密和SSL加密 + +## 2、测试思路和实现 +(1)准备1KB、10KB、20KB、50KB、100KB、1MB、2MB、5MB、10MB、20MB、100MB、500MB的文件 + +(2)使用Nodejs实现的anywhere工具,在本地运行简单http服务 + +(3)在server端生成3个licenseKey,分别对应不加密、SM2+AES加密和SSL加密通道,端口分别为9101,9102和9103,并同时映射到anywhere的8000端口 + +(4)使用Hutool里的HttpUtil工具包,对每个文件进行下载,记录下载使用时间,重复执行10次 + +## 测试结果 + +序号| 加密方式 | 文件大小 |响应时间(ms) +---|------|------|--- +1| 不加密 | 1KB |4 +2| SM2+AES | 1KB |21 +3| SSL | 1KB |67 +4| 不加密 | 10KB |3 +5| SM2+AES | 10KB |7 +6| SSL | 10KB |4 +7| 不加密 | 20KB |4 +8| SM2+AES | 20KB |6 +9| SSL | 20KB |5 +10| 不加密 | 50KB |4 +11| SM2+AES | 50KB |7 +12| SSL | 50KB |6 +13| 不加密 | 100KB |6 +14| SM2+AES | 100KB |8 +15| SSL | 100KB |6 +16| 不加密 | 1MB |19 +17| SM2+AES | 1MB |30 +18| SSL | 1MB |19 +19| 不加密 | 2MB |21 +20| SM2+AES | 2MB |40 +21| SSL | 2MB |24 +22| 不加密 | 5MB |31 +23| SM2+AES | 5MB |51 +24| SSL | 5MB |36 +25| 不加密 | 10MB |44 +26| SM2+AES | 10MB |85 +27| SSL | 10MB |47 +28| 不加密 | 20MB |63 +29| SM2+AES | 20MB |139 +30| SSL | 20MB |92 +31| 不加密 | 100MB |323 +32| SM2+AES | 100MB |590 +33| SSL | 100MB |322 +34| 不加密 | 500MB |1414 +35| SM2+AES | 500MB |2797 +36| SSL | 500MB |1561 + +## 测试结论 + +从测试结果可以看出,在请求的文件大小小于50KB时,SM2+AES的方式比SSL的方式效率高,当请求的文件大小大于50KB时,SSL的方式比SM2+AES的方式效率高。 + +## 测试使用的代码 +```java + public static void main(String[] args) { + int serialNumber = 1; + int[] ports = new int[]{9101, 9102, 9103}; + Map portMap = new HashMap<>(); + portMap.put(9101, "不加密"); + portMap.put(9102, "SM2+AES"); + portMap.put(9103, "SSL"); + HttpUtil.downloadBytes("http://127.0.0.1:9101/1KB"); // 使用不加密通道做一下测试,避免初始化时耗时过高 + String[] fileNames = "1KB,10KB,20KB,50KB,100KB,1MB,2MB,5MB,10MB,20MB,100MB,500MB".split(","); + for (String fileName : fileNames) { + for (int port : ports) { + String url = String.format("http://127.0.0.1:%s/%s", port, fileName); + long startTime = System.currentTimeMillis(); + HttpUtil.downloadBytes(url); + long endTime = System.currentTimeMillis(); + long resTimeMs = endTime - startTime; + String record = String.format("%s|%s|%s|%s", serialNumber++, portMap.get(port), fileName, resTimeMs); + System.out.println(record); + } + } + } +``` \ No newline at end of file diff --git a/neutrino-proxy-client/src/main/resources/app.yml b/neutrino-proxy-client/src/main/resources/app.yml index 7975bbb9..2b182b5f 100644 --- a/neutrino-proxy-client/src/main/resources/app.yml +++ b/neutrino-proxy-client/src/main/resources/app.yml @@ -34,16 +34,16 @@ neutrino: tunnel: # 线程池相关配置,用于技术调优,可忽略 thread-count: 50 - sm2-encrypt-enable: ${SM2_ENCRYPT_ENABLE:false} + sm2-encrypt-enable: ${SM2_ENCRYPT_ENABLE:true} # 隧道SSL证书配置 key-store-password: ${STORE_PASS:123456} jks-path: ${JKS_PATH:classpath:/test.jks} # 服务端IP server-ip: ${SERVER_IP:localhost} # 服务端端口(对应服务端app.yml中的tunnel.port、tunnel.ssl-port) - server-port: ${SERVER_PORT:9002} + server-port: ${SERVER_PORT:9000} # 是否启用SSL(注意:该配置必须和server-port对应上) - ssl-enable: ${SSL_ENABLE:true} + ssl-enable: ${SSL_ENABLE:false} # 客户端连接唯一凭证 license-key: ${LICENSE_KEY:b0a907332b474b25897c4dcb31fc7eb6} # 客户端唯一身份标识(可忽略,若不设置首次启动会自动生成) diff --git a/neutrino-proxy-core/src/main/java/org/dromara/neutrinoproxy/core/util/AesUtil.java b/neutrino-proxy-core/src/main/java/org/dromara/neutrinoproxy/core/util/AesUtil.java new file mode 100644 index 00000000..4606181f --- /dev/null +++ b/neutrino-proxy-core/src/main/java/org/dromara/neutrinoproxy/core/util/AesUtil.java @@ -0,0 +1,58 @@ +package org.dromara.neutrinoproxy.core.util; + +import cn.hutool.core.util.RandomUtil; + +import javax.crypto.Cipher; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Random; + +/** + * AES工具 + */ +public class AesUtil { + + public static byte[] generateKey() { + byte[] keyBytes = new byte[16]; + Random random = RandomUtil.getRandom(true); + random.nextBytes(keyBytes); + return keyBytes; + } + + /** + * AES解密 + * @param decryptKey 秘钥,16位 + * @param encryptBytes 密文 + * @return 明文 + * @throws Exception + */ + public static byte[] decrypt(byte[] decryptKey, byte[] encryptBytes) { + try{ + Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); + cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(decryptKey, "AES")); + return cipher.doFinal(encryptBytes); + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + /** + * AES加密 + * @param encryptKey 秘钥,必须为16个字符组成 + * @param data 明文 + * @return 密文 + * @throws Exception + */ + public static byte[] encrypt(byte[] encryptKey, byte[] data) { + try { + Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); + cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(encryptKey, "AES")); + return cipher.doFinal(data); + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } +} diff --git a/neutrino-proxy-core/src/main/java/org/dromara/neutrinoproxy/core/util/EncryptUtil.java b/neutrino-proxy-core/src/main/java/org/dromara/neutrinoproxy/core/util/EncryptUtil.java index 6a660419..b1e67256 100644 --- a/neutrino-proxy-core/src/main/java/org/dromara/neutrinoproxy/core/util/EncryptUtil.java +++ b/neutrino-proxy-core/src/main/java/org/dromara/neutrinoproxy/core/util/EncryptUtil.java @@ -27,10 +27,15 @@ import cn.hutool.crypto.SecureUtil; import cn.hutool.crypto.SmUtil; import cn.hutool.crypto.symmetric.SymmetricAlgorithm; import cn.hutool.crypto.symmetric.SymmetricCrypto; +import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey; +import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey; +import org.bouncycastle.util.encoders.Hex; import org.dromara.neutrinoproxy.core.KeyPairRecord; import javax.crypto.SecretKey; import java.security.KeyPair; +import java.security.PrivateKey; +import java.security.PublicKey; /** * 国密算法加解密工具 @@ -44,14 +49,25 @@ public class EncryptUtil { * @return */ public static KeyPairRecord generateSm2KeyPair() { - KeyPair keyPair = SecureUtil.generateKeyPair("SM2"); - byte[] privateKeyBytes = keyPair.getPrivate().getEncoded(); - byte[] publicKeyBytes = keyPair.getPublic().getEncoded(); - String privateKey = HexUtil.encodeHexStr(privateKeyBytes); - String publicKey = HexUtil.encodeHexStr(publicKeyBytes); + String privateKeyHex = null; + String publicKeyHex = null; - return new KeyPairRecord(privateKey, publicKey); + KeyPair keyPair = Sm2Util.createECKeyPair(); + + PrivateKey privateKey = keyPair.getPrivate(); + if (privateKey instanceof BCECPrivateKey) { + //获取32字节十六进制私钥串 + privateKeyHex = ((BCECPrivateKey) privateKey).getD().toString(16); + } + + PublicKey publicKey = keyPair.getPublic(); + if (publicKey instanceof BCECPublicKey) { + //获取65字节非压缩缩的十六进制公钥串(0x04) + publicKeyHex = Hex.toHexString(((BCECPublicKey) publicKey).getQ().getEncoded(false)); + } + + return new KeyPairRecord(privateKeyHex, publicKeyHex); } /** @@ -61,7 +77,7 @@ public class EncryptUtil { * @return 加密后的字节数组 */ public static byte[] encryptBySm2(String publicKey, byte[] data) { - return SmUtil.sm2(null, publicKey).encrypt(data); + return Sm2Util.encrypt(publicKey, data); } /** @@ -71,7 +87,7 @@ public class EncryptUtil { * @return 解密后的字节数组 */ public static byte[] decryptBySm2(String privateKey, byte[] data) { - return SmUtil.sm2(privateKey, null).decrypt(data); + return Sm2Util.decrypt(privateKey, data); } public static byte[] generateSm4Key() { @@ -99,7 +115,7 @@ public class EncryptUtil { } public static byte[] generateAesKey() { - return SecureUtil.generateKey(SymmetricAlgorithm.AES.getValue()).getEncoded(); + return AesUtil.generateKey(); } /** @@ -109,8 +125,7 @@ public class EncryptUtil { * @return 加密后的数据 */ public static byte[] encryptByAes(byte[] key, byte[] data) { - SymmetricCrypto aes = new SymmetricCrypto(SymmetricAlgorithm.AES, key); - return aes.encrypt(data); + return AesUtil.encrypt(key, data); } /** @@ -120,8 +135,7 @@ public class EncryptUtil { * @return 解密后的数据 */ public static byte[] decryptByAes(byte[] key, byte[] encryptedData) { - SymmetricCrypto aes = new SymmetricCrypto(SymmetricAlgorithm.AES, key); - return aes.decrypt(encryptedData); + return AesUtil.decrypt(key, encryptedData); } /** diff --git a/neutrino-proxy-core/src/main/java/org/dromara/neutrinoproxy/core/util/Sm2Util.java b/neutrino-proxy-core/src/main/java/org/dromara/neutrinoproxy/core/util/Sm2Util.java new file mode 100644 index 00000000..9a4af786 --- /dev/null +++ b/neutrino-proxy-core/src/main/java/org/dromara/neutrinoproxy/core/util/Sm2Util.java @@ -0,0 +1,216 @@ +package org.dromara.neutrinoproxy.core.util; + +import org.bouncycastle.asn1.gm.GMNamedCurves; +import org.bouncycastle.asn1.x9.X9ECParameters; +import org.bouncycastle.crypto.engines.SM2Engine; +import org.bouncycastle.crypto.params.ECDomainParameters; +import org.bouncycastle.crypto.params.ECPrivateKeyParameters; +import org.bouncycastle.crypto.params.ECPublicKeyParameters; +import org.bouncycastle.crypto.params.ParametersWithRandom; +import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey; +import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.jce.spec.ECParameterSpec; +import org.bouncycastle.jce.spec.ECPrivateKeySpec; +import org.bouncycastle.jce.spec.ECPublicKeySpec; + +import java.math.BigInteger; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.SecureRandom; +import java.security.spec.ECGenParameterSpec; + +/** + * @ClassName SM2Utils + * @Description SM2算法工具类 + */ +public class Sm2Util { + + /** + * @Description 生成秘钥对 + * @return KeyPair + */ + public static KeyPair createECKeyPair() { + //使用标准名称创建EC参数生成的参数规范 + final ECGenParameterSpec sm2Spec = new ECGenParameterSpec("sm2p256v1"); + + // 获取一个椭圆曲线类型的密钥对生成器 + final KeyPairGenerator kpg; + try { + kpg = KeyPairGenerator.getInstance("EC", new BouncyCastleProvider()); + // 使用SM2算法域参数集初始化密钥生成器(默认使用以最高优先级安装的提供者的 SecureRandom 的实现作为随机源) + // kpg.initialize(sm2Spec); + + // 使用SM2的算法域参数集和指定的随机源初始化密钥生成器 + kpg.initialize(sm2Spec, new SecureRandom()); + + // 通过密钥生成器生成密钥对 + return kpg.generateKeyPair(); + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + + /** + * @Description 公钥加密 + * @param publicKeyHex SM2十六进制公钥 + * @param data 明文数据 + * @return String + */ + public static byte[] encrypt(String publicKeyHex, byte[] data) { + return encrypt(getECPublicKeyByPublicKeyHex(publicKeyHex), data, 1); + } + + /** + * @Description 公钥加密 + * @param publicKey SM2公钥 + * @param data 明文数据 + * @param modeType 加密模式 + * @return String + */ + public static byte[] encrypt(BCECPublicKey publicKey, byte[] data, int modeType) { + //加密模式 + SM2Engine.Mode mode = SM2Engine.Mode.C1C3C2; + if (modeType != 1) { + mode = SM2Engine.Mode.C1C2C3; + } + //通过公钥对象获取公钥的基本域参数。 + ECParameterSpec ecParameterSpec = publicKey.getParameters(); + ECDomainParameters ecDomainParameters = new ECDomainParameters(ecParameterSpec.getCurve(), + ecParameterSpec.getG(), ecParameterSpec.getN()); + //通过公钥值和公钥基本参数创建公钥参数对象 + ECPublicKeyParameters ecPublicKeyParameters = new ECPublicKeyParameters(publicKey.getQ(), ecDomainParameters); + //根据加密模式实例化SM2公钥加密引擎 + SM2Engine sm2Engine = new SM2Engine(mode); + //初始化加密引擎 + sm2Engine.init(true, new ParametersWithRandom(ecPublicKeyParameters, new SecureRandom())); + byte[] arrayOfBytes = null; + try { + //将明文字符串转换为指定编码的字节串 + //通过加密引擎对字节数串行加密 + arrayOfBytes = sm2Engine.processBlock(data, 0, data.length); + } catch (Exception e) { + System.out.println("SM2加密时出现异常:" + e.getMessage()); + e.printStackTrace(); + } + //将加密后的字节串转换为十六进制字符串 + return arrayOfBytes; + } + + /** + * @Description 私钥解密 + * @param privateKeyHex SM2十六进制私钥 + * @param cipherData 密文数据 + * @return String + */ + public static byte[] decrypt(String privateKeyHex, byte[] cipherData) { + return decrypt(getBCECPrivateKeyByPrivateKeyHex(privateKeyHex), cipherData, 1); + } + + /** + * @Description 私钥解密 + * @param privateKey SM私钥 + * @param cipherDataByte 密文数据 + * @param modeType 解密模式 + * @return + */ + public static byte[] decrypt(BCECPrivateKey privateKey, byte[] cipherDataByte, int modeType) { + //解密模式 + SM2Engine.Mode mode = SM2Engine.Mode.C1C3C2; + if (modeType != 1) + mode = SM2Engine.Mode.C1C2C3; + //通过私钥对象获取私钥的基本域参数。 + ECParameterSpec ecParameterSpec = privateKey.getParameters(); + ECDomainParameters ecDomainParameters = new ECDomainParameters(ecParameterSpec.getCurve(), + ecParameterSpec.getG(), ecParameterSpec.getN()); + //通过私钥值和私钥钥基本参数创建私钥参数对象 + ECPrivateKeyParameters ecPrivateKeyParameters = new ECPrivateKeyParameters(privateKey.getD(), + ecDomainParameters); + //通过解密模式创建解密引擎并初始化 + SM2Engine sm2Engine = new SM2Engine(mode); + sm2Engine.init(false, ecPrivateKeyParameters); + try { + //通过解密引擎对密文字节串进行解密 + return sm2Engine.processBlock(cipherDataByte, 0, cipherDataByte.length); + } catch (Exception e) { + System.out.println("SM2解密时出现异常" + e.getMessage()); + } + return new byte[0]; + } + //椭圆曲线ECParameters ASN.1 结构 + private static X9ECParameters x9ECParameters = GMNamedCurves.getByName("sm2p256v1"); + //椭圆曲线公钥或私钥的基本域参数。 + private static ECParameterSpec ecDomainParameters = new ECParameterSpec(x9ECParameters.getCurve(), x9ECParameters.getG(), x9ECParameters.getN()); + + /** + * @Description 公钥字符串转换为 BCECPublicKey 公钥对象 + * @param pubKeyHex 64字节十六进制公钥字符串(如果公钥字符串为65字节首个字节为0x04:表示该公钥为非压缩格式,操作时需要删除) + * @return BCECPublicKey SM2公钥对象 + */ + public static BCECPublicKey getECPublicKeyByPublicKeyHex(String pubKeyHex) { + //截取64字节有效的SM2公钥(如果公钥首个字节为0x04) + if (pubKeyHex.length() > 128) { + pubKeyHex = pubKeyHex.substring(pubKeyHex.length() - 128); + } + //将公钥拆分为x,y分量(各32字节) + String stringX = pubKeyHex.substring(0, 64); + String stringY = pubKeyHex.substring(stringX.length()); + //将公钥x、y分量转换为BigInteger类型 + BigInteger x = new BigInteger(stringX, 16); + BigInteger y = new BigInteger(stringY, 16); + //通过公钥x、y分量创建椭圆曲线公钥规范 + ECPublicKeySpec ecPublicKeySpec = new ECPublicKeySpec(x9ECParameters.getCurve().createPoint(x, y), ecDomainParameters); + //通过椭圆曲线公钥规范,创建出椭圆曲线公钥对象(可用于SM2加密及验签) + return new BCECPublicKey("EC", ecPublicKeySpec, BouncyCastleProvider.CONFIGURATION); + } + + /** + * @Description 私钥字符串转换为 BCECPrivateKey 私钥对象 + * @param privateKeyHex 32字节十六进制私钥字符串 + * @return BCECPrivateKey SM2私钥对象 + */ + public static BCECPrivateKey getBCECPrivateKeyByPrivateKeyHex(String privateKeyHex) { + //将十六进制私钥字符串转换为BigInteger对象 + BigInteger d = new BigInteger(privateKeyHex, 16); + //通过私钥和私钥域参数集创建椭圆曲线私钥规范 + ECPrivateKeySpec ecPrivateKeySpec = new ECPrivateKeySpec(d, ecDomainParameters); + //通过椭圆曲线私钥规范,创建出椭圆曲线私钥对象(可用于SM2解密和签名) + return new BCECPrivateKey("EC", ecPrivateKeySpec, BouncyCastleProvider.CONFIGURATION); + } + + public static void main(String[] args) { + /*String publicKeyHex = null; + String privateKeyHex = null;*/ + /*KeyPair keyPair = createECKeyPair(); + PublicKey publicKey = keyPair.getPublic(); + if (publicKey instanceof BCECPublicKey) { + //获取65字节非压缩缩的十六进制公钥串(0x04) + publicKeyHex = Hex.toHexString(((BCECPublicKey) publicKey).getQ().getEncoded(false)); + System.out.println("---->SM2公钥:" + publicKeyHex); + } + PrivateKey privateKey = keyPair.getPrivate(); + if (privateKey instanceof BCECPrivateKey) { + //获取32字节十六进制私钥串 + privateKeyHex = ((BCECPrivateKey) privateKey).getD().toString(16); + System.out.println("---->SM2私钥:" + privateKeyHex); + }*/ + + /** + * 公钥加密 + */ +// String data = "az"; + + //将十六进制公钥串转换为 BCECPublicKey 公钥对象 + /*String encryptData = encrypt(publicKeyHex, data); + System.out.println("---->加密结果:" + encryptData);*/ + + /** + * 私钥解密 + */ + //将十六进制私钥串转换为 BCECPrivateKey 私钥对象 + /*data = decrypt("xx", "xx"); + System.out.println("---->解密结果:" + data);*/ + } +} +