对AES 128 CBC加密的对象进行块解密



我在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

为了避免;内存不足错误";如果您想解密大小为1 mb的大块(加密的(文件,是的,使用AES CBC模式是可能的。

下面是一个完整的示例,它生成了一个随机内容的示例明文文件("laintext.dat"(,大小为50 mb+1字节(+1字节适合测试不是16=AES块大小的整数倍的文件大小(。

在下一步中,使用随机创建的初始化向量和密钥将该文件加密为"密文.dat"。

最后一步是请求的解密方法-它将加密文件解密为1 mb的块,在"//obuf保存解密的块,对数据执行您想做的操作"one_answers"//final data"行中,您确实在字节数组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();
}
}

是的,使用AES-128-CBC,可以只解密单个密文块。每个块是128位(16字节(。

请参阅上的图表https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Cipher_block_chaining_(CBC(。正如你所看到的,要解密任何密文块,你需要对密文块进行AES解密,然后将明文与前一个密文块异或。(对于第一个块,明文与IV进行异或运算(。

您正在使用的库可能会抛出这些异常,因为它正在检查解密的密文是否正确填充。当然,如果你只解密一个任意的密文块,它将没有适当的填充。然而,您可以使用像openssl这样的工具来解密单个密文块,给定密文、密钥和前一个密文块,如下所示:

echo -n 'bc6d8afc78e805b7ed7551e42da4d877' | xxd -p -r |  openssl aes-128-cbc -d -nopad -K e3e33d2d9591b462c55503f7ec697839 -iv 1d3fa2b7c9008e1cdbc76a1f22388b89

其中:

bc6d8afc78e805b7ed7551e42da4d877是要解密的密文块

e3e33d2d9591b462c55503f7ec697839是关键

1d3fa2b7c9008e1cdbc76a1f22388b89是密文的前一块

是的,这是可能的。然而,由于模式和填充,它的编程可能比乍一看更难。

然而,我已经创建了一个类,它可以很高兴地从任何偏移量解码到任何大小。请注意,密文应该而不是包含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测试来包装它。

最新更新