C# Rijndael解密returns多出问号字符

C# Rijndael decryption returns extra question mark character

我正在 class 中组织一些非常基本的对称 encryption/decryption 代码以备将来使用。我能够成功加密和解密...只是有一个小问题。

这是我的代码,它从一个流中读入并 en/decrypt 到另一个流:

public void Encrypt(Stream input, Stream output) {
    byte[] key = Encoding.UTF8.GetBytes(_pw);
    byte[] iv = Encoding.UTF8.GetBytes(GenerateInitVector());
    RijndaelManaged rm = new RijndaelManaged();
    CryptoStream cs = new CryptoStream(
        output,
        rm.CreateEncryptor(key, iv),
        CryptoStreamMode.Write);
    int data;
    while ((data = input.ReadByte()) != -1)
        cs.WriteByte((byte) data);
    cs.Close();
}

public void Decrypt(Stream input, Stream output) {
    byte[] key = Encoding.UTF8.GetBytes(_pw);
    byte[] iv = Encoding.UTF8.GetBytes(GenerateInitVector());
    RijndaelManaged rm = new RijndaelManaged();
    CryptoStream cs = new CryptoStream(
        input,
        rm.CreateDecryptor(key, iv),
        CryptoStreamMode.Read);
    int data;
    while ((data = cs.ReadByte()) != -1)
        output.WriteByte((byte) data);
    cs.Close();
}

供您参考,方法 GenerateInitVector() 总是 returns 相同的值。

这是我的测试文件。

hello
world
this
is
a
word
list

当我尝试 en/decrypt 从 FileStream 到 FileStream 时,一切正常,使用这些方法:

public void Encrypt(string inputPath, string outputPath) {
    FileStream input = new FileStream(inputPath, FileMode.Open);
    FileStream output = new FileStream(outputPath, FileMode.Create);
    Encrypt(input, output);
    output.Close();
    input.Close();
}

public void Decrypt(string inputPath, string outputPath) {
    FileStream input = new FileStream(inputPath, FileMode.Open);
    FileStream output = new FileStream(outputPath, FileMode.Create);
    Decrypt(input, output);
    output.Close();
    input.Close();
}

当我尝试将文件解密到 MemoryStream 中,然后将字节转换为字符的数组加载到 StringBuilder 中,最后将其作为字符串打印到控制台时,我进入了控制台:

?hello
world
this
is
a
word
list

我所有的文字前面都多了一个问号。这是我的解密方法:

public StringBuilder Decrypt(string inputPath) {
    FileStream input = new FileStream(inputPath, FileMode.Open);
    byte[] buffer = new byte[4096];
    MemoryStream output = new MemoryStream(buffer);
    Decrypt(input, output);
    StringBuilder sb = new StringBuilder(4096);
    sb.Append(Encoding.UTF8.GetChars(buffer, 0, (int) output.Position));
    input.Close();
    output.Close();
    return sb;
}

我在这里读过一些关于 BOM 和 C# 的类似内容字符:

Result of RSA encryption/decryption has 3 question marks

因此我再次确认我使用的是UTF-8编码。尽管如此,我还是看到了这个额外的问号。为了完整起见,我还写了这个方法来将StringBuilder中的内容加密到文件中:

public void Encrypt(string outputPath, StringBuilder builder) {
    char[] buffer = new char[builder.Length];
    builder.CopyTo(0, buffer, 0, builder.Length);
    byte[] buf = Encoding.UTF8.GetBytes(buffer);
    MemoryStream input = new MemoryStream(buf);
    FileStream output = new FileStream(outputPath, FileMode.Create);
    Encrypt(input, output);
    output.Close();
    input.Close();
}

然后我通过做交叉检查:

var builder = new StringBuilder();
builder.Append("Hello world.");
Encrypt("test.txt.enc", builder);
Decrypt("test.txt.enc", "test.txt");
builder = Decrypt("test.txt.enc");
Console.WriteLine(builder.ToString());

对于文件 test.txt,一切正常。奇怪的是,对于打印在控制台上的文本,我在前面得到 NO 额外的问号:

Hello world.

整个过程有什么问题?

问号是UTF8的BOM(Byte Order Mark)是0xef 0xbb 0xbf 这些字节写在以 UTF8 编码的文件的开头。

因为 FileStream 以字节形式读取文件并且不将其解释为 UTF8 文本文件,所以 BOM 包含在您的加密中,因此如果您解密它并将其保存到文件中,它在 TextEditor 中看起来一切正常,但如果您将其转储到控制台还打印了 BOM,因为控制台不知道它是某种控件 sequence/marker

编辑: 这是获取没有BOM的字符串的解决方案。

    public static string Decrypt(string inputPath)
    {
        FileStream input = new FileStream(inputPath, FileMode.Open);
        MemoryStream output = new MemoryStream();
        Decrypt(input, output);
        StreamReader reader = new StreamReader(output, new UTF8Encoding()); //Read with encoding
        output.Seek(0, SeekOrigin.Begin); //Set stream Position to beginning
        string result = reader.ReadToEnd(); //read to string
        reader.Close();
        input.Close();
        output.Close();
        return result;
    }

一些问题:

  • Key和IV是固定长度的二进制序列,可以包含任意字节,所以UTF-8不可能是对的

    从名为 pw 的变量复制密钥,大概是 'password' 的缩写,表示相关的混淆。密码不是密钥。您应该使用密钥派生函数,最好是专门用于密码散列的函数,例如 PBKDF2 或 scrypt。

  • 使用固定的 IV 错过了 IV 的全部要点。您需要为每条消息使用一个新的随机值。不是密文,直接放在密文前面就可以了

  • 如果没有 MAC,您将容易受到主动攻击,包括填充预言机。
  • 使用Stream.CopyTo在两个流之间复制数据。