diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/util/ByteUtil.java b/hutool-core/src/main/java/org/dromara/hutool/core/util/ByteUtil.java
index 43bb71e54..ae21a1afc 100644
--- a/hutool-core/src/main/java/org/dromara/hutool/core/util/ByteUtil.java
+++ b/hutool-core/src/main/java/org/dromara/hutool/core/util/ByteUtil.java
@@ -12,6 +12,7 @@
package org.dromara.hutool.core.util;
+import org.dromara.hutool.core.io.buffer.FastByteBuffer;
import org.dromara.hutool.core.math.NumberUtil;
import java.math.BigDecimal;
@@ -608,4 +609,24 @@ public class ByteUtil {
}
return new BigInteger(1, mag);
}
+
+ /**
+ * 连接多个byte[]
+ *
+ * @param byteArrays 多个byte[]
+ * @return 连接后的byte[]
+ * @since 6.0.0
+ */
+ public static byte[] concat(final byte[]... byteArrays){
+ int totalLength = 0;
+ for (final byte[] byteArray : byteArrays) {
+ totalLength += byteArray.length;
+ }
+
+ final FastByteBuffer buffer = new FastByteBuffer(totalLength);
+ for (final byte[] byteArray : byteArrays) {
+ buffer.append(byteArray);
+ }
+ return buffer.toArrayZeroCopyIfPossible();
+ }
}
diff --git a/hutool-crypto/src/main/java/org/dromara/hutool/crypto/symmetric/OpenSSLSaltParser.java b/hutool-crypto/src/main/java/org/dromara/hutool/crypto/symmetric/OpenSSLSaltParser.java
new file mode 100755
index 000000000..bdad21076
--- /dev/null
+++ b/hutool-crypto/src/main/java/org/dromara/hutool/crypto/symmetric/OpenSSLSaltParser.java
@@ -0,0 +1,196 @@
+/*
+ * Copyright (c) 2023 looly(loolly@aliyun.com)
+ * Hutool is licensed under Mulan PSL v2.
+ * You can use this software according to the terms and conditions of the Mulan PSL v2.
+ * You may obtain a copy of Mulan PSL v2 at:
+ * http://license.coscl.org.cn/MulanPSL2
+ * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
+ * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
+ * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
+ * See the Mulan PSL v2 for more details.
+ */
+
+package org.dromara.hutool.crypto.symmetric;
+
+import org.dromara.hutool.core.array.ArrayUtil;
+import org.dromara.hutool.core.lang.Assert;
+import org.dromara.hutool.core.text.StrUtil;
+import org.dromara.hutool.core.util.ByteUtil;
+import org.dromara.hutool.crypto.digest.MD5;
+
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.util.Arrays;
+
+/**
+ * OpenSSL中加盐解析器
+ * 参考:
+ *
+ * https://stackoverflow.com/questions/11783062/how-to-decrypt-file-in-java-encrypted-with-openssl-command-using-aes
+ * https://stackoverflow.com/questions/32508961/java-equivalent-of-an-openssl-aes-cbc-encryption
+ *
+ *
+ * @author looly
+ * @since 6.0.0
+ */
+public class OpenSSLSaltParser {
+ private final static byte SALT_LEN = 8;
+
+ /**
+ * OpenSSL's magic initial bytes.
+ */
+ private static final byte[] SALTED_MAGIC = "Salted__".getBytes(StandardCharsets.US_ASCII);
+
+ /**
+ * 获取魔术值和随机盐的长度:16(128位)
+ */
+ public static final int MAGIC_SALT_LENGTH = SALTED_MAGIC.length + SALT_LEN;
+
+ /**
+ * 获取去除头部盐的加密数据
+ *
+ * @param encryptedData 密文
+ * @return 实际密文
+ */
+ public static byte[] getData(final byte[] encryptedData) {
+ if (ArrayUtil.startWith(encryptedData, SALTED_MAGIC)) {
+ return Arrays.copyOfRange(encryptedData, SALTED_MAGIC.length + SALT_LEN, encryptedData.length);
+ }
+ return encryptedData;
+ }
+
+ /**
+ * 获取8位salt随机数
+ *
+ * @param encryptedData 密文
+ * @return salt随机数
+ */
+ public static byte[] getSalt(final byte[] encryptedData) {
+ if (ArrayUtil.startWith(encryptedData, SALTED_MAGIC)) {
+ return Arrays.copyOfRange(encryptedData, SALTED_MAGIC.length, MAGIC_SALT_LENGTH);
+ }
+ return null;
+ }
+
+ /**
+ * 为加密后的数据添加Magic头,生成的密文格式为:
+ *
+ * Salted__[salt][data]
+ *
+ *
+ * @param data 数据
+ * @param salt 加盐值,必须8位,{@code null}表示返回原文
+ * @return 密文
+ */
+ public static byte[] addMagic(final byte[] data, final byte[] salt) {
+ if (null == salt) {
+ return data;
+ }
+ Assert.isTrue(SALT_LEN == salt.length);
+ return ByteUtil.concat(SALTED_MAGIC, salt, data);
+ }
+
+ /**
+ * 获取Magic头,生成的密文格式为:
+ *
+ * Salted__[salt]
+ *
+ *
+ * @param salt 加盐值,必须8位,不能为{@code null}
+ * @return Magic头
+ */
+ public static byte[] getSaltedMagic(final byte[] salt) {
+ return ByteUtil.concat(SALTED_MAGIC, salt);
+ }
+
+ /**
+ * 创建MD5 OpenSSLSaltParser
+ *
+ * @param keyLength 密钥长度
+ * @param algorithm 算法
+ * @return OpenSSLSaltParser
+ */
+ public static OpenSSLSaltParser ofMd5(final int keyLength, final String algorithm) {
+ return of(new MD5().getDigest(), keyLength, algorithm);
+ }
+
+ /**
+ * 创建OpenSSLSaltParser
+ *
+ * @param digest {@link MessageDigest}
+ * @param keyLength 密钥长度
+ * @param algorithm 算法
+ * @return OpenSSLSaltParser
+ */
+ public static OpenSSLSaltParser of(final MessageDigest digest, final int keyLength, final String algorithm) {
+ return new OpenSSLSaltParser(digest, keyLength, algorithm);
+ }
+
+ private final MessageDigest digest;
+ private final int keyLength;
+ private final int ivLength;
+ private String algorithm;
+
+ /**
+ * 构造
+ *
+ * @param digest {@link MessageDigest}
+ * @param keyLength 密钥长度
+ * @param algorithm 算法
+ */
+ public OpenSSLSaltParser(final MessageDigest digest, final int keyLength, final String algorithm) {
+ int ivLength = 16;
+ if (StrUtil.containsIgnoreCase(algorithm, "des")) {
+ ivLength = 8;
+ }
+ this.digest = digest;
+ this.keyLength = keyLength;
+ this.ivLength = ivLength;
+ this.algorithm = algorithm;
+ }
+
+ /**
+ * 构造
+ *
+ * @param digest {@link MessageDigest}
+ * @param keyLength 密钥长度
+ * @param ivLength IV长度
+ */
+ public OpenSSLSaltParser(final MessageDigest digest, final int keyLength, final int ivLength) {
+ this.digest = digest;
+ this.keyLength = keyLength;
+ this.ivLength = ivLength;
+ }
+
+ /**
+ * 通过密钥和salt值,获取实际的密钥
+ *
+ * @param pass 密钥
+ * @param salt 加盐值
+ * @return 实际密钥
+ */
+ public byte[][] getKeyAndIV(final byte[] pass, final byte[] salt) {
+ final byte[][] keyAndIvResult = new byte[2][];
+ if (null == salt) {
+ keyAndIvResult[0] = pass;
+ return keyAndIvResult;
+ }
+ Assert.isTrue(SALT_LEN == salt.length);
+
+ final byte[] passAndSalt = ByteUtil.concat(pass, salt);
+
+ byte[] hash = new byte[0];
+ byte[] keyAndIv = new byte[0];
+ for (int i = 0; i < 3 && keyAndIv.length < keyLength + ivLength; i++) {
+ final byte[] hashData = ByteUtil.concat(hash, passAndSalt);
+ hash = digest.digest(hashData);
+ keyAndIv = ByteUtil.concat(keyAndIv, hash);
+ }
+
+ keyAndIvResult[0] = Arrays.copyOfRange(keyAndIv, 0, keyLength);
+ if (!StrUtil.containsAnyIgnoreCase(algorithm, "RC", "DES")) {
+ keyAndIvResult[1] = Arrays.copyOfRange(keyAndIv, keyLength, keyLength + ivLength);
+ }
+ return keyAndIvResult;
+ }
+}
diff --git a/hutool-crypto/src/main/java/org/dromara/hutool/crypto/symmetric/SaltUtil.java b/hutool-crypto/src/main/java/org/dromara/hutool/crypto/symmetric/SaltUtil.java
deleted file mode 100755
index efe3f6e94..000000000
--- a/hutool-crypto/src/main/java/org/dromara/hutool/crypto/symmetric/SaltUtil.java
+++ /dev/null
@@ -1,86 +0,0 @@
-/*
- * Copyright (c) 2023 looly(loolly@aliyun.com)
- * Hutool is licensed under Mulan PSL v2.
- * You can use this software according to the terms and conditions of the Mulan PSL v2.
- * You may obtain a copy of Mulan PSL v2 at:
- * http://license.coscl.org.cn/MulanPSL2
- * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
- * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
- * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
- * See the Mulan PSL v2 for more details.
- */
-
-package org.dromara.hutool.crypto.symmetric;
-
-import org.dromara.hutool.core.array.ArrayUtil;
-import org.dromara.hutool.crypto.digest.DigestUtil;
-
-import java.nio.charset.StandardCharsets;
-import java.security.MessageDigest;
-import java.util.Arrays;
-
-/**
- * OpenSSL风格前缀和加盐相关工具类
- * 参考:https://stackoverflow.com/questions/32508961/java-equivalent-of-an-openssl-aes-cbc-encryption
- *
- * @author looly
- * @since 6.0.0
- */
-public class SaltUtil {
-
- private final static byte SALT_LEN = 8;
- private final static byte IV_LEN = 16;
- private final static byte KEY_LEN = 32;
-
- /**
- * OpenSSL's magic initial bytes.
- */
- private static final byte[] SALTED_MAGIC = "Salted__".getBytes(StandardCharsets.US_ASCII);
-
- /**
- * 获取8位salt随机数
- *
- * @param encryptedData 密文
- * @return salt随机数
- */
- public static byte[] getSalt(final byte[] encryptedData) {
- if (ArrayUtil.startWith(encryptedData, SALTED_MAGIC)) {
- return Arrays.copyOfRange(encryptedData, SALTED_MAGIC.length, SALTED_MAGIC.length + SALT_LEN);
- }
- return null;
- }
-
- /**
- * 通过密钥和salt值,获取实际的密钥
- *
- * @param pass 密钥
- * @param salt 加盐值
- * @return 实际密钥
- */
- public static byte[] getKey(final byte[] pass, final byte[] salt) {
- if (null == salt) {
- return pass;
- }
- final byte[] passAndSalt = arrayConcat(pass, salt);
-
- byte[] hash = new byte[0];
- byte[] keyAndIv = new byte[0];
- for (int i = 0; i < 3 && keyAndIv.length < 48; i++) {
- final byte[] hashData = arrayConcat(hash, passAndSalt);
- hash = DigestUtil.md5(hashData);
- keyAndIv = arrayConcat(keyAndIv, hash);
- }
-
- return Arrays.copyOfRange(keyAndIv, 0, KEY_LEN);
- }
-
- private static byte[] arrayConcat(final byte[] a, final byte[] b) {
- if (ArrayUtil.isEmpty(a)) {
- return b;
- }
- final byte[] c = new byte[a.length + b.length];
- System.arraycopy(a, 0, c, 0, a.length);
- System.arraycopy(b, 0, c, a.length, b.length);
- return c;
- }
-}
diff --git a/hutool-crypto/src/main/java/org/dromara/hutool/crypto/symmetric/SymmetricCrypto.java b/hutool-crypto/src/main/java/org/dromara/hutool/crypto/symmetric/SymmetricCrypto.java
index ec3121512..4a0dbd2a4 100644
--- a/hutool-crypto/src/main/java/org/dromara/hutool/crypto/symmetric/SymmetricCrypto.java
+++ b/hutool-crypto/src/main/java/org/dromara/hutool/crypto/symmetric/SymmetricCrypto.java
@@ -15,6 +15,7 @@ package org.dromara.hutool.crypto.symmetric;
import org.dromara.hutool.core.io.IORuntimeException;
import org.dromara.hutool.core.io.IoUtil;
import org.dromara.hutool.core.lang.Assert;
+import org.dromara.hutool.core.lang.Console;
import org.dromara.hutool.core.lang.Opt;
import org.dromara.hutool.core.array.ArrayUtil;
import org.dromara.hutool.core.codec.HexUtil;
@@ -32,10 +33,7 @@ import javax.crypto.CipherOutputStream;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEParameterSpec;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.io.Serializable;
+import java.io.*;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.SecureRandom;
@@ -65,7 +63,7 @@ public class SymmetricCrypto implements SymmetricEncryptor, SymmetricDecryptor,
private boolean isZeroPadding;
private final Lock lock = new ReentrantLock();
- // ------------------------------------------------------------------ Constructor start
+ // region ----- Constructor
/**
* 构造,使用随机密钥
@@ -140,7 +138,7 @@ public class SymmetricCrypto implements SymmetricEncryptor, SymmetricDecryptor,
initParams(algorithm, paramsSpec);
}
- // ------------------------------------------------------------------ Constructor end
+ // endregion
/**
* 初始化
@@ -234,9 +232,20 @@ public class SymmetricCrypto implements SymmetricEncryptor, SymmetricDecryptor,
* @since 5.7.12
*/
public SymmetricCrypto setMode(final CipherMode mode) {
+ return setMode(mode, null);
+ }
+
+ /**
+ * 初始化模式并清空数据
+ *
+ * @param mode 模式枚举
+ * @param salt 加盐值,用于
+ * @return this
+ */
+ public SymmetricCrypto setMode(final CipherMode mode, final byte[] salt) {
lock.lock();
try {
- initMode(mode.getValue());
+ initMode(mode.getValue(), salt);
} catch (final Exception e) {
throw new CryptoException(e);
} finally {
@@ -281,23 +290,39 @@ public class SymmetricCrypto implements SymmetricEncryptor, SymmetricDecryptor,
@Override
public byte[] encrypt(final byte[] data) {
+ return encrypt(data, null);
+ }
+
+ /**
+ * 加密
+ *
+ * @param data 被加密的bytes
+ * @param salt 加盐值,如果为{@code null}不设置,否则生成带Salted__头的密文数据
+ * @return 加密后的bytes
+ * @since 6.0.0
+ */
+ public byte[] encrypt(final byte[] data, final byte[] salt) {
lock.lock();
+
+ byte[] result;
try {
- final Cipher cipher = initMode(Cipher.ENCRYPT_MODE);
- return cipher.doFinal(paddingDataWithZero(data, cipher.getBlockSize()));
+ final Cipher cipher = initMode(Cipher.ENCRYPT_MODE, salt);
+ result = cipher.doFinal(paddingDataWithZero(data, cipher.getBlockSize()));
} catch (final Exception e) {
throw new CryptoException(e);
} finally {
lock.unlock();
}
+ return OpenSSLSaltParser.addMagic(result, salt);
}
@Override
public void encrypt(final InputStream data, final OutputStream out, final boolean isClose) throws IORuntimeException {
lock.lock();
+
CipherOutputStream cipherOutputStream = null;
try {
- final Cipher cipher = initMode(Cipher.ENCRYPT_MODE);
+ final Cipher cipher = initMode(Cipher.ENCRYPT_MODE, null);
cipherOutputStream = new CipherOutputStream(out, cipher);
final long length = IoUtil.copy(data, cipherOutputStream);
if (this.isZeroPadding) {
@@ -335,11 +360,11 @@ public class SymmetricCrypto implements SymmetricEncryptor, SymmetricDecryptor,
final byte[] decryptData;
lock.lock();
-
+ final byte[] salt = OpenSSLSaltParser.getSalt(bytes);
try {
- final Cipher cipher = initMode(Cipher.DECRYPT_MODE);
+ final Cipher cipher = initMode(Cipher.DECRYPT_MODE, salt);
blockSize = cipher.getBlockSize();
- decryptData = cipher.doFinal(bytes);
+ decryptData = cipher.doFinal(OpenSSLSaltParser.getData(bytes));
} catch (final Exception e) {
throw new CryptoException(e);
} finally {
@@ -354,7 +379,7 @@ public class SymmetricCrypto implements SymmetricEncryptor, SymmetricDecryptor,
lock.lock();
CipherInputStream cipherInputStream = null;
try {
- final Cipher cipher = initMode(Cipher.DECRYPT_MODE);
+ final Cipher cipher = initMode(Cipher.DECRYPT_MODE, null);
cipherInputStream = new CipherInputStream(data, cipher);
if (this.isZeroPadding) {
final int blockSize = cipher.getBlockSize();
@@ -424,8 +449,19 @@ public class SymmetricCrypto implements SymmetricEncryptor, SymmetricDecryptor,
* @throws InvalidKeyException 无效key
* @throws InvalidAlgorithmParameterException 无效算法
*/
- private Cipher initMode(final int mode) throws InvalidKeyException, InvalidAlgorithmParameterException {
- return this.cipherWrapper.initMode(mode, this.secretKey).getRaw();
+ private Cipher initMode(final int mode, final byte[] salt) throws InvalidKeyException, InvalidAlgorithmParameterException {
+ SecretKey secretKey = this.secretKey;
+ if (null != salt) {
+ // /issues#I6YWWD,提供OpenSSL格式兼容支持
+ final String algorithm = getCipher().getAlgorithm();
+ final byte[][] keyAndIV = OpenSSLSaltParser.ofMd5(32, algorithm)
+ .getKeyAndIV(secretKey.getEncoded(), salt);
+ secretKey = KeyUtil.generateKey(algorithm, keyAndIV[0]);
+ if(ArrayUtil.isNotEmpty(keyAndIV[1])){
+ this.cipherWrapper.setParams(new IvParameterSpec(keyAndIV[1]));
+ }
+ }
+ return this.cipherWrapper.initMode(mode, secretKey).getRaw();
}
/**
diff --git a/hutool-crypto/src/test/java/org/dromara/hutool/crypto/symmetric/RC4Test.java b/hutool-crypto/src/test/java/org/dromara/hutool/crypto/symmetric/RC4Test.java
index 921125ff6..bb26a1e9a 100644
--- a/hutool-crypto/src/test/java/org/dromara/hutool/crypto/symmetric/RC4Test.java
+++ b/hutool-crypto/src/test/java/org/dromara/hutool/crypto/symmetric/RC4Test.java
@@ -69,11 +69,4 @@ public class RC4Test {
final String msg2 = SymmetricCrypto.decryptStr(encryptHex2);
Assertions.assertEquals(message2, msg2);
}
-
- @Test
- void decryptTest() {
- GlobalProviderFactory.setUseCustomProvider(false);
- final String key16 = "1234567890123456";
- final SymmetricCrypto aes = new SymmetricCrypto("SymmetricCrypto", key16.getBytes());
- }
}
diff --git a/hutool-crypto/src/test/java/org/dromara/hutool/crypto/symmetric/SaltUtilTest.java b/hutool-crypto/src/test/java/org/dromara/hutool/crypto/symmetric/SaltUtilTest.java
new file mode 100755
index 000000000..bd1dc0ff7
--- /dev/null
+++ b/hutool-crypto/src/test/java/org/dromara/hutool/crypto/symmetric/SaltUtilTest.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (c) 2023 looly(loolly@aliyun.com)
+ * Hutool is licensed under Mulan PSL v2.
+ * You can use this software according to the terms and conditions of the Mulan PSL v2.
+ * You may obtain a copy of Mulan PSL v2 at:
+ * http://license.coscl.org.cn/MulanPSL2
+ * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
+ * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
+ * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
+ * See the Mulan PSL v2 for more details.
+ */
+
+package org.dromara.hutool.crypto.symmetric;
+
+import org.dromara.hutool.crypto.KeyUtil;
+import org.dromara.hutool.crypto.SecureUtil;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import javax.crypto.SecretKey;
+
+public class SaltUtilTest {
+
+ /**
+ * 测试:
+ * https://www.bejson.com/enc/aesdes/
+ */
+ @Test
+ void rc4Test() {
+ final String encrypted = "U2FsdGVkX19DSROPe0+Ejkw84osqWw==";
+
+ final byte[] salt = OpenSSLSaltParser.getSalt(SecureUtil.decode(encrypted));
+ Assertions.assertNotNull(salt);
+
+ final byte[][] keyAndIV = OpenSSLSaltParser.ofMd5(32, "RC4")
+ .getKeyAndIV("1234567890123456".getBytes(), salt);
+ Assertions.assertNotNull(keyAndIV);
+ Assertions.assertNotNull(keyAndIV[0]);
+
+ final SecretKey rc4Key = KeyUtil.generateKey("RC4", keyAndIV[0]);
+ Assertions.assertNotNull(rc4Key);
+
+ final byte[] data = OpenSSLSaltParser.getData(SecureUtil.decode(encrypted));
+
+ final SymmetricCrypto rc4 = new SymmetricCrypto("RC4", rc4Key);
+ final String decrypt = rc4.decryptStr(data);
+ Assertions.assertEquals("hutool", decrypt);
+ }
+
+ /**
+ * 测试:
+ * https://www.bejson.com/enc/aesdes/
+ */
+ @Test
+ void rc4Test2() {
+ final String encrypted = "U2FsdGVkX19DSROPe0+Ejkw84osqWw==";
+ final SymmetricCrypto rc4 = new SymmetricCrypto("RC4", "1234567890123456".getBytes());
+ final String decrypt = rc4.decryptStr(encrypted);
+ Assertions.assertEquals("hutool", decrypt);
+ }
+
+ /**
+ * 测试:
+ * https://www.bejson.com/enc/aesdes/
+ */
+ @Test
+ void aesTest() {
+ final String encrypted = "U2FsdGVkX1+lqsuKAR+OdOeNduvx5wgXf6yEUdDIh3g=";
+ final SymmetricCrypto des = new SymmetricCrypto("AES/CBC/PKCS5Padding", "1234567890123456".getBytes());
+ final String decrypt = des.decryptStr(encrypted);
+ Assertions.assertEquals("hutool", decrypt);
+ }
+}