This commit is contained in:
Looly 2023-04-26 19:43:51 +08:00
parent c2d419ffdc
commit ba2813becb
6 changed files with 342 additions and 109 deletions

View File

@ -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();
}
}

View File

@ -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中加盐解析器<br>
* 参考
* <pre>
* 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
* </pre>
*
* @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);
/**
* 获取魔术值和随机盐的长度16128位
*/
public static final int MAGIC_SALT_LENGTH = SALTED_MAGIC.length + SALT_LEN;
/**
* 获取去除头部盐的加密数据<br>
*
* @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随机数<br>
*
* @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头生成的密文格式为
* <pre>
* Salted__[salt][data]
* </pre>
*
* @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头生成的密文格式为
* <pre>
* Salted__[salt]
* </pre>
*
* @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;
}
}

View File

@ -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风格前缀和加盐相关工具类<br>
* 参考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随机数<br>
*
* @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;
}
}

View File

@ -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();
}
/**

View File

@ -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());
}
}

View File

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