解密AES 128 CBC加密的对象时,分块操作。

huangapple go评论77阅读模式
英文:

Decrypt in chunks a AES 128 CBC encrypted object

问题

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

这个对象相当大(约50MB),所以我不是将它完全加载到内存中(这可能会导致内存不足异常),而是以1MB的块检索它。在使用之前,我需要解密它。

是否可以以这种方式解密对象(每次1MB,整个对象是一次性加密的)?
如果可以,我该如何做?
我尝试过解密16字节的块,但会产生以下错误:

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

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

英文:

I have an Encrypted object in Minio, encrypted using the AES 128 bit CBC algorithm.

The object is quite large (~50 MB) so instead of loading it into the memory completely (which may cause out of memory exception), I am retrieving it in chunks of 1MB. I need to decrypt it before use.

Is it possible to decrypt the object in this way (1MB at a time, the whole object was encrypted in one go)?
If yes, how can I do it?
I have tried decrypting 16-byte chunks which produce the following errors:

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

得分: 2

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

下面是一个完整的示例,它生成一个带有随机内容的示例明文文件('plaintext.dat'),文件大小为50 MB + 1字节(+1字节用于测试文件大小不是16的精确倍数 = AES块大小)。

接下来,此文件将使用随机创建的初始化向量和密钥加密为'ciphertext.dat'。

最后一步是所请求的解密方法 - 它将以1 MB的块解密加密文件,并在行'// obuf holds the decrypted chunk, do what you want to do with the data'和'// 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");

        // 设置用于创建一个50MB加密文件的参数
        int filesize = (50 * 1024 * 1024) + 1; // 50 MB + 1字节 = 52428801字节
        String filenamePlaintext = "plaintext.dat";
        String filenameCiphertext = "ciphertext.dat";
        String filenameDecryptedtext = "decryptedtext.dat";

        File file = new File("plaintext.dat");
        // 填充随机字节。
        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);
        // 如果存在,删除解密后的文件
        Files.deleteIfExists(new File(filenameDecryptedtext).toPath());

        // 设置随机密钥和初始化向量
        SecureRandom secureRandom = new SecureRandom();
        byte[] iv = new byte[16];
        byte[] key = new byte[32]; // 我使用32字节= 256位长的aes 256密钥
        secureRandom.nextBytes(iv);
        secureRandom.nextBytes(key);

        // 加密完整文件
        boolean resultEncryption = encryptCbcFileBufferedCipherOutputStream(filenamePlaintext, filenameCiphertext, key, iv);
        System.out.println("encryption to " + filenameCiphertext + " was successfull: " + resultEncryption);
        // 加密后的文件长度为52428816字节

        System.out.println("\ndecryption in chunks of 1 mb");
        // 以1 MB的块解密
        try (FileInputStream in = new FileInputStream(filenameCiphertext)) {
            byte[] ibuf = new byte[(1024 * 1024)]; // 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保存解密的块,根据需要对数据进行操作
                    // 我将其以追加模式写入文件
                    try (FileOutputStream output = new FileOutputStream(filenameDecryptedtext, true)) {
                        output.write(obuf);
                    }
            }
            byte[] obuf = cipher.doFinal();
            if (obuf != null)
                // 最终数据
                try (FileOutputStream output = new FileOutputStream(filenameDecryptedtext, true)) {
                    output.write(obuf);
                }
        }
        System.out.println("decrypted file written to " + filenameDecryptedtext);
        System.out.println("plaintext equals decryptedtext 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 {
       

<details>
<summary>英文:</summary>

To avoid an &quot;out of memory error&quot; you want to decrypt a large (encrypted) file in chunks of 1 mb size - **yes it&#39;s possible** with AES CBC mode.

Below you find a complete example that is generating a sample plaintext file (&#39;plaintext.dat&#39;) with random content with the size of 50 mb + 1 byte (the + 1 byte is good to test for file sizes that are not exact multiples of 16 = AES blocksize).

In the next step this file is getting encrypted to &#39;ciphertext.dat&#39; using a randomly created initialization vector and key.

The last step is the requested decryption method - it decrypts the encrypted file in chunks of 1 mb and in the lines &#39;// obuf holds the decrypted chunk, do what you want to do with the data&#39; and &#39;// final data&#39; you do have the decrypted data in the byte array obuf. For testing I&#39;m writing the decrypted data to the file &#39;decryptedtext.dat&#39; in appending mode (for that reason this file is deleted in the beginning if it exists).

To prove that decryption was successful I&#39;m comparing the SHA256-hashes of plaintext- and decryptedtext-files.

Two notes: I&#39;m using a 32 byte = 256 bit long key for AES CBC 256. **This program has no proper exception handling and is for educational purposes only.**

result:

    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

code:

    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(&quot;https://stackoverflow.com/questions/63325528/decrypt-in-chunks-a-aes-128-cbc-encrypted-object/63325529#63325529&quot;);
            System.out.println(&quot;decrypt AES CBC 256 in 1 mb chunks&quot;);
    
            // setup for creation of a 50mb encrypted file
            int filesize = (50 * 1024 * 1024) + 1; // 50 mb + 1 byte = 52428801 bytes
            String filenamePlaintext = &quot;plaintext.dat&quot;;
            String filenameCiphertext = &quot;ciphertext.dat&quot;;
            String filenameDecryptedtext = &quot;decryptedtext.dat&quot;;
    
            File file = new File(&quot;plaintext.dat&quot;);
            // 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(&quot;\nfile with random data created: &quot; + filenamePlaintext);
            // delete decrypted file if it exists
            Files.deleteIfExists(new File(filenameDecryptedtext).toPath());
    
            // setup random key &amp; iv
            SecureRandom secureRandom = new SecureRandom();
            byte[] iv = new byte[16];
            byte[] key = new byte[32]; // I&#39;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(&quot;encryption to &quot; + filenameCiphertext + &quot; was successfull: &quot; + resultEncryption);
            // encrypted file is 52428816 bytes long
    
            System.out.println(&quot;\ndecryption in chunks of 1 mb&quot;);
            // 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(&quot;AES/CBC/PKCS5PADDING&quot;);
                SecretKeySpec secretKeySpec = new SecretKeySpec(key, &quot;AES&quot;);
                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&#39;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(&quot;decrypted file written to &quot; + filenameDecryptedtext);
            System.out.println(&quot;plaintext equals decrytedtext file: &quot; + 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(&quot;AES/CBC/PKCS5Padding&quot;);
            try (FileInputStream in = new FileInputStream(inputFilename);
                 FileOutputStream out = new FileOutputStream(outputFilename);
                 CipherOutputStream encryptedOutputStream = new CipherOutputStream(out, cipher);) {
                SecretKeySpec secretKeySpec = new SecretKeySpec(key, &quot;AES&quot;);
                IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
                cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec);
                byte[] buffer = new byte[8096];
                int nread;
                while ((nread = in.read(buffer)) &gt; 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(&quot;SHA-256&quot;);
            BufferedInputStream bis = new BufferedInputStream(new FileInputStream(filenameString));
            while ((count = bis.read(buffer)) &gt; 0) {
                md.update(buffer, 0, count);
            }
            bis.close();
            return md.digest();
        }
    }



</details>



# 答案2
**得分**: 1

是的使用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`是前一个密文块。
<details>
<summary>英文:</summary>
Yes, with AES-128-CBC, it is possible to decrypt just a single block of cyphertext.  Each block is 128 bits (16 bytes).
See the diagram at https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Cipher_block_chaining_(CBC).  As you can see, to decrypt any block of ciphertext, you AES-decrypt the block of ciphertext, then XOR the plaintext with the previous block of ciphertext. (For the first block, the plaintext is XOR&#39;d with the IV).
The library that you are using is probably throwing these exceptions, because it is checking if the decrypted ciphertext is properly padded.  Of course, if you are decrypting just one arbitrary block of ciphertext, it will not have the proper padding.  However, you can use a tool like `openssl` to decrypt a single block of ciphertext, given the ciphertext, the key, and the previous block of ciphertext, like so:
echo -n &#39;bc6d8afc78e805b7ed7551e42da4d877&#39; | xxd -p -r |  openssl aes-128-cbc -d -nopad -K e3e33d2d9591b462c55503f7ec697839 -iv 1d3fa2b7c9008e1cdbc76a1f22388b89
where:
bc6d8afc78e805b7ed7551e42da4d877 is the block of ciphertext that you want to decrypt
e3e33d2d9591b462c55503f7ec697839 is the key
1d3fa2b7c9008e1cdbc76a1f22388b89 is the previous block of ciphertext
</details>
# 答案3
**得分**: 1
是的,这是可能的。但是,由于模式和填充,编程起来可能比乍一看更复杂。
不过,我已经创建了一个类,它可以从任何偏移和任何大小愉快地解密。请注意,密文**不应**包含IV。
回过头来看,我可能应该使用`ByteBuffer`使它更灵活一些,但是,是的,那将需要完全重写...
```java
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;
/**
* 一个帮助您部分解密CBC密文的类。虽然这个类可以帮助您部分解密任何部分,但您可能希望解密由特定数量块组成的块;both the &lt;code&gt;off&lt;/code&gt;和&lt;code&gt;len&lt;/code&gt;参数应该是块大小的模数。如果您知道确切的明文长度,那么您可以精确地设置最后一个块的大小。
*
* @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;
/**
* 创建CBC解密器类并进行初始化。
* @param blockCipher 块密码,不包含块密码模式或填充指示,例如&lt;code&gt;&quot;AES&quot;&lt;/code&gt;
* @throws NoSuchAlgorithmException 如果块密码在&lt;code&gt;&quot;CBC&quot;&lt;/code&gt;中不可用
* @throws NoSuchPaddingException 如果块密码在CBC模式中不可用&lt;code&gt;&quot;NoPadding&quot;&lt;/code&gt; 
*/
public CBCDecryptByOffset(String blockCipher) throws NoSuchAlgorithmException, NoSuchPaddingException {
this.cbcCipher = Cipher.getInstance(blockCipher + &quot;/CBC/NoPadding&quot;);
}
/**
* 模仿{@link Cipher#init(int, java.security.Key, java.security.spec.AlgorithmParameterSpec)},除了它不包括加密、包装或解包选项。
* 
* @param symKey 要使用的密钥
* @param iv     要使用的IV
* @throws InvalidKeyException                如果密钥对于块密码无效
* @throws InvalidAlgorithmParameterException 如果IV对于CBC无效,即不是块大小
*/
public void init(SecretKey symKey, IvParameterSpec iv)
throws InvalidKeyException, InvalidAlgorithmParameterException {
this.symKey = symKey;
this.iv = iv;
// 直接初始化,可能我们想从这里开始,它会执行密钥和IV的初步检查
this.cbcCipher.init(Cipher.DECRYPT_MODE, symKey, iv);
this.state = State.INITIALIZED;
}
/**
* 使用PKCS#7兼容填充从CBC加密的密文中解密部分字节。
* 
* @param fullCT 完整的密文
* @param off    在完整密文中开始解密的偏移量
* @param len    要解密的字节数
* @return 部分解密的明文
* @throws BadPaddingException       如果密文未正确填充(仅针对最终CT块进行检查)
* @throws IllegalBlockSizeException 如果密文为空或不是块大小的倍数
*/
public byte[] decryptFromOffset(byte[] fullCT, int off, int len)
throws BadPaddingException, IllegalBlockSizeException {
if (state == State.UNINITIALIZED) {
throw new IllegalStateException(&quot;必须在解密之前初始化实例&quot;);
}
int n = cbcCipher.getBlockSize();
if (fullCT.length == 0 || fullCT.length % n != 0) {
throw new IllegalBlockSizeException(
&quot;密文必须是块大小的倍数,并且应该至少包含一个块&quot;);
}
if (off &lt; 0 || off &gt; fullCT.length) {
throw new IllegalArgumentException(&quot;无效的偏移量:&quot; + off);
}
if (len &lt; 0 || off + len &lt; 0 || off + len &gt; fullCT.length) {
throw new IllegalArgumentException(&quot;无效的len&quot;);
}
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 {
// 确定要使用的IV
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);
}
// 执行实际解密(请注意,偏移量和长度以字节为单位)
pt = cbcCipher.doFinal(fullCT, blockToDecryptFirst * n, bytesToDecrypt);
} catch (GeneralSecurityException e) {
throw new RuntimeException(&quot;编程错误,错误不应该出现&quot;, e);
}
// 如果最后一个块是最终密文块,则需要取消填充
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 ||
<details>
<summary>英文:</summary>
Yes, it is possible. However, due to the mode and padding it may be trickier to program than it looks at first sight.
However, I&#39;ve created a class that will happily decode from any offset and to any size. Note that the ciphertext should **not** contain the IV.
In hindsight I might better have used `ByteBuffer` to make it a bit more flexible, but yeah, that will require an entire rewrite...
```java
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&#39;d probably want to decrypt chunks that consists of a specific number of blocks; both the &lt;code&gt;off&lt;/code&gt;
* and &lt;code&gt;len&lt;/code&gt; 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. &lt;code&gt;&quot;AES&quot;&lt;/code&gt;
* @throws NoSuchAlgorithmException if the block cipher is not available for &lt;code&gt;&quot;CBC&quot;&lt;/code&gt;
* @throws NoSuchPaddingException if the block cipher in CBC mode is not available with &lt;code&gt;&quot;NoPadding&quot;&lt;/code&gt; 
*/
public CBCDecryptByOffset(String blockCipher) throws NoSuchAlgorithmException, NoSuchPaddingException {
this.cbcCipher = Cipher.getInstance(blockCipher + &quot;/CBC/NoPadding&quot;);
}
/**
* Mimics {@link Cipher#init(int, java.security.Key, java.security.spec.AlgorithmParameterSpec)} except that it
* doesn&#39;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(&quot;Instance should be initialized before decryption&quot;);
}
int n = cbcCipher.getBlockSize();
if (fullCT.length == 0 || fullCT.length % n != 0) {
throw new IllegalBlockSizeException(
&quot;Ciphertext must be a multiple of the blocksize, and should contain at least one block&quot;);
}
if (off &lt; 0 || off &gt; fullCT.length) {
throw new IllegalArgumentException(&quot;Invalid offset: &quot; + off);
}
if (len &lt; 0 || off + len &lt; 0 || off + len &gt; fullCT.length) {
throw new IllegalArgumentException(&quot;Invalid len&quot;);
}
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(&quot;Incorrectly programmed, error should never appear&quot;, 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 &gt; n) {
throw new BadPaddingException(&quot;Invalid padding&quot;);
}
for (int padOff = curPaddingByte - 1; padOff &gt; curPaddingByte - padValue; padOff--) {
if (Byte.toUnsignedInt(pt[padOff]) != padValue) {
throw new BadPaddingException(&quot;Invalid padding&quot;);
}
}
// somebody tries to decrypt just padding bytes
if (off &gt;= (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 &lt; 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);
}
}

Note that I've tested the general functionality but I'd make sure that I wrap it with some JUnit tests if I were you.

huangapple
  • 本文由 发表于 2020年8月8日 18:24:35
  • 转载请务必保留本文链接:https://go.coder-hub.com/63325528.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定