From 51dc50dcd3eac1a489a9bdeafadd8c45b7819f99 Mon Sep 17 00:00:00 2001 From: Looly Date: Sun, 12 Oct 2025 00:48:38 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0`SpecUtil`=EF=BC=8C`KeyUtil`?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0`generateRSAPrivateKey`=E9=87=8D=E8=BD=BD?= =?UTF-8?q?=EF=BC=8C=EF=BC=88issue#ID1EIK@Gitee=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 4 +- .../java/cn/hutool/core/util/RandomUtil.java | 11 ++ .../main/java/cn/hutool/crypto/KeyUtil.java | 30 ++++ .../main/java/cn/hutool/crypto/SpecUtil.java | 138 ++++++++++++++++++ .../crypto/asymmetric/IssueID1EIKTest.java | 40 +++++ 5 files changed, 222 insertions(+), 1 deletion(-) create mode 100644 hutool-crypto/src/main/java/cn/hutool/crypto/SpecUtil.java create mode 100644 hutool-crypto/src/test/java/cn/hutool/crypto/asymmetric/IssueID1EIKTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 5760333b2d..4f840970e1 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ # 🚀Changelog ------------------------------------------------------------------------------------------------------------- -# 5.8.41(2025-10-11) +# 5.8.41(2025-10-12) ### 🐣新特性 * 【core 】 增加`WeakKeyValueConcurrentMap`及其关联类,同时废弃`WeakConcurrentMap`并替换(issue#4039@Github) @@ -22,6 +22,8 @@ * 【core 】 `LocalDateTimeUtil.parseDate`注释修正(pr#4085@Github) * 【core 】 `StrUtil`增加null检查处理(pr#4086@Github) * 【json 】 增加Record支持(pr#4096@Github) +* 【crypto 】 增加`SpecUtil`,`KeyUtil`增加`generateRSAPrivateKey`重载,(issue#ID1EIK@Gitee) +* 【core 】 `RandomUtil`增加`randomStringLower`方法 ### 🐞Bug修复 * 【core 】 修复`ReflectUtil`中因class和Method关联导致的缓存无法回收问题(issue#4039@Github) diff --git a/hutool-core/src/main/java/cn/hutool/core/util/RandomUtil.java b/hutool-core/src/main/java/cn/hutool/core/util/RandomUtil.java index f96d77f228..f4d78bcac5 100755 --- a/hutool-core/src/main/java/cn/hutool/core/util/RandomUtil.java +++ b/hutool-core/src/main/java/cn/hutool/core/util/RandomUtil.java @@ -588,6 +588,17 @@ public class RandomUtil { return randomString(BASE_CHAR_NUMBER, length); } + /** + * 获得一个随机的字符串(只包含数字和小写字母) + * + * @param length 字符串的长度 + * @return 随机字符串 + * @since 5.8.41 + */ + public static String randomStringLower(final int length) { + return randomString(BASE_CHAR_NUMBER_LOWER, length); + } + /** * 获得一个随机的字符串(只包含数字和大写字符) * diff --git a/hutool-crypto/src/main/java/cn/hutool/crypto/KeyUtil.java b/hutool-crypto/src/main/java/cn/hutool/crypto/KeyUtil.java index d021af16b3..f46ad40905 100644 --- a/hutool-crypto/src/main/java/cn/hutool/crypto/KeyUtil.java +++ b/hutool-crypto/src/main/java/cn/hutool/crypto/KeyUtil.java @@ -222,6 +222,24 @@ public class KeyUtil { } } + /** + * 生成RSA私钥,仅用于非对称加密 + * 算法见:... + * + * @param key 密钥,支持XML和Base64两种格式,XML为C#生成格式,见{@link SpecUtil#xmlToRSAPrivateCrtKeySpec(String)} + * @return RSA私钥 {@link PrivateKey} + * @since 7.0.0 + */ + public static PrivateKey generateRSAPrivateKey(String key) { + Assert.notBlank(key, "Key is blank!"); + key = StrUtil.trim(key); + if(StrUtil.startWith(key, '<')){ + return generateRSAPrivateKey(SpecUtil.xmlToRSAPrivateCrtKeySpec(key)); + } + + return generatePrivateKey(AsymmetricAlgorithm.RSA.getValue(), Base64.decode(key)); + } + /** * 生成RSA私钥,仅用于非对称加密
* 采用PKCS#8规范,此规范定义了私钥信息语法和加密私钥语法
@@ -235,6 +253,18 @@ public class KeyUtil { return generatePrivateKey(AsymmetricAlgorithm.RSA.getValue(), key); } + /** + * 生成RSA私钥,仅用于非对称加密
+ * 算法见:... + * + * @param keySpec {@link KeySpec} + * @return RSA私钥 {@link PrivateKey} + * @since 5.8.41 + */ + public static PrivateKey generateRSAPrivateKey(final KeySpec keySpec) { + return generatePrivateKey(AsymmetricAlgorithm.RSA.getValue(), keySpec); + } + /** * 生成私钥,仅用于非对称加密
* 采用PKCS#8规范,此规范定义了私钥信息语法和加密私钥语法
diff --git a/hutool-crypto/src/main/java/cn/hutool/crypto/SpecUtil.java b/hutool-crypto/src/main/java/cn/hutool/crypto/SpecUtil.java new file mode 100644 index 0000000000..3136b8e573 --- /dev/null +++ b/hutool-crypto/src/main/java/cn/hutool/crypto/SpecUtil.java @@ -0,0 +1,138 @@ +package cn.hutool.crypto; + +import cn.hutool.core.codec.Base64; +import cn.hutool.core.util.RandomUtil; +import cn.hutool.core.util.XmlUtil; +import org.w3c.dom.Element; + +import javax.crypto.spec.*; +import java.math.BigInteger; +import java.security.InvalidKeyException; +import java.security.spec.KeySpec; +import java.security.spec.RSAPrivateCrtKeySpec; + +/** + * 规范相关工具类,用于生成密钥规范、参数规范等快捷方法。 + * + * + * @author Looly + * @since 5.8.41 + */ +public class SpecUtil { + + /** + * 根据算法创建{@link KeySpec} + * + * + * @param algorithm 算法 + * @param key 密钥 + * @return {@link KeySpec} + */ + public static KeySpec createKeySpec(final String algorithm, byte[] key) { + try { + if (algorithm.startsWith("DESede")) { + if (null == key) { + key = RandomUtil.randomBytes(24); + } + // DESede兼容 + return new DESedeKeySpec(key); + } else if (algorithm.startsWith("DES")) { + if (null == key) { + key = RandomUtil.randomBytes(8); + } + return new DESKeySpec(key); + } + } catch (final InvalidKeyException e) { + throw new CryptoException(e); + } + + return new SecretKeySpec(key, algorithm); + } + + /** + * 创建{@link PBEKeySpec}
+ * PBE算法没有密钥的概念,密钥在其它对称加密算法中是经过算法计算得出来的,PBE算法则是使用口令替代了密钥。 + * + * @param password 口令 + * @return {@link PBEKeySpec} + */ + public static PBEKeySpec createPBEKeySpec(char[] password) { + if (null == password) { + password = RandomUtil.randomStringLower(32).toCharArray(); + } + return new PBEKeySpec(password); + } + + /** + * 创建{@link PBEParameterSpec} + * + * @param salt 加盐值 + * @param iterationCount 摘要次数 + * @return {@link PBEParameterSpec} + */ + public static PBEParameterSpec createPBEParameterSpec(final byte[] salt, final int iterationCount) { + return new PBEParameterSpec(salt, iterationCount); + } + + /** + * 将XML格式的密钥参数转化为{@link RSAPrivateCrtKeySpec},XML为C#生成格式,类似于: + *
{@code
+	 * 
+	 *     xx
+	 *     xx
+	 *     

xxxxxxxxx

+ * xxxxxxxxx + * xxxxxxxx + * xxxxxxxx + * xx + * xxxxxxxxx + *
+ * }
+ * + * @param xml xml格式密钥字符串 + * @return {@link RSAPrivateCrtKeySpec} + */ + public static RSAPrivateCrtKeySpec xmlToRSAPrivateCrtKeySpec(final String xml) { + // 1. 解析XML + final Element rootElement = XmlUtil.getRootElement(XmlUtil.parseXml(xml)); + + // 2. 提取各个字段 + final String modulusB64 = XmlUtil.elementText(rootElement, "Modulus"); + final String exponentB64 = XmlUtil.elementText(rootElement, "Exponent"); + final String pB64 = XmlUtil.elementText(rootElement, "P"); + final String qB64 = XmlUtil.elementText(rootElement, "Q"); + final String dpB64 = XmlUtil.elementText(rootElement, "DP"); + final String dqB64 = XmlUtil.elementText(rootElement, "DQ"); + final String inverseQB64 = XmlUtil.elementText(rootElement, "InverseQ"); + final String dB64 = XmlUtil.elementText(rootElement, "D"); + + // 3. Base64解码 + final byte[] modulus = Base64.decode(modulusB64); + final byte[] publicExponent = Base64.decode(exponentB64); + final byte[] privateExponent = Base64.decode(dB64); + final byte[] primeP = Base64.decode(pB64); + final byte[] primeQ = Base64.decode(qB64); + final byte[] primeExponentP = Base64.decode(dpB64); + final byte[] primeExponentQ = Base64.decode(dqB64); + final byte[] crtCoefficient = Base64.decode(inverseQB64); + + // 4. 创建RSAPrivateCrtKeySpec + return new RSAPrivateCrtKeySpec( + new BigInteger(1, modulus), + new BigInteger(1, publicExponent), + new BigInteger(1, privateExponent), + new BigInteger(1, primeP), + new BigInteger(1, primeQ), + new BigInteger(1, primeExponentP), + new BigInteger(1, primeExponentQ), + new BigInteger(1, crtCoefficient) + ); + } +} diff --git a/hutool-crypto/src/test/java/cn/hutool/crypto/asymmetric/IssueID1EIKTest.java b/hutool-crypto/src/test/java/cn/hutool/crypto/asymmetric/IssueID1EIKTest.java new file mode 100644 index 0000000000..82827e67ff --- /dev/null +++ b/hutool-crypto/src/test/java/cn/hutool/crypto/asymmetric/IssueID1EIKTest.java @@ -0,0 +1,40 @@ +package cn.hutool.crypto.asymmetric; + +import cn.hutool.core.codec.Base64; +import cn.hutool.crypto.KeyUtil; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.security.PrivateKey; + +public class IssueID1EIKTest { + + @Test + void rsaTest(){ + // 1. Base64解码 + String str = "PFJTQUtleVZhbHVlPjxNb2R1bHVzPnVscHlkSXJydHJUMzJBSnFDV0FFMHQxNXdHYjBKUTJqSnpBUW1FakpRRzhkcnUrdDhyQUtzekVoNXRRL2x4eTdnMFVMR3dzWjNmekQrdm12d2lKWkx5d1dncmszMDdRbFpXSkU3dWIxM2ZtN2pUa0RLOXM0L294alNabm5JTHcrc0lwVGFoLzdlL2hLNkxEN0VFbzNuTHZZK0VjTzdHa21IYXVCUW5CZmhPaz08L01vZHVsdXM+PEV4cG9uZW50PkFRQUI8L0V4cG9uZW50PjxQPjZlSFdVYUZNdWRTV0svODJPeWxxNHZ2Y0FDbmNHUHYvN1VKWVVETnY1elBZVGE5UFNXUTRzNUk3RHBDTWJYcExLK0VldE5mOUFCZ1ZwVjZERTJlMTR3PT08L1A+PFE+eS9uMkc3d2FYZVlGUnZXWjNROW96NVkyVEpHdUdaSXIzeis3QlVGOWZIckp1Nk9SU2V0YUVkdW5tcjgzSFVNN3E4TGIvWGxtdmVpS0p0OWh2NWx6d3c9PTwvUT48RFA+Sy9IdExTVmJuMGNjZUdQWnNzQVRmMWJIZlpoZjdLbmM2cDJlcm1NYjBadGlOeWFMaFVTNWlyUWRPSjFjWlcybkZqV1VhWEp6N1VLWlBwdEZrYTNZOVE9PTwvRFA+PERRPkJSbm9QTU5VaVhxaU1TY2RSUGtJcndCYnRVaURhU0pOdEpTY2NjSTBpRE50N2lKbUZNb3RBM3RSMHIzcmUvRGRnaXNxWTBsdzkxamtjNXBza0dVZkR3PT08L0RRPjxJbnZlcnNlUT5rVGpLTzBpcXU4M3pTZGpqbWNoT2lYQ0k0bm5veTg5c0JiOFFqMk92TXpnRnhOazhVV1hoT29ZdGVnUDNiVUFhZEJBT3VGSnRCcE1RMmdCemo2ekRWZz09PC9JbnZlcnNlUT48RD5COUhQeDdBa24vQU1EbFpibUxVY3ZyUm9iWGhrZWtHT1BSQzVRWXFjVjBYU1d3clhvNzFiVlpXVU5KbG5hYkhjOUc4clBpRkRIcHVDcGI5Z2JxYitVdmdKRXFrd0t5cU5HSmdnSm9yS1Irb2doWFh3czRuZVVTV1lENnpqbGQvN2U0QlNRM05ScTJGbEFPSEZnRnp3aElhazZwY1pOT2pwazlTUWdSY2ZaSGs9PC9EPjwvUlNBS2V5VmFsdWU+"; + final String xml = Base64.decodeStr(str); + + final PrivateKey privateKey = KeyUtil.generateRSAPrivateKey(xml); + RSA rsa = new RSA(privateKey, null); + String decrypt = rsa.decryptStr("tqmp7hGri5WYcZT8bJXJK3SKVlkAx1i1JSpOlOIGB+EAA5OoWS0PtCcWdwLou/qVM28exXKGpmehYbx0Ez0Co8bLHMMnXU3bxp3PXstF2MvrODJoEz+nEzxQ92ngg2n/96Du1rCbwkletYFRO47HpkcEYSTKBsi6NtC98JhUsYSXG15hCJu/I8vOWDF9sB4FCFF9qScpEOUndhctDvAH/UvxBqvSix8mJdL9pyz6Er3zhhQ//4LnI3dQQM0saTq4rZITliTxalT32DRfz0Vj5hNj/So54SspX6fbHjRu0jEaMAotebYZ1Tgpw4AHCYy1DIYoVeGSACd4kc+6ka67gI8jXD7H0tIhI2zyTU3MWQWm2tSOCj+WllELlmCn7ssDp37M6hNO9Imzzj32hWQrsvYsCFufAh+KqRQ1zoF1CQVK8wHRf2ppSFjfR9cCcunpqHqeRrJIpzhJ11dvGZ3JokcjOfDrTNKyXXr7+NVkmc9jPvByEGJXcgkJuX1EHyMv", KeyType. + PrivateKey); + + String decodeStr = "cpu=178BFBFF00A50F00\r\n" + + "baseBoard=MP242ML1\r\n" + + "bios=MP242ML1\r\n" + + "mac=00:FF:CB:EF:28:18|00:FF:03:A2:FC:D7|C8:94:02:F8:8A:83\r\n" + + "cusname=123\r\n" + + "serviceno=12121\r\n" + + "kcliccount=1\r\n" + + "cjliccount=1\r\n" + + "venprintliccount=1\r\n" + + "beginTime=2025-10-11 14:05:10\r\n" + + "endTime=2026-10-11 14:05:10\r\n" + + "lictype=租赁\r\n" + + "serviceendtime=1\r\n" + + "validate=1\r\n" + + "validateunit=年\r\n"; + Assertions.assertEquals(decodeStr, decrypt); + } +}