当前位置: 首页 > 知识库问答 >
问题:

以块为单位解密AES 128 CBC加密对象

龙欣德
2023-03-14

我在Minio中有一个加密对象,使用AES 128位CBC算法加密。

该对象非常大(约50 MB),因此我没有将其完全加载到内存中(这可能会导致内存不足异常),而是以1MB的块检索它。我需要在使用前解密它。

有没有可能用这种方式解密对象(一次1MB,整个对象一次性加密)?如果是,我该怎么做?我尝试解密16字节的块,产生以下错误:

javax.crypto.BadPaddingException:给定未正确填充的最终块

<code>javax.crypto。IllegalBlockSizeException:使用填充密码解密时,输入长度必须是16的倍数

共有3个答案

太叔灿
2023-03-14

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

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

邹普松
2023-03-14

是的,使用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 是关键

1 D3 fa 2 b 7c 9008 E1 cdbc 76 a1 f 22388 b 89是密文的前一个块

商同
2023-03-14

为了避免内存溢出错误,您需要将大型(加密)文件解密为1 mb大小的块-是的,在AES CBC模式下是可能的。

下面是一个完整的示例,它正在生成一个示例纯文本文件(“纯文本.dat”),其中包含大小为 50 mb 1 字节的随机内容(1 字节适合测试大小不是 16 = AES 块大小的精确倍数的文件大小)。

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

最后一步是请求的解密方法——它以1 mb的块对加密文件进行解密,在“// obuf”行中保存解密的块,对数据执行您想要的操作,在字节数组obuf中确实有解密的数据。为了测试,我在附加模式下将解密的数据写入文件“decryptedtext.dat ”(由于这个原因,如果这个文件存在的话,它在一开始就被删除了)。

为了证明解密成功,我比较了sha 256——明文和解密后的文本文件的散列。

两点说明:我正在对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();
    }
}
 类似资料:
  • 因此,这种特殊的异常非常常见,但我的问题与通常被问到的略有不同。 我有一个AES解密和加密函数,定义如下: 现在,如果我像这样执行单个解密: 字节数组输出很好。而如果我执行双重加密/解密: 我得到了著名的<code>javax.crypto。BadPaddingException:给定的最终块未正确填充异常。请注意,和只是整数(假设它们都是0)。目前,IVBytes只是一个大小为16的空字节数组,

  • 问题内容: 我想使用公共密钥加密技术在JavaScript中加密,在PHP中解密。我一直在尝试找到可以完成此任务的库,但是遇到了问题。 我目前正在使用 openpgpjs ,但是我需要所有浏览器的支持,甚至测试页在唯一列为受支持的浏览器(Google Chrome)上都有错误。 关于最终目标的注意事项: TCP连接已受SSL保护。 此保护层的主要目的是防御有意或无意的Web服务器日志记录,崩溃转储

  • 我不是openssl的专家。我把下面的代码放在一起,使用AES-CTR加密和解密消息。输出不是我期望看到的。 我得到的结果是这样的:“简单:、u∩U└■我的 知道是什么导致的吗?我想做的就是使用AES使用CTR来加密和解密消息。我想得到与纯文本相同的加密长度(或1字节)。我用DES做过这个,但是DES不安全。然后,我将使用AES-CTR加密和解密我的流量(流)。

  • 本文向大家介绍python实现移位加密和解密,包括了python实现移位加密和解密的使用技巧和注意事项,需要的朋友参考一下 本文实例为大家分享了python实现移位加密和解密的具体代码,供大家参考,具体内容如下 代码很简单,就不多做解释啦。主要思路是将字符串转为Ascii码,将大小写字母分别移位密钥表示的位数,然后转回字符串。需要注意的是,当秘钥大于26的时候,我使用循环将其不断减去26,直到密钥

  • 这是可能的还是加密必须共享和使用相同的密钥? 主要目的就是这样。 我将有两个客户端可以发送和接收加密数据到彼此。

  • 1.我有java函数,它加密xml文件并返回加密的字符串。 2.我有c#函数,它可以解密由java函数加密的消息。 Java加密功能运行良好。但问题是C函数, 当我解密时,会得到下面的错误消息 我使用下面的参考搜索解决方案 Java中的AES加密和C#中的解密 C#/Java|AES256加密/解密 C#和Java中的加密/解密 但我仍然面临同样的错误。谁能给我提个建议吗。 我只是改变我的C#加密