Заполнение AES предотвращает отправку последнего блока

#c# #.net-core #encryption #stream #aes

Вопрос:

Я пишу приложение для отправки и получения зашифрованных данных AES. Я использую a CryptoStream , который записывает/читает из a NetworkStream . Сначала я попытался использовать встроенную прокладку, как это:

 // 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
    }
}
 

Обратите внимание, что последний аргумент new CryptoStream() имеет значение true — базовый поток ( networkStream ) не будет закрыт при CryptoStream удалении. Одна интересная вещь заключается в том, что если я не установлю это true значение, то прием начнет работать правильно. Но поскольку мне нужно networkStream , чтобы он оставался открытым, он должен быть установлен на true .

При вышеуказанной реализации принимающий поток никогда не получает все данные — он блокирует себя на последнем csDecrypt.ReadAsync() . Исходя из моего понимания csEncrypt.FlushFinalBlock() , следует отправить последний блок с добавленным дополнением, но по какой-то причине этого не происходит.

Поскольку это не сработало, я сам добавил обивку вот так:

 // 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 amp;amp; 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
    }
}
 

Если я сам добавлю заполнение, то все будет работать правильно, и функция приема не заблокируется в последнем буфере. Что я должен сделать, чтобы встроенная обивка работала?

Ответ №1:

Проблема на стороне получателя. Если вы не закроете поток, то процедура расшифровки не будет знать, что получен последний блок, поэтому она просто ждет следующего блока, который никогда не приходит. То, что вы делаете сейчас, должно работать, если оно реализовано правильно. Но вам нужно выполнить распаковку только в конце — хотя в вашей текущей реализации не реализована распаковка PKCS#7.

Есть еще один, возможно, более чистый способ: считывание из потока, который действительно закрывается. Очевидно, вы знаете размер открытого текста или зашифрованного текста. Итак, что вы можете сделать, так это использовать поток, который обертывает другой поток, но который закрывается после считывания всех байтов зашифрованного текста — и, конечно, не закрывает базовый поток, когда это происходит. Это немного работы, но это не должно быть так сложно, так как вам нужно всего лишь реализовать Read метод (см. Stream Документацию по классу, чтобы понять, почему).

Я думаю, что второй вариант является самым чистым, но вы, конечно, можете просто прочитать необходимые байты, а затем использовать decryptor непосредственно для расшифровки байтов (буфер за буфером). Не самый лучший по сравнению с трюком с ограниченным потоком, но, вероятно, самый простой, с которым вы можете справиться.

Итак, вот вы где: три варианта: выбрать и выбрать и… ну, это очевидно.

Комментарии:

1. Какой поток я должен использовать в качестве обертки? Я пробовал использовать поток памяти раньше, но это приводит к чрезмерному использованию оперативной памяти. Есть ли какая-либо альтернатива потоку памяти?

2. Да, просто прямое Stream производное от вашего собственного. Вам нужно сохранить только одно поле, отличное от родительского потока: количество байтов, которые еще нужно прочитать, прежде чем указывать, что поток закрыт, не возвращая никаких байтов. Назовите это, э-э, BoundedInputStream или чем-то подобным.