This commit is contained in:
Looly 2024-01-08 21:57:53 +08:00
parent 6fba25ad7d
commit 46febc3d05
8 changed files with 252 additions and 73 deletions

View File

@ -12,6 +12,10 @@
package org.dromara.hutool.crypto;
import org.bouncycastle.crypto.BufferedBlockCipher;
import java.util.Arrays;
/**
* 密码接口提供统一的API用于兼容和统一JCE和BouncyCastle等库的操作
*
@ -30,7 +34,7 @@ public interface Cipher {
/**
* 获取块大小当为Stream方式加密时返回0
*
* @return 块大小
* @return 块大小-1表示非块加密
*/
int getBlockSize();
@ -42,16 +46,59 @@ public interface Cipher {
*/
void init(CipherMode mode, Parameters parameters);
/**
* 根据输入长度获取输出长度输出长度与算法相关<br>
* 输出长度只针对本次输入关联即len长度的数据对应输出长度加doFinal的长度
*
* @param len 输入长度
* @return 输出长度-1表示非块加密
*/
int getOutputSize(int len);
/**
* 执行运算可以是加密运算或解密运算
*
* @param data 被处理的数据
* @return 运算结果
* @param in 输入数据
* @param inOff 输入数据开始位置
* @param len 被处理数据长度
* @param out 输出数据
* @param outOff 输出数据开始位置
* @return 处理长度
*/
byte[] process(byte[] data);
int process(byte[] in, int inOff, int len, byte[] out, int outOff);
/**
* 处理最后一块数据<br>
* {@link #process(byte[], int, int, byte[], int)}处理完数据后非完整块数据此方法用于处理块中剩余的bytes<br>
* 如加密数据要求128bit即16byes的整数单数处理数据后为15bytes此时根据padding方式不同可填充剩余1byte为指定值如填充0<br>
* 当对数据进行分段加密时需要首先多次执行process方法在最后一块数据处理完后执行此方法
*
* @param out 经过process执行过运算的结果数据
* @param outOff 数据处理开始位置
* @return 处理长度
*/
int doFinal(byte[] out, int outOff);
/**
* 处理数据并返回最终结果
*
* @param in 输入数据
* @return 结果数据
*/
default byte[] processFinal(final byte[] in) {
final byte[] buf = new byte[getOutputSize(in.length)];
int len = process(in, 0, in.length, buf, 0);
len += doFinal(buf, len);
if (len == buf.length) {
return buf;
}
return Arrays.copyOfRange(buf, 0, len);
}
/**
* Cipher所需参数包括KeyRandomIV等信息
*/
interface Parameters { }
interface Parameters {
}
}

View File

@ -24,19 +24,19 @@ public enum CipherMode {
/**
* 加密模式
*/
encrypt(Cipher.ENCRYPT_MODE),
ENCRYPT(Cipher.ENCRYPT_MODE),
/**
* 解密模式
*/
decrypt(Cipher.DECRYPT_MODE),
DECRYPT(Cipher.DECRYPT_MODE),
/**
* 包装模式
*/
wrap(Cipher.WRAP_MODE),
WRAP(Cipher.WRAP_MODE),
/**
* 拆包模式
*/
unwrap(Cipher.UNWRAP_MODE);
UNWRAP(Cipher.UNWRAP_MODE);
/**

View File

@ -15,6 +15,7 @@ package org.dromara.hutool.crypto;
import org.dromara.hutool.core.lang.Assert;
import org.dromara.hutool.core.lang.wrapper.Wrapper;
import javax.crypto.ShortBufferException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.Key;
@ -63,6 +64,11 @@ public class JceCipher implements Cipher, Wrapper<javax.crypto.Cipher> {
return this.cipher.getBlockSize();
}
@Override
public int getOutputSize(final int len) {
return this.cipher.getOutputSize(len);
}
@Override
public void init(final CipherMode mode, final Parameters parameters) {
Assert.isInstanceOf(JceParameters.class, parameters, "Only support JceParameters!");
@ -100,8 +106,30 @@ public class JceCipher implements Cipher, Wrapper<javax.crypto.Cipher> {
}
@Override
public byte[] process(final byte[] data) {
return new byte[0];
public int process(final byte[] in, final int inOff, final int len, final byte[] out, final int outOff) {
try {
return this.cipher.update(in, inOff, len, out, outOff);
} catch (final ShortBufferException e) {
throw new CryptoException(e);
}
}
@Override
public int doFinal(final byte[] out, final int outOff) {
try {
return this.cipher.doFinal(out, outOff);
} catch (final Exception e) {
throw new CryptoException(e);
}
}
@Override
public byte[] processFinal(final byte[] data) {
try {
return this.cipher.doFinal(data);
} catch (final Exception e) {
throw new CryptoException(e);
}
}
/**

View File

@ -21,8 +21,6 @@ import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import java.io.IOException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.PrivateKey;
import java.security.PublicKey;
@ -255,7 +253,7 @@ public class AsymmetricCrypto extends AbstractAsymmetricCrypto<AsymmetricCrypto>
final Key key = getKeyByType(keyType);
lock.lock();
try {
final Cipher cipher = initMode(Cipher.ENCRYPT_MODE, key);
final JceCipher cipher = initMode(CipherMode.ENCRYPT, key);
if (this.encryptBlockSize < 0) {
// 在引入BC库情况下自动获取块大小
@ -280,7 +278,7 @@ public class AsymmetricCrypto extends AbstractAsymmetricCrypto<AsymmetricCrypto>
final Key key = getKeyByType(keyType);
lock.lock();
try {
final Cipher cipher = initMode(Cipher.DECRYPT_MODE, key);
final JceCipher cipher = initMode(CipherMode.DECRYPT, key);
if (this.decryptBlockSize < 0) {
// 在引入BC库情况下自动获取块大小
@ -352,9 +350,10 @@ public class AsymmetricCrypto extends AbstractAsymmetricCrypto<AsymmetricCrypto>
* @throws BadPaddingException padding错误异常
* @throws IOException IO异常不会被触发
*/
@SuppressWarnings("resource")
private byte[] doFinalWithBlock(final byte[] data, final int maxBlockSize) throws IllegalBlockSizeException, BadPaddingException, IOException {
final int dataLength = data.length;
@SuppressWarnings("resource") final FastByteArrayOutputStream out = new FastByteArrayOutputStream();
final FastByteArrayOutputStream out = new FastByteArrayOutputStream();
int offSet = 0;
// 剩余长度
@ -373,18 +372,15 @@ public class AsymmetricCrypto extends AbstractAsymmetricCrypto<AsymmetricCrypto>
}
/**
* 初始化{@link Cipher}的模式如加密模式或解密模式
* 初始化{@link JceCipher}的模式如加密模式或解密模式
*
* @param mode 模式可选{@link Cipher#ENCRYPT_MODE}或者{@link Cipher#DECRYPT_MODE}
* @param mode 模式可选{@link CipherMode#ENCRYPT}或者{@link CipherMode#DECRYPT}
* @param key 密钥
* @return {@link Cipher}
* @throws InvalidAlgorithmParameterException 异常算法错误
* @throws InvalidKeyException 异常KEY错误
* @return {@link JceCipher}
*/
private Cipher initMode(final int mode, final Key key) throws InvalidAlgorithmParameterException, InvalidKeyException {
private JceCipher initMode(final CipherMode mode, final Key key) {
final JceCipher cipher = this.cipher;
cipher.init(mode,
new JceCipher.JceParameters(key, this.algorithmParameterSpec, this.random));
return cipher.getRaw();
cipher.init(mode, new JceCipher.JceParameters(key, this.algorithmParameterSpec, this.random));
return cipher;
}
}

View File

@ -14,6 +14,9 @@ package org.dromara.hutool.crypto.bc;
import org.bouncycastle.crypto.BufferedBlockCipher;
import org.bouncycastle.crypto.CipherParameters;
import org.bouncycastle.crypto.InvalidCipherTextException;
import org.bouncycastle.crypto.StreamCipher;
import org.bouncycastle.crypto.modes.AEADBlockCipher;
import org.dromara.hutool.core.lang.Assert;
import org.dromara.hutool.core.lang.wrapper.Wrapper;
import org.dromara.hutool.crypto.Cipher;
@ -21,16 +24,31 @@ import org.dromara.hutool.crypto.CipherMode;
import org.dromara.hutool.crypto.CryptoException;
/**
* 基于BouncyCastle库的{@link BufferedBlockCipher}封装的加密解密实现
* 基于BouncyCastle库封装的加密解密实现包装包括
* <ul>
* <li>{@link BufferedBlockCipher}</li>
* <li>{@link StreamCipher}</li>
* <li>{@link AEADBlockCipher}</li>
* </ul>
*
* @author Looly, changhr2013
*/
public class BCCipher implements Cipher, Wrapper<BufferedBlockCipher> {
public class BCCipher implements Cipher, Wrapper<Object> {
/**
* {@link BufferedBlockCipher}包含enginemodepadding
* {@link BufferedBlockCipher}块加密包含enginemodepadding
*/
private final BufferedBlockCipher blockCipher;
private BufferedBlockCipher blockCipher;
/**
* {@link AEADBlockCipher}, 关联数据的认证加密(Authenticated Encryption with Associated Data)
*/
private AEADBlockCipher aeadBlockCipher;
/**
* {@link StreamCipher}
*/
private StreamCipher streamCipher;
// region ----- 构造
/**
* 构造
@ -41,42 +59,112 @@ public class BCCipher implements Cipher, Wrapper<BufferedBlockCipher> {
this.blockCipher = Assert.notNull(blockCipher);
}
/**
* 构造
*
* @param aeadBlockCipher {@link AEADBlockCipher}
*/
public BCCipher(final AEADBlockCipher aeadBlockCipher) {
this.aeadBlockCipher = Assert.notNull(aeadBlockCipher);
}
/**
* 构造
*
* @param streamCipher {@link StreamCipher}
*/
public BCCipher(final StreamCipher streamCipher) {
this.streamCipher = Assert.notNull(streamCipher);
}
// endregion
@Override
public BufferedBlockCipher getRaw() {
public Object getRaw() {
if (null != this.blockCipher) {
return this.blockCipher;
}
if (null != this.aeadBlockCipher) {
return this.aeadBlockCipher;
}
return this.streamCipher;
}
@Override
public String getAlgorithmName() {
if (null != this.blockCipher) {
return this.blockCipher.getUnderlyingCipher().getAlgorithmName();
}
if (null != this.aeadBlockCipher) {
return this.aeadBlockCipher.getUnderlyingCipher().getAlgorithmName();
}
return this.streamCipher.getAlgorithmName();
}
@Override
public int getBlockSize() {
if (null != this.blockCipher) {
return this.blockCipher.getBlockSize();
}
if (null != this.aeadBlockCipher) {
return this.aeadBlockCipher.getUnderlyingCipher().getBlockSize();
}
return -1;
}
@Override
public void init(final CipherMode mode, final Parameters parameters) {
Assert.isInstanceOf(BCParameters.class, parameters, "Only support BCParameters!");
this.blockCipher.init(mode == CipherMode.encrypt, ((BCParameters) parameters).parameters);
if (null != this.blockCipher) {
this.blockCipher.init(mode == CipherMode.ENCRYPT, ((BCParameters) parameters).parameters);
return;
}
if (null != this.aeadBlockCipher) {
this.aeadBlockCipher.init(mode == CipherMode.ENCRYPT, ((BCParameters) parameters).parameters);
return;
}
this.streamCipher.init(mode == CipherMode.ENCRYPT, ((BCParameters) parameters).parameters);
}
@Override
public byte[] process(final byte[] data) {
final byte[] out;
try {
final BufferedBlockCipher cipher = this.blockCipher;
final int updateOutputSize = cipher.getOutputSize(data.length);
final byte[] buf = new byte[updateOutputSize];
int len = cipher.processBytes(data, 0, data.length, buf, 0);
len += cipher.doFinal(buf, len);
out = new byte[len];
System.arraycopy(buf, 0, out, 0, len);
} catch (final Exception e) {
throw new CryptoException("encrypt/decrypt process exception.", e);
public int getOutputSize(final int len) {
if (null != this.blockCipher) {
return this.blockCipher.getOutputSize(len);
}
return out;
if (null != this.aeadBlockCipher) {
return this.aeadBlockCipher.getOutputSize(len);
}
return -1;
}
@Override
public int process(final byte[] in, final int inOff, final int len, final byte[] out, final int outOff) {
if (null != this.blockCipher) {
return this.blockCipher.processBytes(in, inOff, len, out, outOff);
}
if (null != this.aeadBlockCipher) {
return this.aeadBlockCipher.processBytes(in, inOff, len, out, outOff);
}
return this.streamCipher.processBytes(in, inOff, len, out, outOff);
}
@Override
public int doFinal(final byte[] out, final int outOff) {
if (null != this.blockCipher) {
try {
return this.blockCipher.doFinal(out, outOff);
} catch (final InvalidCipherTextException e) {
throw new CryptoException(e);
}
}
if (null != this.aeadBlockCipher) {
try {
return this.aeadBlockCipher.doFinal(out, outOff);
} catch (final InvalidCipherTextException e) {
throw new CryptoException(e);
}
}
return 0;
}
/**

View File

@ -34,8 +34,6 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.SecureRandom;
import java.security.spec.AlgorithmParameterSpec;
import java.util.concurrent.locks.Lock;
@ -253,9 +251,7 @@ public class SymmetricCrypto implements SymmetricEncryptor, SymmetricDecryptor,
public SymmetricCrypto setMode(final CipherMode mode, final byte[] salt) {
lock.lock();
try {
initMode(mode.getValue(), salt);
} catch (final Exception e) {
throw new CryptoException(e);
initMode(mode, salt);
} finally {
lock.unlock();
}
@ -313,10 +309,8 @@ public class SymmetricCrypto implements SymmetricEncryptor, SymmetricDecryptor,
byte[] result;
lock.lock();
try {
final Cipher cipher = initMode(Cipher.ENCRYPT_MODE, salt);
result = cipher.doFinal(paddingDataWithZero(data, cipher.getBlockSize()));
} catch (final Exception e) {
throw new CryptoException(e);
final JceCipher cipher = initMode(CipherMode.ENCRYPT, salt);
result = cipher.processFinal(paddingDataWithZero(data, cipher.getBlockSize()));
} finally {
lock.unlock();
}
@ -328,8 +322,8 @@ public class SymmetricCrypto implements SymmetricEncryptor, SymmetricDecryptor,
CipherOutputStream cipherOutputStream = null;
lock.lock();
try {
final Cipher cipher = initMode(Cipher.ENCRYPT_MODE, null);
cipherOutputStream = new CipherOutputStream(out, cipher);
final JceCipher cipher = initMode(CipherMode.ENCRYPT, null);
cipherOutputStream = new CipherOutputStream(out, cipher.getRaw());
final long length = IoUtil.copy(data, cipherOutputStream);
if (this.isZeroPadding) {
final int blockSize = cipher.getBlockSize();
@ -367,9 +361,9 @@ public class SymmetricCrypto implements SymmetricEncryptor, SymmetricDecryptor,
lock.lock();
try {
final byte[] salt = SaltMagic.getSalt(bytes);
final Cipher cipher = initMode(Cipher.DECRYPT_MODE, salt);
final JceCipher cipher = initMode(CipherMode.DECRYPT, salt);
blockSize = cipher.getBlockSize();
decryptData = cipher.doFinal(SaltMagic.getData(bytes));
decryptData = cipher.processFinal(SaltMagic.getData(bytes));
} catch (final Exception e) {
throw new CryptoException(e);
} finally {
@ -384,8 +378,8 @@ public class SymmetricCrypto implements SymmetricEncryptor, SymmetricDecryptor,
CipherInputStream cipherInputStream = null;
lock.lock();
try {
final Cipher cipher = initMode(Cipher.DECRYPT_MODE, null);
cipherInputStream = new CipherInputStream(data, cipher);
final JceCipher cipher = initMode(CipherMode.DECRYPT, null);
cipherInputStream = new CipherInputStream(data, cipher.getRaw());
if (this.isZeroPadding) {
final int blockSize = cipher.getBlockSize();
if (blockSize > 0) {
@ -447,14 +441,12 @@ public class SymmetricCrypto implements SymmetricEncryptor, SymmetricDecryptor,
}
/**
* 初始化{@link Cipher}为加密或者解密模式
* 初始化{@link JceCipher}为加密或者解密模式
*
* @param mode 模式{@link Cipher#ENCRYPT_MODE} {@link Cipher#DECRYPT_MODE}
* @param mode 模式{@link CipherMode#ENCRYPT} {@link CipherMode#DECRYPT}
* @return {@link Cipher}
* @throws InvalidKeyException 无效key
* @throws InvalidAlgorithmParameterException 无效算法
*/
private Cipher initMode(final int mode, final byte[] salt) throws InvalidKeyException, InvalidAlgorithmParameterException {
private JceCipher initMode(final CipherMode mode, final byte[] salt) {
SecretKey secretKey = this.secretKey;
if (null != salt) {
// /issues#I6YWWD提供OpenSSL格式兼容支持
@ -470,7 +462,7 @@ public class SymmetricCrypto implements SymmetricEncryptor, SymmetricDecryptor,
final JceCipher cipher = this.cipher;
cipher.init(mode,
new JceCipher.JceParameters(secretKey, this.algorithmParameterSpec, this.random));
return cipher.getRaw();
return cipher;
}
/**

View File

@ -0,0 +1,28 @@
package org.dromara.hutool.crypto.bc;
import org.bouncycastle.crypto.BufferedBlockCipher;
import org.bouncycastle.crypto.DefaultBufferedBlockCipher;
import org.bouncycastle.crypto.engines.SM4Engine;
import org.bouncycastle.crypto.modes.CBCBlockCipher;
import org.bouncycastle.crypto.params.KeyParameter;
import org.dromara.hutool.core.util.ByteUtil;
import org.dromara.hutool.crypto.CipherMode;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
public class BCCipherTest {
@Test
void sm4Test() {
final byte[] data = ByteUtil.toUtf8Bytes("我是测试Hutool的字符串00");
final BufferedBlockCipher blockCipher = new DefaultBufferedBlockCipher(CBCBlockCipher.newInstance(new SM4Engine()));
final BCCipher bcCipher = new BCCipher(blockCipher);
bcCipher.init(CipherMode.ENCRYPT, new BCCipher.BCParameters(new KeyParameter(ByteUtil.toUtf8Bytes("1234567890000000"))));
final byte[] encryptData = bcCipher.processFinal(data);
bcCipher.init(CipherMode.DECRYPT, new BCCipher.BCParameters(new KeyParameter(ByteUtil.toUtf8Bytes("1234567890000000"))));
final byte[] decryptData = bcCipher.processFinal(encryptData);
Assertions.assertArrayEquals(data, decryptData);
}
}

View File

@ -135,9 +135,9 @@ public class SymmetricTest {
final AES aes = new AES(Mode.CBC, Padding.PKCS5Padding, "0123456789ABHAEQ".getBytes(), "DYgjCEIMVrj2W9xN".getBytes());
// 加密为16进制表示
aes.setMode(CipherMode.encrypt);
aes.setMode(CipherMode.ENCRYPT);
final String randomData = aes.updateHex(content.getBytes(StandardCharsets.UTF_8));
aes.setMode(CipherMode.encrypt);
aes.setMode(CipherMode.ENCRYPT);
final String randomData2 = aes.updateHex(content.getBytes(StandardCharsets.UTF_8));
Assertions.assertEquals(randomData2, randomData);
Assertions.assertEquals(randomData, "cd0e3a249eaf0ed80c330338508898c4");