为什么在加密字符串时得到空结果,即使我刷新了输入流?

问题描述

我想通过 RijndaelManaged 加密一些字符串并获得加密结果。我想用 MemoryStream 来做。但我得到了空字符串。如果我使用 Rijndael 类而不是 RijndaelManaged,我会遇到同样的问题。我做错了什么?

static string EncodeString(string text,byte[] key,byte[] iv) {
    RijndaelManaged alg = new RijndaelManaged();
    var encryptor = alg.CreateEncryptor(key,iv);
    string encodedString = null;
    using (var s = new MemoryStream())
    using (var cs = new CryptoStream(s,encryptor,CryptoStreamMode.Write))
    using (var sw = new StreamWriter(cs)) {
        // encrypt the string
        sw.Write(text);
        sw.Flush();
        cs.Flush();
        s.Flush();
        Console.WriteLine($"Stream position: {s.Position}"); // Oops... It is 0 still. Why?
        // get encrypted string
        var sr = new StreamReader(s);
        s.Position = 0;
        encodedString = sr.ReadToEnd(); // I get empty string here
    }
    return encodedString;
}

那我就用这个方法

    RijndaelManaged alg = new RijndaelManaged();
    alg.GenerateKey();
    alg.GenerateIV();
    var encodedString = EncodeString("Hello,Dev!",alg.Key,alg.IV); // I get empty string. Why?

解决方法

这里有两个问题:


问题 1:您尝试在结果准备好之前读取结果。您需要先关闭 StreamWriter:

using (var s = new MemoryStream())
using (var cs = new CryptoStream(s,encryptor,CryptoStreamMode.Write))
using (var sw = new StreamWriter(cs)) {
    // encrypt the string
    sw.Write(text);
    Console.WriteLine(s.ToArray().Length); // prints 0
    sw.Close();
    Console.WriteLine(s.ToArray().Length); // prints 16
    ...
}

但是我为什么需要这个?你没看到我代码中的所有 Flush 语句吗? 是的,但 Rijndael 是一个块密码。它只能在读取完整块后才能加密块(或者您告诉它这是最后的部分块)。 Flush 允许将更多数据写入流中,因此加密器无法确定块是否完整。

您可以通过明确告诉加密流您已完成发送输入来解决此问题。 reference implementation 通过使用嵌套 using 语句关闭 StreamWriter(以及 CryptoStream)来完成此操作。一旦 CryptoStream 关​​闭,它就会刷新最后一个块。

using (var s = new MemoryStream())
using (var cs = new CryptoStream(s,CryptoStreamMode.Write))
{ 
    using (var sw = new StreamWriter(cs))
    {
        // encrypt the string
        sw.Write(text);
    }
    Console.WriteLine(s.ToArray().Length); // prints 16
    ...
}

或者,正如 Jimi 在评论中提到的,您可以显式调用 FlushFinalBlock。此外,您可以通过将基本字符串显式转换为字节数组来跳过 StreamWriter:

using (var s = new MemoryStream())
using (var cs = new CryptoStream(s,CryptoStreamMode.Write))
{
    cs.Write(Encoding.UTF8.GetBytes(text));
    cs.FlushFinalBlock();
    Console.WriteLine(s.ToArray().Length); // prints 16
    ...
}

或者,正如 V.Lorz 在评论中提到的,您可以直接处理 CryptoStream 以隐式调用 FlushFinalBlock:

using (var s = new MemoryStream())
{
    using (var cs = new CryptoStream(s,CryptoStreamMode.Write))
    {
        cs.Write(Encoding.UTF8.GetBytes(text));
    }
    Console.WriteLine(s.ToArray().Length); // prints 16
    ...
}

问题 2:您试图将结果作为字符串读取。 加密不适用于字符串,它适用于字节数组。因此,尝试将结果读取为 UTF-8 字符串将导致垃圾。

相反,例如,您可以使用结果字节数组的 Base64 表示:

return Convert.ToBase64String(s.ToArray());

这里是您的代码的工作小提琴,并应用了所有这些修复:

  1. 使用 StreamWriter:https://dotnetfiddle.net/8kGI4N
  2. 没有 StreamWriter:https://dotnetfiddle.net/Nno0DF