我在Minio中有一个加密对象,使用AES 128位CBC算法加密。
对象很大(〜50 MB),所以我没有将其完全加载到内存中(这可能会导致内存不足异常),而是以1MB的块大小进行检索。我需要在使用前对其解密。
是否可以通过这种方式解密对象(一次1MB,整个对象一次性加密)? 如果是,我该怎么办? 我尝试解密产生以下错误的16字节块:
javax.crypto.BadPaddingException: Given final block not properly padded
javax.crypto.IllegalBlockSizeException: Input length must be multiple of 16 when decrypting with padded cipher
答案 0 :(得分:1)
是的,使用AES-128-CBC,可以仅解密一个密文块。每个块为128位(16字节)。
请参见https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Cipher_block_chaining_(CBC)上的图。如您所见,要解密任何密文块,您需要对密文块进行AES解密,然后将明文与先前的密文块进行异或。 (对于第一个块,纯文本与IV进行XOR运算。)
您正在使用的库可能会引发这些异常,因为它正在检查是否正确填充了解密的密文。当然,如果仅解密一个任意的密文块,它将没有适当的填充。但是,您可以使用openssl
之类的工具,根据给定的密文,密钥和上一个密文块来解密单个密文块,如下所示:
echo -n 'bc6d8afc78e805b7ed7551e42da4d877' | xxd -p -r | openssl aes-128-cbc -d -nopad -K e3e33d2d9591b462c55503f7ec697839 -iv 1d3fa2b7c9008e1cdbc76a1f22388b89
其中:
bc6d8afc78e805b7ed7551e42da4d877是您要解密的密文块
e3e33d2d9591b462c55503f7ec697839是关键
1d3fa2b7c9008e1cdbc76a1f22388b89是密文的前一个块
答案 1 :(得分:1)
要避免出现“内存不足错误”,您想以1 mb大小的块解密一个较大的(加密的)文件-是的,使用AES CBC模式。
下面找到一个完整的示例,该示例生成带有随机内容的样本纯文本文件('plaintext.dat'),其大小为50 mb + 1字节(+1字节适合测试不是精确的16的倍数= AES块大小。
下一步,使用随机创建的初始化向量和密钥将此文件加密为'ciphertext.dat'。
最后一步是请求的解密方法-它以1 mb的块解密加密的文件,并在'// obuf行中保存解密的块,对数据做您想做的事情'和'// final数据”,您确实在字节数组obuf中具有解密的数据。为了进行测试,我将以追加模式将解密后的数据写入文件'decryptedtext.dat'(因此,如果存在该文件,将在开始时将其删除)。
为了证明解密成功,我正在比较纯文本文件和解密文本文件的SHA256哈希。
两个注意事项:我使用的是32字节= 256位长的AES CBC 256密钥。该程序没有适当的异常处理,仅用于教育目的。
结果:
decrypt AES CBC 256 in 1 mb chunks
file with random data created: plaintext.dat
encryption to ciphertext.dat was successfull: true
decryption in chunks of 1 mb
decrypted file written to decryptedtext.dat
plaintext equals decrytedtext file: true
代码:
import javax.crypto.*;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.*;
import java.nio.file.Files;
import java.security.*;
import java.util.Arrays;
public class AES_CBC_chunk_decryption {
public static void main(String[] args) throws IOException, InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException,
InvalidAlgorithmParameterException, BadPaddingException, IllegalBlockSizeException {
System.out.println("https://stackoverflow.com/questions/63325528/decrypt-in-chunks-a-aes-128-cbc-encrypted-object/63325529#63325529");
System.out.println("decrypt AES CBC 256 in 1 mb chunks");
// setup for creation of a 50mb encrypted file
int filesize = (50 * 1024 * 1024) + 1; // 50 mb + 1 byte = 52428801 bytes
String filenamePlaintext = "plaintext.dat";
String filenameCiphertext = "ciphertext.dat";
String filenameDecryptedtext = "decryptedtext.dat";
File file = new File("plaintext.dat");
// fill with random bytes.
try (FileOutputStream out = new FileOutputStream(file)) {
byte[] bytes = new byte[filesize];
new SecureRandom().nextBytes(bytes);
out.write(bytes);
}
System.out.println("\nfile with random data created: " + filenamePlaintext);
// delete decrypted file if it exists
Files.deleteIfExists(new File(filenameDecryptedtext).toPath());
// setup random key & iv
SecureRandom secureRandom = new SecureRandom();
byte[] iv = new byte[16];
byte[] key = new byte[32]; // I'm using a 32 byte = 256 bit long key for aes 256
secureRandom.nextBytes(iv);
secureRandom.nextBytes(key);
// encrypt complete file
boolean resultEncryption = encryptCbcFileBufferedCipherOutputStream(filenamePlaintext, filenameCiphertext, key, iv);
System.out.println("encryption to " + filenameCiphertext + " was successfull: " + resultEncryption);
// encrypted file is 52428816 bytes long
System.out.println("\ndecryption in chunks of 1 mb");
// decryption in chunks of 1 mb
try (FileInputStream in = new FileInputStream(filenameCiphertext)) {
byte[] ibuf = new byte[(1024 * 1024)]; // chunks of 1 mb
int len;
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
SecretKeySpec secretKeySpec = new SecretKeySpec(key, "AES");
IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec);
while ((len = in.read(ibuf)) != -1) {
byte[] obuf = cipher.update(ibuf, 0, len);
if (obuf != null)
// obuf holds the decrypted chunk, do what you want to do with the data
// I'm writing it to a file in appending mode
try (FileOutputStream output = new FileOutputStream(filenameDecryptedtext, true)) {
output.write(obuf);
}
}
byte[] obuf = cipher.doFinal();
if (obuf != null)
// final data
try (FileOutputStream output = new FileOutputStream(filenameDecryptedtext, true)) {
output.write(obuf);
}
}
System.out.println("decrypted file written to " + filenameDecryptedtext);
System.out.println("plaintext equals decrytedtext file: " + filecompareSha256Large(filenamePlaintext, filenameDecryptedtext));
}
public static boolean encryptCbcFileBufferedCipherOutputStream(String inputFilename, String outputFilename, byte[] key, byte[] iv)
throws IOException, NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, InvalidAlgorithmParameterException {
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
try (FileInputStream in = new FileInputStream(inputFilename);
FileOutputStream out = new FileOutputStream(outputFilename);
CipherOutputStream encryptedOutputStream = new CipherOutputStream(out, cipher);) {
SecretKeySpec secretKeySpec = new SecretKeySpec(key, "AES");
IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec);
byte[] buffer = new byte[8096];
int nread;
while ((nread = in.read(buffer)) > 0) {
encryptedOutputStream.write(buffer, 0, nread);
}
encryptedOutputStream.flush();
}
if (new File(outputFilename).exists()) {
return true;
} else {
return false;
}
}
public static boolean filecompareSha256Large(String filename1, String filename2) throws IOException, NoSuchAlgorithmException {
boolean result = false;
byte[] hash1 = generateSha256Buffered(filename1);
byte[] hash2 = generateSha256Buffered(filename2);
result = Arrays.equals(hash1, hash2);
return result;
}
private static byte[] generateSha256Buffered(String filenameString) throws IOException, NoSuchAlgorithmException {
// even for large files
byte[] buffer = new byte[8192];
int count;
MessageDigest md = MessageDigest.getInstance("SHA-256");
BufferedInputStream bis = new BufferedInputStream(new FileInputStream(filenameString));
while ((count = bis.read(buffer)) > 0) {
md.update(buffer, 0, count);
}
bis.close();
return md.digest();
}
}
答案 2 :(得分:1)
是的,有可能。但是,由于模式和填充的原因,编程可能比乍一看要难。
但是,我创建了一个类,可以很高兴地从任何偏移量和任何大小解码。请注意,密文应不包含IV。
事后看来,我最好使用ByteBuffer
使其更加灵活,但是,是的,这需要整个重写...
package com.stackexchange.so;
import java.security.GeneralSecurityException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
/**
* A class that helps you to partially decrypt a CBC ciphertext. Although this class helps you to partially decrypt any
* part, you'd probably want to decrypt chunks that consists of a specific number of blocks; both the <code>off</code>
* and <code>len</code> parameter should be a modulus the block size. If you know the exact plaintext length then you
* can size the last chunk precisely.
*
* @author maartenb
*/
public class CBCDecryptByOffset {
private enum State {
UNINITIALIZED, INITIALIZED, RUNNING;
};
private final Cipher cbcCipher;
private SecretKey symKey;
private IvParameterSpec iv;
private State state = State.UNINITIALIZED;
/**
* Creates the CBC decryptor class and initializes it.
* @param blockCipher the block cipher, without block cipher mode or padding indication e.g. <code>"AES"</code>
* @throws NoSuchAlgorithmException if the block cipher is not available for <code>"CBC"</code>
* @throws NoSuchPaddingException if the block cipher in CBC mode is not available with <code>"NoPadding"</code>
*/
public CBCDecryptByOffset(String blockCipher) throws NoSuchAlgorithmException, NoSuchPaddingException {
this.cbcCipher = Cipher.getInstance(blockCipher + "/CBC/NoPadding");
}
/**
* Mimics {@link Cipher#init(int, java.security.Key, java.security.spec.AlgorithmParameterSpec)} except that it
* doesn't include options for encryption, wrapping or unwrapping.
*
* @param symKey the key to use
* @param iv the IV to use
* @throws InvalidKeyException if the key is not valid for the block cipher
* @throws InvalidAlgorithmParameterException if the IV is not valid for CBC, i.e. is not the block size
*/
public void init(SecretKey symKey, IvParameterSpec iv)
throws InvalidKeyException, InvalidAlgorithmParameterException {
this.symKey = symKey;
this.iv = iv;
// init directly, probably we want to start here, and it will perform a cursory check of the key and IV
this.cbcCipher.init(Cipher.DECRYPT_MODE, symKey, iv);
this.state = State.INITIALIZED;
}
/**
* Decrypts a partial number of bytes from a CBC encrypted ciphertext with PKCS#7 compatible padding.
*
* @param fullCT the full ciphertext
* @param off the offset within the full ciphertext to start decrypting
* @param len the amount of bytes to decrypt
* @return the plaintext of the partial decryption
* @throws BadPaddingException if the ciphertext is not correctly padded (only checked for the final CT block)
* @throws IllegalBlockSizeException if the ciphertext is empty or not a multiple of the block size
*/
public byte[] decryptFromOffset(byte[] fullCT, int off, int len)
throws BadPaddingException, IllegalBlockSizeException {
if (state == State.UNINITIALIZED) {
throw new IllegalStateException("Instance should be initialized before decryption");
}
int n = cbcCipher.getBlockSize();
if (fullCT.length == 0 || fullCT.length % n != 0) {
throw new IllegalBlockSizeException(
"Ciphertext must be a multiple of the blocksize, and should contain at least one block");
}
if (off < 0 || off > fullCT.length) {
throw new IllegalArgumentException("Invalid offset: " + off);
}
if (len < 0 || off + len < 0 || off + len > fullCT.length) {
throw new IllegalArgumentException("Invalid len");
}
if (len == 0) {
return new byte[0];
}
final int blockToDecryptFirst = off / n;
final int blockToDecryptLast = (off + len - 1) / n;
final int bytesToDecrypt = (blockToDecryptLast - blockToDecryptFirst + 1) * n;
final byte[] pt;
try {
// determine the IV to use
if (state != State.INITIALIZED || off != 0) {
IvParameterSpec vector;
final int blockWithVector = blockToDecryptFirst - 1;
if (blockWithVector == -1) {
vector = iv;
} else {
vector = new IvParameterSpec(fullCT, blockWithVector * n, n);
}
cbcCipher.init(Cipher.DECRYPT_MODE, symKey, vector);
}
// perform the actual decryption (note that offset and length are in bytes)
pt = cbcCipher.doFinal(fullCT, blockToDecryptFirst * n, bytesToDecrypt);
} catch (GeneralSecurityException e) {
throw new RuntimeException("Incorrectly programmed, error should never appear", e);
}
// we need to unpad if the last block is the final ciphertext block
int sigPadValue = 0;
final int finalCiphertextBlock = (fullCT.length - 1) / n;
if (blockToDecryptLast == finalCiphertextBlock) {
int curPaddingByte = bytesToDecrypt - 1;
int padValue = Byte.toUnsignedInt(pt[curPaddingByte]);
if (padValue == 0 || padValue > n) {
throw new BadPaddingException("Invalid padding");
}
for (int padOff = curPaddingByte - 1; padOff > curPaddingByte - padValue; padOff--) {
if (Byte.toUnsignedInt(pt[padOff]) != padValue) {
throw new BadPaddingException("Invalid padding");
}
}
// somebody tries to decrypt just padding bytes
if (off >= (blockToDecryptLast + 1) * n - padValue) {
sigPadValue = len;
} else {
// calculate if any (significant) padding bytes need to be ignored within the plaintext
int bytesInFinalBlock = (off + len - 1) % n + 1;
sigPadValue = padValue - (n - bytesInFinalBlock);
if (sigPadValue < 0) {
sigPadValue = 0;
}
}
}
int ptStart = off - blockToDecryptFirst * n;
int ptSize = len - sigPadValue;
state = State.RUNNING;
if (pt.length == ptSize) {
return pt;
}
return Arrays.copyOfRange(pt, ptStart, ptStart + ptSize);
}
}
请注意,我已经测试了常规功能,但是如果您是我,请确保将其与一些JUnit测试一起包装。