【问题标题】:Decrypt in chunks a AES 128 CBC encrypted object分块解密 AES 128 CBC 加密对象
【发布时间】:2020-11-29 04:36:41
【问题描述】:

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

【问题讨论】:

  • 同意@nobody 评论,但是,您可能有一些解码问题...

标签: aes java encryption


【解决方案1】:

为了避免“内存不足错误”,您希望以 1 mb 大小的块解密一个大(加密)文件 - 是的,可以使用 AES CBC 模式。

您可以在下面找到一个完整的示例,该示例生成一个示例纯文本文件 ('plaintext.dat'),其中包含大小为 50 mb + 1 字节的随机内容(+ 1 字节可以很好地测试文件大小不是16 的精确倍数 = AES 块大小)。

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

最后一步是请求的解密方法 - 它以 1 mb 的块解密加密文件,并在“// obuf 保存解密的块,对数据做你想做的事情”和“// 最终data' 你确实在字节数组 obuf 中有解密的数据。为了测试,我以附加模式将解密的数据写入文件“decryptedtext.dat”(因此,如果该文件存在,则在开头删除该文件)。

为了证明解密成功,我比较了明文和解密文本文件的 SHA256 哈希。

两个注意事项:我为 AES CBC 256 使用 32 字节 = 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();
    }
}

【讨论】:

  • 我已经解决了这个问题,这个解决方案提到了 CipherOutputStream 的用法,在我的案例中它为我解决了这个问题。
【解决方案2】:

是的,使用 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是前一个密文块

【讨论】:

    【解决方案3】:

    是的,这是可能的。但是,由于模式和填充的原因,编程可能比乍一看要复杂。

    但是,我创建了一个可以愉快地从任何偏移量和任何大小解码的类。请注意,密文应该包含 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 测试对其进行包装。

    【讨论】:

    • 是的,不要太统一地使用final,我知道...
    • 就我而言,IV 参数是必需的。
    猜你喜欢
    • 2013-08-11
    • 2019-08-17
    • 2020-04-15
    • 2018-04-04
    • 2021-06-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多