为何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存在两个问题:
- 未提前刷新
StreamWriter缓冲区:writer.Write(plaintext)后,数据仍在StreamWriter的缓冲区中,未写入CryptoStream。 - 重复调用
FlushFinalBlock():手动调用encryptingStream.FlushFinalBlock()时,CryptoStream中无有效数据,生成一个16字节的填充块;之后StreamWriter被Dispose时自动刷新缓冲区,将明文字节写入CryptoStream,而CryptoStream在Dispose时会再次调用FlushFinalBlock()处理这些数据。
- 最终输出是“空数据填充块 + 明文加密块”的组合,导致结果与正确加密结果(方法2/4)不符。
正确的加密实现
要保证加密结果正确,需满足以下两点:
- 确保明文数据完整传递给
CryptoStream:如果使用StreamWriter,必须在调用FlushFinalBlock()前手动调用writer.Flush(),或依靠StreamWriter的Dispose自动刷新(但需避免提前调用FlushFinalBlock())。 - 仅调用一次
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
相关产品推荐
相关产品推荐

