在文件结束前停止解密会抛出异常:填充无效且无法移除。

10

这是我们的场景:我们有巨大的加密文件,大小达到几个千兆字节,如果我们读取到文件结尾,我们可以正确地解密它们。但是当我们在读取文件时检测到某些标志后,我们会停止读取并调用reader.Close()函数时,会抛出一个CryptographicException异常:"Padding is invalid and cannot be removed."。 我有一个小型的控制台应用程序来重现这种行为,要测试它只需运行它,它将在你的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;
        }

    }
}

在我们原始的类中,在Dispose()期间我们执行reader.Close。我的问题是,检查reader.EndOfStream是否为false然后捕获CryptographicException是否有效?或者加密/解密方法存在问题?也许我们漏掉了什么。

此致!


顺便问一下,你能解决这个问题吗? - Panda Pajama
我们通过检查一些状态来“解决”,这些状态告诉我们用户是否中止了阅读,并通过检查.EndOfStream。我们并不关心它是否是未定义的行为;只有在系统检测到某些标志并且我们必须有意停止阅读时,它才会引起问题。正如你所说,这是一个未记录的行为,但在加密库中有许多类似的情况,因此我们尽力处理它,如果在将来的版本中进行修复,我们将不得不再次更改我们的代码 :S。 - emmanuel
5个回答

7
在Dispose(true)期间抛出此异常。从Dispose中抛出异常已经是一个设计缺陷(https://learn.microsoft.com/en-us/visualstudio/code-quality/ca1065-do-not-raise-exceptions-in-unexpected-locations#dispose-methods),但更糟糕的是,这个异常甚至在底层流关闭之前就被抛出了。
这意味着任何接收可能是CryptoStream的流的东西都需要解决这个问题,并在“catch”块中自己关闭底层流(实际上需要引用与其完全无关的东西),或者以某种方式警告所有侦听器,流可能仍然处于打开状态(例如,“不要尝试删除底层文件——它仍然处于打开状态!”)。
在我看来,这是一个相当大的疏忽,其他答案似乎没有解决根本问题。CryptoStream获取传入流的所有权,因此它负责在控制离开Dispose(true)之前关闭底层流,结束故事。
Ideally, it should also never throw under circumstances that are not truly exceptional (such as "we stopped reading early, because the decrypted data is in the wrong format and it's a waste of time to continue reading").
我们的解决方案基本上是这样的(更新:但请注意--正如Will Krause在评论中指出的那样,这可能会在私有_InputBuffer和_OutputBuffer字段中留下敏感信息,可以通过反射访问。.NET Framework 4.5及以上版本没有此问题)。
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();
            }
        }
    }
}

1
您可能还想将CryptoStream的私有_InputBuffer和_OutputBuffer成员清零,因为此异常可能会在内存中留下敏感信息。查看referencesource.microsoft.com,这似乎不再是一个问题。 - Will Krause
@WillKrause:谢谢!我已经编辑了答案,包括这些信息和你提到的确切代码的链接。另外,我非常确定_InputBuffer / _OutputBuffer问题的严重性已经被缓解,因为它只能通过反射来利用,而默认情况下反射是受限于完全信任的代码的。 - Joe Amenta
请注意,至少对于读取模式流而言,在 .NET Core 3.0 预览版 4 中似乎已经修复了此问题:https://github.com/dotnet/corefx/issues/7779 --> https://github.com/dotnet/corefx/pull/36048 --> https://github.com/dotnet/corefx/commit/f99562f - Joe Amenta

1
据我所理解,当最后一个读取的字节不是有效的填充字节时,会抛出异常。当您故意提前关闭流时,最后一个读取的字节很可能被视为“无效的填充”,然后抛出异常。由于您是有意结束的,因此可以安全地忽略该异常。

1
< p > 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是否为false,然后捕获CryptographicException是否有效?

我认为这是可以的。


我不确定捕获异常是否可行。很可能没有人会受到影响,但“Dispose”的目的是释放非托管资源。因此,除非异常在所有其他内容都已释放之后的最后一步抛出,否则捕获并忽略它很可能会导致资源或内存泄漏。此外,您如何知道“Dispose(false)”会按照您所说的方式执行? - Panda Pajama
很可能捕获它并忽略它会导致资源或内存泄漏。不,CryptoStream 很聪明,它在 try 块中包装了 FlushFinalBlock 和基础流关闭,而其他清理代码则在 finally 块中。此外,CryptoStream 不拥有任何非托管资源。它没有任何泄漏。 - Ark-kun
我使用DotPeek检查了程序集,并发现调用Dispose(false)不会关闭底层流。这与您的发现不同,因此很可能因实现而异。捕获异常是我想避免的事情,因为CryptoStream是许多分层流之一,并且在许多地方使用。我猜我可以派生一个新类并在那里捕获异常,但我宁愿在第一时间防止异常触发。 - Panda Pajama
很不幸,您提出的两个解决方案都不能在生产环境中使用。CryptoStream有一个规范;覆盖Close功能可能会引入其他问题,即使在这种特定情况下没有问题,也可能会在该类的将来版本或其他供应商提供的类中出现问题。您自己进行研究很有趣,但我更喜欢来自权威来源的明确答案,就像您用大写字母表示的NO一样,而这也是悬赏的目的所在。似乎没有其他人关心这个问题,所以无论如何... - Panda Pajama
“似乎”和“是”之间有很大的区别。这个特定的CryptoStream实现不按照规范行事,因此存在缺陷。规范甚至没有提到这种异常的可能性。我提出这个问题是因为在PSM Mono库上它并没有抛出异常,在迁回MS实现时我发现了这个异常。 - Panda Pajama
显示剩余21条评论

0

你能关掉填充吗?

// aes.Padding = PaddingMode.PKCS7;
aes.Padding = PaddingMode.None;

我尝试关闭填充,但这样做会导致AES加密失败。据我所知,AES需要填充。 - emmanuel
@emmanuel - 它可以让你的测试程序顺利运行,不出现异常。 - Steve Wellens
你说得没错,但是当我检查输出时,上面有垃圾 :S,请尝试替换 while 并写入文件:File.WriteAllText("c:\\test_decrypted", reader.ReadToEnd());。如果我注释掉 cryptoStream.FlushFinalBlock(); 这一行,那么当关闭 EOF 之前就会出现我之前提到的异常。 - emmanuel
我试过了,文件末尾出现了一些损坏。我移除了cryptoStream.FlushFinalBlock()并将padding设置为none,在没有任何异常和损坏的情况下成功运行。 - Steve Wellens
这真的很奇怪,如果我这样做,加密时会出现异常:“要加密的数据长度无效。”因此文件没有生成:S。 - emmanuel

0
我的解决方案是,在我的派生类中,将以下内容添加到我的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中的内部问题。当然,您需要权衡一下您所读内容的性质,以确保它不会产生负面影响。只在已知有限长度的源上使用它。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接