C#复用CryptoStream批量加密时解密提示填充无效的问题排查
我尝试对大量独立值进行加密,每个值加密后需能解密还原为原始值。为避免重复创建CryptoStream,我选择复用现有流,但在解密单个值时遇到了无法解决的问题:抛出CryptographicException异常,提示“Padding is invalid and cannot be removed”。
我已验证每个读取的数据块都能被8字节整除,以适配所使用的AES加密器的128位块大小。请问我哪里出错了?
加密代码:
var results = new Dictionary<string, string>(); var aes = Aes.Create(); aes.Mode = CipherMode.CBC; aes.Padding = PaddingMode.PKCS7; var encryptor = aes.CreateEncryptor(Key, iv); using (var outputStream = new MemoryStream()) { using (var cryptoStream = new CryptoStream(outputStream, encryptor, CryptoStreamMode.Write)) { long writeStart = 0; foreach (var toEncrypt in allToEncrypt) { var bytesToEncrypt= Encoding.ASCII.GetBytes(toEncrypt); cryptoStream.Write(bytesToEncrypt, 0, bytesToEncrypt.Length); cryptoStream.Flush(); long count = outputStream.Position - writeStart; outputStream.Seek(writeStart, SeekOrigin.Begin); var encryptedBytes = new byte[(int)count]; outputStream.Read(encryptedBytes, 0, encryptedBytes.Length); results.Add(toEncrypt, formattedIV + EncryptionFormatter(encryptedBytes)); writeStart = outputStream.Position; } } }
编辑说明:
关于仅贴出加密代码但问题出在解密的疑问:实际是加密逻辑存在错误,导致解密时抛出异常。单值加密的正常逻辑如下(解密流程也基于此简单模式):
单值加密代码:
using (var outputStream = new MemoryStream()) { using (var cryptoStream = new CryptoStream(outputStream, encryptor, CryptoStreamMode.Write)) { var bytes = Encoding.ASCII.GetBytes(toEncrypt); cryptoStream.Write(bytes, 0, bytes.Length); cryptoStream.FlushFinalBlock(); } return EncryptionFormatter(iv, outputStream.ToArray()); }
单值解密代码:
using (var outputStream = new MemoryStream()) { using (var cryptoStream = new CryptoStream(outputStream, decryptor, CryptoStreamMode.Write)) { cryptoStream.Write(bytes, 0, bytes.Length); cryptoStream.FlushFinalBlock(); return DecryptionFormatter(outputStream.ToArray()); } }
核心问题分析
CBC模式的链式依赖破坏独立性
CBC模式下,每个加密块的输出会自动作为下一个加密块的IV。你复用同一个CryptoStream加密多个独立值,会导致后一个值的加密依赖前一个值的加密结果,而非初始IV。解密单个值时你用的是初始IV,但实际加密该值时的IV已经是前一个加密块的输出,直接导致解密逻辑不匹配。未正确处理PKCS7填充
单值加密时调用FlushFinalBlock()会自动添加PKCS7填充并完成最后一个块的加密,而你复用流时只调用了Flush()——这个方法仅刷新缓冲区,不会处理填充或完成当前块加密。你读取的加密字节只是不完整的中间数据,缺少必要的填充信息,解密时自然无法验证填充有效性。流缓冲导致的数据不完整
CryptoStream是缓冲式设计,调用Flush()后并非所有加密数据都会立即写入底层MemoryStream。你直接读取MemoryStream内容,大概率拿到的是不完整的加密片段。
修正方案
- 放弃复用CryptoStream:CBC模式下每个独立值的加密都需要独立的加密上下文(包括独立IV),复用流会彻底破坏加密的独立性。创建CryptoStream的开销极小,不会成为性能瓶颈。
- 为每个值生成独立IV:IV不需要保密,推荐为每个加密值生成新IV,并和密文一起存储,解密时用对应IV即可。
正确的批量加密示例:
var results = new Dictionary<string, string>(); var aes = Aes.Create(); aes.Mode = CipherMode.CBC; aes.Padding = PaddingMode.PKCS7; foreach (var toEncrypt in allToEncrypt) { // 为每个值生成独立IV aes.GenerateIV(); var iv = aes.IV; var encryptor = aes.CreateEncryptor(Key, iv); using (var outputStream = new MemoryStream()) { using (var cryptoStream = new CryptoStream(outputStream, encryptor, CryptoStreamMode.Write)) { var bytesToEncrypt = Encoding.ASCII.GetBytes(toEncrypt); cryptoStream.Write(bytesToEncrypt, 0, bytesToEncrypt.Length); cryptoStream.FlushFinalBlock(); } var encryptedBytes = outputStream.ToArray(); results.Add(toEncrypt, EncryptionFormatter(iv) + EncryptionFormatter(encryptedBytes)); } }
解密时只需从密文中提取对应IV,再用该IV创建解密器即可正常解密。
内容的提问来源于stack exchange,提问作者SpicyMike

