Wojtek Wencel
Wojtek Wencel

Reputation: 2117

AES padding prevents sending of the final block

I'm writing an app for sending and receiving AES encrypted data. I'm using a CryptoStream which writes to/reads from a NetworkStream. Initially I tried using the built-in padding like this:

// sending
using (Aes aesAlg = Aes.Create())
{
    aesAlg.Padding = PaddingMode.PKCS7; // built-in padding
    // set other AES params

    using (MemoryStream ms = new MemoryStream())
    using (ICryptoTransform encryptor = aesAlg.CreateEncryptor(aesAlg.Key, aesAlg.IV))
    using (CryptoStream csEncrypt = new CryptoStream(networkStream, encryptor, CryptoStreamMode.Write, true))
    {
        BinaryFormatter bf = new BinaryFormatter();
        bf.Serialize(ms, data);
        byte[] bytes = ms.ToArray();

        await csEncrypt.WriteAsync(bytes, 0, bytes.Length);

        csEncrypt.FlushFinalBlock(); // this should add padding and send all remaining data
    }
}

// receiving
using (Aes aesAlg = Aes.Create())
{
    aesAlg.Padding = PaddingMode.PKCS7; // built-in padding
    // set other AES params

    using (ICryptoTransform decryptor = aesAlg.CreateDecryptor(aesAlg.Key, aesAlg.IV))
    using (CryptoStream csDecrypt = new CryptoStream(networkStream, decryptor, CryptoStreamMode.Read, true))
    using (MemoryStream ms = new MemoryStream())
    {
        int totalBytesRead = 0;

        while (totalBytesRead < messageLength) // read content until length
        {
            var toRead = Math.Min(buffer.Length, messageLength - totalBytesRead);
            var nowRead = await csDecrypt.ReadAsync(buffer, 0, toRead ); // read from network there
            totalBytesRead += nowRead;
            await ms.WriteAsync(buffer, 0, nowRead);
        }
        ms.Position = 0;
        received = (Data)formatter.Deserialize(ms); // deserialise the object
    }
}

Note that the last argument new CryptoStream() is set to true - the base stream (networkStream) won't be closed when CryptoStream gets disposed. One interesting thing is that if I don't set this to true then receiving start working correctly. But because I need the networkStream to stay open it has to be set to true.

With the above implementation the receiving stream never receives all the data - it blocks itself on the last csDecrypt.ReadAsync(). Based on my understanding the csEncrypt.FlushFinalBlock() should send the last block with the added padding, but for some reason it doesn't happen.

Because this didn't work I added the padding myself like this:

// sending
using (Aes aesAlg = Aes.Create())
{
    aesAlg.Padding = PaddingMode.None; // no built-in padding
    // set other AES params

    using (MemoryStream ms = new MemoryStream())
    using (ICryptoTransform encryptor = aesAlg.CreateEncryptor(aesAlg.Key, aesAlg.IV))
    using (CryptoStream csEncrypt = new CryptoStream(networkStream, encryptor, CryptoStreamMode.Write, true))
    {
        BinaryFormatter bf = new BinaryFormatter();
        bf.Serialize(ms, data);
        byte[] bytes = ms.ToArray();

        await csEncrypt.WriteAsync(bytes, 0, bytes.Length);

        // manually add padding (so the total number of bytes is divisible by 16 bytes/128 bits)
        if (bytes.Length % 16 != 0)
            await csEncrypt.WriteAsync(bytes, 0, 16 - (bytes.Length % 16)); // paddingdata
    }
}

// receiving
using (Aes aesAlg = Aes.Create())
{
    aesAlg.Padding = PaddingMode.None; // no built-in padding

    using (ICryptoTransform decryptor = aesAlg.CreateDecryptor(aesAlg.Key, aesAlg.IV))
    using (CryptoStream csDecrypt = new CryptoStream(networkStream, decryptor, CryptoStreamMode.Read, true))
    using (MemoryStream ms = new MemoryStream())
    {
        int totalBytesRead = 0;

        while (totalBytesRead < messageLength) // read content until length
        {
            var toRead = Math.Min(buffer.Length, messageLength - totalBytesRead);

            // if it's the last buffer to be sent and the number of bytes is not divisible by 16 then add padding
            if (messageLength - totalBytesRead <= 1024 && toRead % 16 != 0)
                min += 16 - (toRead % 16);

            var nowRead = await csDecrypt.ReadAsync(buffer, 0, toRead); // read from network there
            totalBytesRead += nowRead;
            await ms.WriteAsync(buffer, 0, nowRead);
        }
        ms.Position = 0;
        received = (Data)formatter.Deserialize(ms); // deserialise the object
    }
}

If I add the padding myself then everything works correctly and the receiving function doesn't block itself on the last buffer. What should I do to make the built-in padding work?

Upvotes: 1

Views: 76

Answers (1)

Maarten Bodewes
Maarten Bodewes

Reputation: 94038

The problem is on the receiving side. If you don't close the stream then the decryption routine doesn't know that the last block has been received, so it just waits for the next block, which never comes. What you are doing now should work when implemented correctly. But you only have to perform the unpadding at the end - your current implementation doesn't implement PKCS#7 unpadding though.

There is another, possibly cleaner way: read from a stream that does close. Apparently you know the size of the plaintext or ciphertext. So what you can do is to use a stream that wraps the other stream, but that does close after reading all the bytes of the ciphertext - and, of course, doesn't close the underlying stream when that happens. It's a bit of work, but it should not be that hard as you only have to implement the Read method (see the Stream class documentation to see why).

I think the second option is the cleanest, but you could of course just read the necessary bytes and then use the decryptor directly to decrypt the bytes (buffer-by-buffer). Not the nicest one compared to the bounded stream trick, but probably the easiest to get your head around.

So there you are: three options: pick & choose and ... well, implement obviously.

Upvotes: 1

Related Questions