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); + } +}