mirror of
https://gitee.com/dromara/hutool.git
synced 2026-02-09 09:16:26 +08:00
增加SpecUtil,KeyUtil增加generateRSAPrivateKey重载,(issue#ID1EIK@Gitee)
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得一个随机的字符串(只包含数字和大写字符)
|
||||
*
|
||||
|
||||
@@ -222,6 +222,24 @@ public class KeyUtil {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成RSA私钥,仅用于非对称加密
|
||||
* 算法见:<a href="https://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#KeyFactory">...</a>
|
||||
*
|
||||
* @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私钥,仅用于非对称加密<br>
|
||||
* 采用PKCS#8规范,此规范定义了私钥信息语法和加密私钥语法<br>
|
||||
@@ -235,6 +253,18 @@ public class KeyUtil {
|
||||
return generatePrivateKey(AsymmetricAlgorithm.RSA.getValue(), key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成RSA私钥,仅用于非对称加密<br>
|
||||
* 算法见:<a href="https://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#KeyFactory">...</a>
|
||||
*
|
||||
* @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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成私钥,仅用于非对称加密<br>
|
||||
* 采用PKCS#8规范,此规范定义了私钥信息语法和加密私钥语法<br>
|
||||
|
||||
138
hutool-crypto/src/main/java/cn/hutool/crypto/SpecUtil.java
Normal file
138
hutool-crypto/src/main/java/cn/hutool/crypto/SpecUtil.java
Normal file
@@ -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;
|
||||
|
||||
/**
|
||||
* 规范相关工具类,用于生成密钥规范、参数规范等快捷方法。
|
||||
* <ul>
|
||||
* <li>{@link KeySpec}: 密钥规范</li>
|
||||
* <li>{@link java.security.spec.AlgorithmParameterSpec}: 参数规范</li>
|
||||
* </ul>
|
||||
*
|
||||
* @author Looly
|
||||
* @since 5.8.41
|
||||
*/
|
||||
public class SpecUtil {
|
||||
|
||||
/**
|
||||
* 根据算法创建{@link KeySpec}
|
||||
* <ul>
|
||||
* <li>DESede: {@link DESedeKeySpec}</li>
|
||||
* <li>DES : {@link DESedeKeySpec}</li>
|
||||
* <li>其它 : {@link SecretKeySpec}</li>
|
||||
* </ul>
|
||||
*
|
||||
* @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}<br>
|
||||
* 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#生成格式,类似于:
|
||||
* <pre>{@code
|
||||
* <RSAKeyValue>
|
||||
* <Modulus>xx</Modulus>
|
||||
* <Exponent>xx</Exponent>
|
||||
* <P>xxxxxxxxx</P>
|
||||
* <Q>xxxxxxxxx</Q>
|
||||
* <DP>xxxxxxxx</DP>
|
||||
* <DQ>xxxxxxxx</DQ>
|
||||
* <InverseQ>xx</InverseQ>
|
||||
* <D>xxxxxxxxx</D>
|
||||
* </RSAKeyValue>
|
||||
* }</pre>
|
||||
*
|
||||
* @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)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user