Search code examples
c#encryptionaes

How to chain-encrypt using AES CBC mode without writing to file


I need to encrypt a large contiguous amount of bytes, not fitting into single byte array.

There are already a few similar questions, but none of the answers works for me:

stackoverflow.com/questions/5090233/how-to-encrpyt-decrypt-data-in-chunks stackoverflow.com/questions/45735983/encrypting-files-with-aes-c-sharp-in-chunks stackoverflow.com/questions/27645527/aes-encryption-on-large-files

Following code works if the output length is under limit (Key and IV are hardcoded for testing):

byte[] encrypt(byte[] input, byte[] iv = null)
{
    var aes = new AesManaged();
    aes.KeySize = 128;
    aes.Key = new byte[16] { 0x0F, 0xD4, 0x33, 0x82, 0xF4, 0xDF,
        0x62, 0xA5, 0x55, 0x7C, 0x6E, 0x92, 0xC5, 0x64, 0x67, 0xA9 };
    aes.IV = (iv != null) ? iv :
        new byte[16] { 0xB3, 0x87, 0xDA, 0xA0, 0x47, 0x7C,
        0x52, 0x76, 0xCB, 0x3A, 0x69, 0x9B, 0x0F, 0x82, 0xAF, 0xA7 };

    using (var stream = new MemoryStream())   // size limit is 2 GB
    using (var cryptoStream = new CryptoStream(stream,
            aes.CreateEncryptor(), CryptoStreamMode.Write))
    {
        cryptoStream.Write(input, 0, input.Length);
        cryptoStream.FlushFinalBlock();
        return stream.ToArray();    // max number of bytes = int.MaxValue
    }
}

One alternative is to use FileStream instead of MemoryStream, but I prefer to get the results in memory.

I am trying to implement the chain operation, dividing the input on chunks of [multiples of AES BlockSize] and capture the encrypted output after feeding each chunk.

According to what I read, the last bytes of each result are supposed to be used as an IV for the next encryption, but that does not work. For simplicity, chunk size is equal to BlockSize:

byte[] subArray(byte[] source, int start, int length = -1)
{
    if (length == -1) length = source.Length - start;
    var target = new byte[length];
    Buffer.BlockCopy(source, start, target, 0, length);
    return target;
}
var encr0 = encrypt(subArray(input, 0,         blockSize * 2));
var encr1 = encrypt(subArray(input, 0,         blockSize));
var encr2 = encrypt(subArray(input, blockSize, blockSize),
                    subArray(encr1, blockSize, 16));
Assert.IsTrue(encr2.SequenceEqual(subArray(encr0, blockSize)));  // FAILS

What I am doing wrong?


Solution

  • The encrypt() method uses the CBC mode by default. In this mode, the ciphertext of a block serves as IV of the following block (a random IV is applied for the first block).
    In the posted code, decryption fails because an incorrect IV is used for encr2. The correct IV is subArray(encr1, 0, blockSize) (namely the ciphertext of the first block) and not subArray(encr1, blockSize, 16).
    When this is fixed, the exception no longer occurs.

    Another issue is that encrypt() implicitly performs PKCS#7 padding (the default). PKCS#7 padding pads even if the last plaintext block is completely filled. In this case, a full padding block (consisting of 16 0x10 values for AES) is appended.
    The padding in encrypt() causes plaintext blocks that are not at the end to be incorrectly padded with a full padding block as well. This results in a wrong ciphertext block, as can be easily checked when encr0, encr1 and encr2 (e.g. hex encoded) are output.

    To avoid these wrong padding blocks, the padding in encrypt() must be disabled for all plaintext blocks except the last one which is regulary padded (if it is incomplete it is filled, if it is complete, a full padding block is appended).


    Edit: For arbitrary chunksizes (of the size of a multiple of the blocksize) subArray(encr1, chunkSize - blockSize, blockSize) must be applied as IV for encr1. If the chunksize is equal to the blocksize (as assumed here) this simplifies to subArray(encr1, 0, blockSize).