Остановка дешифрования до того, как EOF выдаст исключение: заполнение недопустимо и не может быть удалено

У нас есть такой сценарий: у нас есть огромные зашифрованные файлы порядка гигабайт, которые мы можем правильно расшифровать, если прочитаем их до конца. Проблема возникает, когда мы читаем и обнаруживаем какой-то флаг в файле, затем мы прекращаем чтение и вызываем reader.Close(), что происходит, так это то, что CryptographicException: «Заполнение недопустимо и не может быть удалено». бросается. У меня есть это небольшое консольное приложение, которое воспроизводит это поведение, чтобы протестировать его, просто запустите его, оно создаст файл на вашем диске C: \, а затем будет читать строку за строкой при нажатии любой клавиши и остановится при нажатии «q». .

using System;
using System.IO;
using System.Security.Cryptography;

namespace encryptSample
{
    class Program
    {
        static void Main(string[] args)
        {
            var transform = CreateCryptoTransform(true);
            // first create encrypted file
            using (FileStream destination = new FileStream("c:\\test_enc.txt", FileMode.OpenOrCreate, FileAccess.Write, FileShare.ReadWrite))
            {
                using (CryptoStream cryptoStream = new CryptoStream(destination, transform, CryptoStreamMode.Write))
                {
                    using (StreamWriter source = new StreamWriter(cryptoStream))
                    {
                        for (int i = 0; i < 1000; i++)
                        {
                            source.WriteLine("This is just random text to fill the file and show what happens when I stop reading in the middle - " + i);
                        }
                        // Also tried this line, but is the same with or without it
                        cryptoStream.FlushFinalBlock();
                    }
                }
            }

            StreamReader reader;
            ICryptoTransform transformDec;
            CryptoStream cryptoStreamReader;

            transformDec = CreateCryptoTransform(false);
            FileStream fileStream = new FileStream("c:\\test_enc.txt", FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
            cryptoStreamReader = new CryptoStream(fileStream, transformDec, CryptoStreamMode.Read);
            reader = new StreamReader(cryptoStreamReader);

            while (Console.In.ReadLine() != "q")
            {
                Console.WriteLine(reader.ReadLine());
            }

            try
            {
                cryptoStreamReader.Close();
                reader.Close();
                reader.Dispose();
            }
            catch (CryptographicException ex)
            {
                if (reader.EndOfStream)
                    throw;

            }
        }

        private static ICryptoTransform CreateCryptoTransform(bool encrypt)
        {
            byte[] salt = new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; // Must be at least eight bytes.  MAKE THIS SALTIER!
            const int iterations = 1042; // Recommendation is >= 1000.
            const string password = "123456";

            AesManaged aes = new AesManaged();
            aes.BlockSize = aes.LegalBlockSizes[0].MaxSize;
            aes.KeySize = aes.LegalKeySizes[0].MaxSize;
            // NB: Rfc2898DeriveBytes initialization and subsequent calls to   GetBytes   must be eactly the same, including order, on both the encryption and decryption sides.
            Rfc2898DeriveBytes key = new Rfc2898DeriveBytes(password, salt, iterations);
            aes.Key = key.GetBytes(aes.KeySize / 8);
            aes.IV = key.GetBytes(aes.BlockSize / 8);
            aes.Mode = CipherMode.CBC;
            aes.Padding = PaddingMode.PKCS7;
            ICryptoTransform transform = encrypt ? aes.CreateEncryptor(aes.Key, aes.IV) : aes.CreateDecryptor(aes.Key, aes.IV);
            return transform;
        }

    }
}

В нашем исходном классе мы делаем reader.Close во время Dispose(). Мой вопрос: допустимо ли проверять, является ли reader.EndOfStream ложным, а затем перехватывать CryptographicException? Или что-то не так в методах шифрования/дешифрования? Может быть, мы что-то упускаем.

С Уважением!


person emmanuel    schedule 26.03.2013    source источник
comment
Кстати, ты смог это решить?   -  person Panda Pajama    schedule 27.02.2014
comment
Мы решили, проверив некоторый статус, который сообщает нам, прервал ли пользователь чтение, а также проверив .EndOfStream. Нам было все равно, если это неопределенное поведение; это вызывало проблему только тогда, когда система обнаруживала определенные флаги, и нам приходилось специально прекращать чтение. Как вы сказали, это недокументированное поведение, но одно из многих в библиотеках криптографии, поэтому мы справляемся с этим как можно лучше, и если в будущей версии будет сделано исправление, нам придется снова изменить наш код: S.   -  person emmanuel    schedule 06.03.2014


Ответы (5)


Это исключение выдается во время Dispose(true). Выброс из Dispose уже является недостатком дизайна (https://docs.microsoft.com/en-us/visualstudio/code-quality/ca1065-do-not-raise-exceptions-in-unexpected-locations#dispose-methods), но это еще хуже, поскольку это исключение генерируется еще до закрытия основного потока.

Это означает, что все, что получает поток, который может быть криптопотоком, должен обойти это и либо сам закрыть базовый поток в блоке catch (по сути, требуется ссылка на что-то совершенно не связанное), либо каким-то образом предупредить всех слушателей, что поток все еще может быть открыт (например, «не пытайтесь удалить основной файл — он все еще открыт!»).

Нет, в моей книге это довольно большое упущение, и другие ответы, похоже, не затрагивают фундаментальную проблему. CryptoStream берет на себя ответственность за переданный поток, поэтому он берет на себя ответственность закрыть базовый поток до того, как управление покинет Dispose(true), конец истории.

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

Наше решение было в основном таким (обновление: но имейте в виду - как указал Уилл Краузе в комментариях, это может оставить конфиденциальную информацию в приватных полях _InputBuffer и _OutputBuffer, к которым можно получить доступ через отражение. Версии 4.5 и выше в .NET Framework такой проблемы нет.):

internal sealed class SilentCryptoStream : CryptoStream
{
    private readonly Stream underlyingStream;

    public SilentCryptoStream(Stream stream, ICryptoTransform transform, CryptoStreamMode mode)
        : base(stream, transform, mode)
    {
        // stream is already implicitly validated non-null in the base constructor.
        this.underlyingStream = stream;
    }

    protected override void Dispose(bool disposing)
    {
        try
        {
            base.Dispose(disposing);
        }
        catch (CryptographicException)
        {
            if (disposing)
            {
                this.underlyingStream.Dispose();
            }
        }
    }
}
person Joe Amenta    schedule 27.02.2014
comment
Вы также можете захотеть обнулить закрытые элементы _InputBuffer и _OutputBuffer CryptoStream, поскольку это исключение может оставить конфиденциальную информацию в памяти. Глядя на referencesource.microsoft.com, похоже, это больше не проблема. - person Will Krause; 22.11.2014
comment
@WillKrause: Спасибо! Я отредактировал ответ, включив эту информацию вместе со ссылкой на точный код, на который вы ссылаетесь. В качестве примечания: я почти уверен, что серьезность проблемы _InputBuffer/_OutputBuffer смягчена тем фактом, что ее можно использовать только с использованием отражения, которое по умолчанию ограничено полностью надежным кодом. - person Joe Amenta; 22.11.2014
comment
Обратите внимание, что, по крайней мере, для потоков в режиме чтения это было исправлено для .NET Core 3.0 начиная с предварительной версии 4: github.com/dotnet/corefx/issues/7779 --› github.com/dotnet/corefx/pull/36048 --› github.com /dotnet/corefx/commit/f99562f - person Joe Amenta; 28.04.2019

Насколько я понимаю, исключение выдается, когда последний прочитанный байт не является допустимым байтом заполнения. Когда вы намеренно закрываете поток досрочно, последний прочитанный байт, скорее всего, будет считаться «недопустимым дополнением», и будет выдано исключение. Поскольку вы намеренно заканчиваете, вы должны быть в безопасности, игнорируя исключение.

person Ryan Frame    schedule 26.03.2013

Close вызывает Dispose(true), который вызывает FlushFinalBlock, который выдает исключение, потому что на самом деле это не последний блок.

Вы можете предотвратить это, переопределив метод Close, чтобы он не вызывал FlushFinalBlock:

public class SilentCryptoStream : CryptoStream {
    public SilentCryptoStream(Stream stream, ICryptoTransform transform, CryptoStreamMode mode) :
        base(stream, transform, mode) {
    }

    public override void Close() {
        this.Dispose(false);
        GC.SuppressFinalize(this);
    }
}

(Вам также необходимо вручную закрыть базовый поток.)

допустимо ли проверять, является ли reader.EndOfStream ложным, а затем захватывать CryptographicException

Я думаю, это нормально.

person Ark-kun    schedule 12.02.2014
comment
Я не уверен, что захват исключения в порядке. Скорее всего, никто не пострадает, но смысл Dispose в освобождении неуправляемых ресурсов. Таким образом, если исключение не выброшено на самом последнем шаге после того, как все остальное было выпущено, вполне вероятно, что его захват и игнорирование приведет к утечке ресурсов или памяти. Кроме того, откуда вы знаете, что Dispose(false) будет вести себя именно так, как вы говорите? - person Panda Pajama; 13.02.2014
comment
›Вполне вероятно, что его захват и игнорирование приведет к утечке ресурсов или памяти. Нет. CryptoStream умен и заключает FlushFinalBlock и закрытие базового потока в блок try, в то время как другой код очистки находится в блоке finally. Кроме того, CryptoStream не владеет никакими неуправляемыми ресурсами. Ему нечему течь. - person Ark-kun; 14.02.2014
comment
› Кроме того, откуда вы знаете, что Dispose(false) будет вести себя именно так, как вы говорите? Я посмотрел исходный код. Логический параметр disposing разрешает только вызов FlushFinalBlock (если он еще не сброшен) и закрытие базового потока. - person Ark-kun; 14.02.2014
comment
Я проверил сборку с помощью DotPeek, и вызов Dispose(false) не закроет базовый поток. Это отличается от того, что вы нашли, поэтому вполне вероятно, что оно зависит от реализации. Я бы хотел избежать перехвата исключения, поскольку CryptoStream является одним из многих многоуровневых потоков и используется во многих местах. Я думаю, я мог бы создать новый класс и поймать там исключение, но я бы предпочел предотвратить исключение исключения в первую очередь. - person Panda Pajama; 15.02.2014
comment
›вызов Dispose(false) не закроет базовый поток. да. Я специально писал об этом с самого начала. Это отличается от того, что вы нашли Нет, это то же самое. Перечитайте мой ответ - (You also need to manually close the underlying stream.) и мой комментарий - The boolean disposing parameter only enables calling the FlushFinalBlock (if not yet flushed) and the base stream closing. - person Ark-kun; 16.02.2014
comment
Хорошо. Я ожидал, что кто-то знает способ аккуратно закрыть CryptoStream. Например, предотвратить проблему, а не решить ее. Простите, что доставил вам столько неудобств. - person Panda Pajama; 16.02.2014
comment
@PandaPajama Это то, что я пытался сделать. Я тщательно изучил CryptoStream источники. В настоящее время НЕТ способа предотвратить это. Дело не в знаниях. Если бы был способ, я бы его нашел. - person Ark-kun; 17.02.2014
comment
К сожалению, ни одно из двух ваших решений нельзя использовать в производственной среде. У CryptoStream есть спецификация; переопределение функции Close может привести к другим проблемам, даже если не в этом конкретном случае, возможно, в будущей версии этого класса или в версии, предлагаемой другим поставщиком. Интересно, что вы сами исследовали это, но я бы предпочел категоричный ответ, например, ваше НЕТ с большой буквы из авторитетного источника, и в этом был смысл награды. Казалось, никому до этого нет дела, так что неважно... - person Panda Pajama; 18.02.2014
comment
Перехват исключения в этом конкретном случае кажется безопасным в будущем и пригодным для производства. По сути, вы прерываете какую-то операцию, поэтому естественно, что генерируется исключение, и вы имеете право поймать его в этой ситуации. Поймайте его и проверьте условия. - person Ark-kun; 21.02.2014
comment
Между кажется и есть огромная разница. Эта конкретная реализация CryptoStream не действует в соответствии со спецификацией и поэтому содержит ошибки. В спецификации даже не упоминается возможность такого исключения. Я упомянул об этом, потому что это не вызывает исключения для библиотек PSM Mono, и я обнаружил это исключение при обратном переносе на реализацию MS. - person Panda Pajama; 21.02.2014
comment
Это также небезопасно для производства, потому что может быть (сейчас или в будущем, а также в той или иной реализации этого класса) исключение, которое вы можете непреднамеренно перехватить и проигнорировать с помощью этого взлома. - person Panda Pajama; 21.02.2014
comment
исключение, которое вы можете непреднамеренно перехватить и проигнорировать с помощью этого хака. Этого не может случиться: я предложил перехватывать исключение ТОЛЬКО в явной ситуации, когда вы в основном прерываете операцию (и вы знаете, когда вы делаем это). Вы же не удивляетесь, когда получаете ThreadAbortException при звонке thread.Abort(), не так ли? Это исключение. Вы должны поймать его и оценить ситуацию, чтобы решить, бросать ли его повторно или продолжать. Это цель блока catch. - person Ark-kun; 27.02.2014
comment
ПСМ ужасен. Он очень старый и устаревший. - person Ark-kun; 27.02.2014
comment
Повторюсь: вы проверили соответствующую часть потока. Вам не нужны никакие дополнительные потоковые данные (если бы вам было небезразлично, вы бы прочитали их до конца, чтобы проверить правильность шифрования). Вы выполняете операцию прерывания, которая должна вызывать исключение, но может не выполнять этого в некоторых реализациях. Это не является исключительным — вы на 100% ожидаете этого. Лови. try{ cryptoStreamReader.Close(); } catch() {} - person Ark-kun; 27.02.2014
comment
msdn.microsoft.com/en-us/ library/ ...вызов метода Close. При этом поток очищается, а все оставшиеся блоки данных обрабатываются объектом CryptoStream. msdn.microsoft.com/en-us/library/ Алгоритм AES по сути является алгоритмом Rijndael. - person Ark-kun; 27.02.2014
comment
msdn.microsoft.com/en-us/ library/ CryptographicException Здесь это referencesource -beta.microsoft.com/#mscorlib/system/security/ . - person Ark-kun; 27.02.2014
comment
Вы, кажется, упускаете мою мысль. Проблема здесь не в том, могу ли я поймать это исключение. Конечно я могу. Проблема связана с спецификацией Close(). Он никогда не упоминает, что может генерировать такое исключение, поэтому ситуация, в которой возникает это исключение, является чем-то, что не считалось возможным, когда библиотека была написана. Полагаться на неопределенное поведение очень опасно: другие версии библиотеки могут выдать другое исключение, привести к сбою программы или отформатировать ваш жесткий диск. Мне нужен документированный способ предотвращения этой проблемы. - person Panda Pajama; 27.02.2014
comment
Что такого ужасного и устаревшего в PSM? Я считаю, что это очень полезно. Может быть, вы захотите просветить меня, что в этом такого ужасного и устаревшего? Может быть, даже предложить альтернативу? Обратите внимание, что библиотека с ошибками здесь является официальной библиотекой Microsoft, а не той, что есть в PSM. - person Panda Pajama; 27.02.2014
comment
К сожалению, обсуждать особо нечего. Решение, которое вы предлагаете, хотя и заставляет программу работать, неприемлемо для моих целей. Если у вас есть решение, которое действительно задокументировано, опубликуйте его как новый ответ, и мы обсудим это, но это обсуждение никуда не денется, и в любом случае заливать комментарии не очень конструктивно. - person Panda Pajama; 27.02.2014
comment
Проблема в спецификации Close(). Он никогда не упоминает, что может генерировать такое исключение. И Stream.Close, и Stream.Dispose являются виртуальными. Никто не может контролировать (и, следовательно, документировать) поведение переопределений. Вызов виртуального метода может привести к любому исключению. В вашем случае это класс RjindaelManaged, который бросает. И именно вы соединяете их вместе (передавая Aesmanaged конструктору CryptoStream). - person Ark-kun; 27.02.2014
comment
Полагаться на неопределенное поведение очень опасно: другие версии библиотеки могут выдать другое исключение, привести к сбою программы или отформатировать ваш жесткий диск. Да, конечно. Любое переопределение IDisposable.Dispose() может отформатировать ваш жесткий диск. Документация IDisposable.Dispose() никогда не скажет вам об этом (поскольку это не известно заранее) и не предотвратит этого. - person Ark-kun; 27.02.2014
comment
Что такого ужасного и устаревшего в PSM? Если под PSM вы подразумеваете PlayStation Mobile Studio, то она устарела, потому что использует очень старую версию Mono. Sony никогда не обновляла его, за исключением небольших исправлений. - person Ark-kun; 27.02.2014
comment
Мне нужен документированный способ предотвратить эту проблему. Каков задокументированный способ предотвратить int a = 1 / 0; от выбрасывания DivisionByZeroException? Каков задокументированный способ предотвратить new Stack<int>().Pop() от выбрасывания InvalidOperationException? Ответы на эти вопросы помогут вам ответить на ваш первоначальный вопрос. - person Ark-kun; 27.02.2014
comment
CryptoStream.Close ( не Stream.Close) бросать CryptographicException куда попало. Неопределенное поведение — это не то же самое, что исключение. Деление на 0 определено для создания исключения; Popping пустой стек определен для генерации исключения, и пока он определен таким образом, вы можете положиться на спецификацию и знать, что вы собираетесь получить. CryptoStream (не Stream) не определено для выдачи CryptographicException. Это неопределенное поведение, и полагаться на него опасно. - person Panda Pajama; 28.02.2014
comment
Я предполагаю, что вы широко использовали PSM, чтобы заявить, что это ужасно, поэтому я полагаю, что вы используете альтернативу, которая использует гораздо более новую версию Mono (хотя в моей книге поддержка C # 4 достаточно недавно), что для целях этого потока, вызывает исключение в этом случае. Не хотите поделиться? Нет? Тогда к чему был этот комментарий? - person Panda Pajama; 28.02.2014
comment
давайте продолжим это обсуждение в чате - person Panda Pajama; 28.02.2014

Можно ли отключить прокладку?

// aes.Padding = PaddingMode.PKCS7;
aes.Padding = PaddingMode.None;
person Steve Wellens    schedule 26.03.2013
comment
Я попытался отключить заполнение, но если я это сделаю, шифрование AES не удастся. Насколько я понимаю, AES нуждается в дополнении. - person emmanuel; 26.03.2013
comment
@emmanuel - Это заставляет вашу тестовую программу работать правильно без исключений. - person Steve Wellens; 26.03.2013
comment
Вы правы, но когда я проверяю вывод, на нем мусор :S, попробуйте заменить while и записать в файл: File.WriteAllText("c:\\test_decrypted", reader.ReadToEnd());. Если я прокомментирую строку cryptoStream.FlushFinalBlock();, ошибка, которую я получаю, является исключением, о котором я упоминал ранее при закрытии перед EOF. - person emmanuel; 27.03.2013
comment
Я попробовал это и получил некоторое повреждение в конце файла. Я удалил cryptoStream.FlushFinalBlock() и с отступом, установленным на none, он работал без исключений и без повреждений. - person Steve Wellens; 27.03.2013
comment
Это действительно странно, если я это сделаю, я получу это исключение: длина данных для шифрования недействительна. при шифровании, поэтому файл не создается :S. - person emmanuel; 27.03.2013

Мое решение состояло в том, чтобы в моем производном классе добавить это к моему переопределению Dispose (bool):

    protected override void Dispose(bool disposing)
    {
        // CryptoStream.Dispose(bool) has a bug in read mode. If the reader doesn't read all the way to the end of the stream, it throws an exception while trying to
        // read the final block during Dispose(). We'll work around this here by moving to the end of the stream for them. This avoids the thrown exception and
        // allows everything to be cleaned up (disposed, wiped from memory, etc.) properly.
        if ((disposing) &&
            (CanRead) &&
            (m_TransformMode == CryptoStreamMode.Read))
        {
            const int BUFFER_SIZE = 32768;
            byte[] buffer = new byte[BUFFER_SIZE];

            while (Read(buffer, 0, BUFFER_SIZE) == BUFFER_SIZE)
            {
            }
        }

        base.Dispose(disposing);
        ...

Убедившись, что поток всегда читается до конца, можно избежать внутренней проблемы в CryptStream.Dispose. Конечно, вам нужно сопоставить это с характером того, что вы читаете, чтобы убедиться, что это не окажет негативного влияния. Используйте его только против источника известной конечной длины.

person gregsohl    schedule 06.07.2015