逐块解密AES 128 CBC加密对象

时间:2020-08-08 10:24:35

标签: aes java encryption

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

3 个答案:

答案 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测试一起包装。