Brian Vallelunga
Brian Vallelunga

Reputation: 10201

.NET to web crypto interoperability padding issue

I'm attempting to encrypt some data in .NET (using C#) and then decrypt it in the browser using the web-crypto APIs (and vise-versa). For some reason the encrypted output of each side differs slightly. The browser output has an additional 16 bytes appended to the end of the data and I can't find what's causing it.

The .NET code that does the encryption is:

public static byte[] Encrypt(byte[] data)
{
    using (var aes = new AesManaged())
    {
        aes.Mode = CipherMode.CBC;
        aes.Padding = PaddingMode.PKCS7;
        var key = new byte[] { 168, 126, 39, 25, 51, 65, 246, 41, 228, 56, 66, 237, 5, 8, 211, 102, 250, 16, 99, 12, 204, 14, 162, 126, 166, 140, 15, 124, 194, 186, 141, 111 };
        var iv = new byte[] { 188, 227, 223, 253, 171, 64, 82, 150, 10, 130, 159, 79, 68, 134, 192, 50 };

        using (var encryptor = aes.CreateEncryptor(key, iv))
        using (var msEncrypt = new MemoryStream())
        using (var csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write))
        {
            csEncrypt.Write(data, 0, data.Length);
            return msEncrypt.ToArray();
        }
    }
}

The JavaScript code is as follows (assume missing variables are correct):

function encrypt(data, key) {
    window.crypto.subtle.encrypt(
        {
            name: "AES-CBC",
            iv: Uint8Array.from([188, 227, 223, 253, 171, 64, 82, 150, 10, 130, 159, 79, 68, 134, 192, 50]);
        },
        key,
        data 
    )
    .then(function(d){
        var encryptedBytes = new Uint8Array(d);
        // encryptedBytes had an additional 16 bytes added to the end of it for the same input.
        console.log(encryptedBytes);
    });
}

The encrypted bytes match the ones from the C# version, but will have an extra 16 bytes added onto the end. Is this a padding issue? From the docs I can find, it seems PKCS7 is the correct padding mode to use.

I thought the browser version might be appending the initialization vector to the end of the data, but I checked and it didn't match up.

Trying to decrypt the data encrypted by the C# code just errors with odd exceptions in the browser.

Upvotes: 1

Views: 549

Answers (2)

Brian Vallelunga
Brian Vallelunga

Reputation: 10201

MarkovskI's answer, while not the final solution, helped me discover the root cause. The issue ended up being that the CryptoStream wasn't finished writing the data to the MemoryStream due to how my using statements were arranged.

There are two possible solutions.

1) Call FlushFinalBlock() before returning the MemoryStream's array.

csEncrypt.Write(data, 0, data.Length);
csEncrypt.FlushFinalBlock();
return msEncrypt.ToArray();

Just calling Flush() (which I had tried) did not work. This makes sense since it needs to be sure all data has been written before calculating and writing the padding.

2) Nest an inner using statement to be sure the CryptoStream has been disposed of before calling reading the MemoryStream. This interally must call FlushFinalBlock() in the Dispose method:

using (var aes = new AesManaged())
{
    aes.Mode = CipherMode.CBC;
    aes.Padding = PaddingMode.PKCS7;
    var key = new byte[] { 168, 126, 39, 25, 51, 65, 246, 41, 228, 56, 66, 237, 5, 8, 211, 102, 250, 16, 99, 12, 204, 14, 162, 126, 166, 140, 15, 124, 194, 186, 141, 111 };
    var iv = new byte[] { 188, 227, 223, 253, 171, 64, 82, 150, 10, 130, 159, 79, 68, 134, 192, 50 };

    using (var encryptor = aes.CreateEncryptor(key, iv))
    using (var msEncrypt = new MemoryStream())
    {
        using (var csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write))
        {
            csEncrypt.Write(data, 0, data.Length);
        }

        return msEncrypt.ToArray();
    }
}

Upvotes: 2

MarkovskI
MarkovskI

Reputation: 1587

AES requires the length input to be multiples of 16, so you must apply padding, this is where PKCS#7 comes in and the extra data comes from, which is a problem in the .Net implementation. If you want to remove the padding you need to remove it from the final block, you can do that using the following:

public class NoPaddingTransformWrapper : ICryptoTransform
{
    private ICryptoTransform _mTransform;
    public NoPaddingTransformWrapper(ICryptoTransform symmetricAlgoTransform)
    {
        _mTransform = symmetricAlgoTransform ?? throw new ArgumentNullException(nameof(symmetricAlgoTransform));
    }

    public bool CanReuseTransform => _mTransform.CanReuseTransform;

    public bool CanTransformMultipleBlocks => _mTransform.CanTransformMultipleBlocks;

    public int InputBlockSize => _mTransform.InputBlockSize;

    public int OutputBlockSize => _mTransform.OutputBlockSize;

    public int TransformBlock(byte[] inputBuffer, int inputOffset, int inputCount, byte[] outputBuffer, int outputOffset)
    {
        return _mTransform.TransformBlock(inputBuffer, inputOffset, inputCount, outputBuffer, outputOffset);
    }

    public byte[] TransformFinalBlock(byte[] inputBuffer, int inputOffset, int inputCount)
    {
        if (inputCount % _mTransform.InputBlockSize == 0)
        {
            return _mTransform.TransformFinalBlock(inputBuffer, inputOffset, inputCount);
        }
        byte[] lastBlocks = new byte[inputCount / _mTransform.InputBlockSize + _mTransform.InputBlockSize];
        Buffer.BlockCopy(inputBuffer, inputOffset, lastBlocks, 0, inputCount);
        byte[] result = _mTransform.TransformFinalBlock(lastBlocks, 0, lastBlocks.Length);
        Debug.Assert(inputCount < result.Length);
        Array.Resize(ref result, inputCount);
        return result;
    }

    public void Dispose()
    {
        _mTransform.Dispose();
    }
}

Upvotes: 0

Related Questions