You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

为何CryptoStream的输出会因明文输入方式不同而变化?

AES加密结果差异原因及正确实现

问题背景

使用System.Security.Cryptography.CryptoStream进行AES加密时,不同的明文传入方式会导致输出结果差异,以下是测试代码及输出。

测试代码

private Aes CreateAes()
{
   var aes = Aes.Create();

   aes.Mode = CipherMode.CBC;
   aes.Padding = PaddingMode.PKCS7;
   aes.Key = ...; // 此处为实际密钥
   aes.IV = ...;  // 此处为实际初始向量

   return aes;
}

var plaintext = "This is some plaintext. Isn't it nice?";
var plainbytes = Encoding.UTF8.GetBytes(plaintext);


// 方法1:使用StreamWriter写入字符串
using (var aes = CreateAes())
{
    using (var output = new MemoryStream())
    using (var encryptor = aes.CreateEncryptor())
    {
        using (var encryptingStream = new CryptoStream(output, encryptor, CryptoStreamMode.Write))
        using (var writer = new StreamWriter(encryptingStream))
        {
            writer.Write(plaintext);
            encryptingStream.FlushFinalBlock();
        }
        var ciphertext = output.ToArray();
        Console.WriteLine("Method 1: {0}", BitConverter.ToString(ciphertext));
    }
}

// 方法2:直接向CryptoStream写入字节数组
using (var aes = CreateAes())
{
    using (var output = new MemoryStream())
    using (var encryptor = aes.CreateEncryptor())
    {
        using (var encryptingStream = new CryptoStream(output, encryptor, CryptoStreamMode.Write))
        {
            encryptingStream.Write(plainbytes, 0, plainbytes.Length);
            encryptingStream.FlushFinalBlock();
        }
        var ciphertext = output.ToArray();
        Console.WriteLine("Method 2: {0}", BitConverter.ToString(ciphertext));
    }
}

// 方法3:复制已写入的输入流
using (var aes = CreateAes())
{
    using (var input = new MemoryStream())
    using (var writer = new StreamWriter(input))
    {
        writer.Write(plaintext);

        using (var output = new MemoryStream())
        using (var encryptor = aes.CreateEncryptor())
        {
            using (var encryptingStream = new CryptoStream(output, encryptor, CryptoStreamMode.Write))
            {
                input.CopyTo(encryptingStream);
                encryptingStream.FlushFinalBlock();
            }
            var ciphertext = output.ToArray();
            Console.WriteLine("Method 3: {0}", BitConverter.ToString(ciphertext));
        }
    }
}

// 方法4:复制字节包装的流
using (var aes = CreateAes())
{
    using (var input = new MemoryStream(plainbytes, writable: false))
    using (var output = new MemoryStream())
    using (var encryptor = aes.CreateEncryptor())
    {
        using (var encryptingStream = new CryptoStream(output, encryptor, CryptoStreamMode.Write))
        {
            input.CopyTo(encryptingStream);
            encryptingStream.FlushFinalBlock();
        }
        var ciphertext = output.ToArray();
        Console.WriteLine("Method 4: {0}", BitConverter.ToString(ciphertext));
    }
}

输出结果

Method 1: 4F-5A-EA-93-53-C4-6E-2B-7B-C1-DE-EF-C3-5E-37-9A-A1-94-FA-25-A8-B7-3A-F2-8F-5D-69-74-4E-BD-B3-C6-A9-B8-B7-37-7A-8D-20-B1-3E-9E-8C-D4-EE-F1-EC-BB

Method 2: A1-94-FA-25-A8-B7-3A-F2-8F-5D-69-74-4E-BD-B3-C6-A9-B8-B7-37-7A-8D-20-B1-3E-9E-8C-D4-EE-F1-EC-BB-F2-C9-39-B4-63-68-9B-0F-FF-75-80-4D-FC-18-5B-09

Method 3: 4F-5A-EA-93-53-C4-6E-2B-7B-C1-DE-EF-C3-5E-37-9A

Method 4: A1-94-FA-25-A8-B7-3A-F2-8F-5D-69-74-4E-BD-B3-C6-A9-B8-B7-37-7A-8D-20-B1-3E-9E-8C-D4-EE-F1-EC-BB-F2-C9-39-B4-63-68-9B-0F-FF-75-80-4D-FC-18-5B-09

差异原因分析

方法2和4结果一致的原因

这两个方法直接处理了原始明文的UTF-8字节数组,没有额外的缓冲区或流操作导致的数据丢失/冗余:

  • 方法2直接将plainbytes写入CryptoStream,数据完整传递给加密器。
  • 方法4用plainbytes初始化MemoryStream,再复制到CryptoStream,同样保证了数据完整且未被修改。

方法3结果异常的原因

方法3中,StreamWriter写入input流后未刷新缓冲区:

  • StreamWriter默认使用1024字节的缓冲区,当写入的字符数未填满缓冲区时,数据会暂存在StreamWriter的内存缓冲区中,不会立即写入底层的MemoryStream。
  • 此时调用input.CopyTo(encryptingStream),input流中没有实际数据,加密器仅处理空输入,根据PKCS7填充规则生成一个16字节的填充块(这就是方法3的输出)。

方法1结果异常的原因

方法1存在两个问题:

  1. 未提前刷新StreamWriter缓冲区:writer.Write(plaintext)后,数据仍在StreamWriter的缓冲区中,未写入CryptoStream。
  2. 重复调用FlushFinalBlock():手动调用encryptingStream.FlushFinalBlock()时,CryptoStream中无有效数据,生成一个16字节的填充块;之后StreamWriter被Dispose时自动刷新缓冲区,将明文字节写入CryptoStream,而CryptoStream在Dispose时会再次调用FlushFinalBlock()处理这些数据。
  • 最终输出是“空数据填充块 + 明文加密块”的组合,导致结果与正确加密结果(方法2/4)不符。

正确的加密实现

要保证加密结果正确,需满足以下两点:

  1. 确保明文数据完整传递给CryptoStream:如果使用StreamWriter,必须在调用FlushFinalBlock()前手动调用writer.Flush(),或依靠StreamWriter的Dispose自动刷新(但需避免提前调用FlushFinalBlock())。
  2. 仅调用一次FlushFinalBlock():CryptoStream的Dispose方法会自动调用FlushFinalBlock(),因此无需手动调用,或者手动调用后不再写入数据。

修正后的方法1示例:

using (var aes = CreateAes())
{
    using (var output = new MemoryStream())
    using (var encryptor = aes.CreateEncryptor())
    {
        using (var encryptingStream = new CryptoStream(output, encryptor, CryptoStreamMode.Write))
        using (var writer = new StreamWriter(encryptingStream))
        {
            writer.Write(plaintext);
            writer.Flush(); // 确保数据写入CryptoStream
            encryptingStream.FlushFinalBlock();
        }
        var ciphertext = output.ToArray();
        Console.WriteLine("修正后Method 1: {0}", BitConverter.ToString(ciphertext));
    }
}

或者更简洁的写法(依靠Dispose自动处理):

using (var aes = CreateAes())
using (var output = new MemoryStream())
using (var encryptor = aes.CreateEncryptor())
using (var encryptingStream = new CryptoStream(output, encryptor, CryptoStreamMode.Write))
using (var writer = new StreamWriter(encryptingStream))
{
    writer.Write(plaintext);
    // 无需手动调用FlushFinalBlock,CryptoStream.Dispose时会自动执行
}
var ciphertext = output.ToArray();

最推荐的是方法2或4的写法,直接操作字节数组,避免流包装带来的缓冲区问题,逻辑更清晰,不易出错。


内容的提问来源于stack exchange,提问作者David Rubin

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 03:23:11