AES解密失败报SafeHandle cannot be null错误求助
加密文件读取时
SafeHandle cannot be null错误修复方案 问题概述
实现了文件写入加密、读取解密的功能,写入过程无异常,但读取解密时抛出错误:SafeHandle cannot be null (parameter 'pHandle')。
调用代码
写入调用:
var manager = LogManager.CreateOrResumeManager(LogIndexByRowIndex, LogIndexByTimestamp, path, data, encryptionPassword, true);
读取调用:
using (var manager = LogManager.Load(encryptionPassword, filepath, testRunId, index)) { manager.TryReadRow(out data); }
核心错误分析
- Decryptor提前被释放:
Load方法中创建LogManager实例后立即调用manager.Close(),而Close方法会释放Decryptor对象。后续TryReadRow尝试使用已释放的Decryptor,就会触发SafeHandle cannot be null错误。 - 读取逻辑错误:
ReadRow方法将整个文件(包括未加密的日志头)当作加密数据解密,但实际文件结构是未加密的日志头 + 加密的行数据,直接解密整个文件会导致数据格式错误。 - 密钥生成不一致:
CreateEncryptor使用了1000次迭代的Rfc2898DeriveBytes,但GetDecryptor未指定迭代次数,虽然后续版本默认值也是1000,但显式统一更稳妥,避免因环境差异导致密钥不匹配。 - RecyclableMemoryStream使用不当:
ReadRow中调用buffer.GetBuffer()返回的是整个缓冲区(包含未使用的空字节),直接传入TransformFinalBlock会导致解密数据包含冗余内容,引发反序列化错误。
代码修复建议
1. 修复Load方法的Decryptor释放问题
移除Load方法中的manager.Close()调用,因为using块会自动调用Dispose(即Close),提前关闭会导致后续操作时Decryptor已失效:
public static LogManager Load(string password, string filePath, Guid testRunId, int rowIndex) { if (File.Exists(filePath) == false) { throw new FileNotFoundException("Log file does not exist"); } if (string.IsNullOrWhiteSpace(password)) { throw new Exception("Missing password"); } var logFileHeader = ReadLogHeader(filePath); if (logFileHeader.IV == null) { throw new Exception("Missing IV"); } var aes = GetDecryptor(password, logFileHeader.IV); var decryptor = aes.CreateDecryptor(); var manager = new LogManager(testRunId, rowIndex, filePath, logFileHeader) { Decryptor = decryptor }; // 移除这行代码:manager.Close(); return manager; }
2. 修正ReadRow的读取逻辑
跳过未加密的日志头,只解密后续的加密行数据:
protected virtual LogData ReadRow(BinaryReader reader, int length) { if (Decryptor != null) { // 先读取日志头,获取其长度,然后定位到加密数据的起始位置 reader.BaseStream.Position = 0; var logHeader = Serializer.DeserializeWithLengthPrefix<LogFileHeader>(reader.BaseStream, PrefixStyle.Fixed32); // 计算加密数据的起始位置和长度 long headerEndPosition = reader.BaseStream.Position; int encryptedDataLength = (int)(length - headerEndPosition); var encryptedData = reader.ReadBytes(encryptedDataLength); // 直接使用encryptedData,无需RecyclableMemoryStream(如需使用,应取实际数据而非缓冲区) var decrypted = Decryptor.TransformFinalBlock(encryptedData, 0, encryptedData.Length); return Serializer.Deserialize<LogData>(decrypted.AsSpan()); } else { return Serializer.Deserialize<LogData>(reader.BaseStream, length: length); } }
3. 统一密钥生成的迭代次数
修改GetDecryptor方法,显式指定迭代次数为1000,与CreateEncryptor保持一致:
public static Aes GetDecryptor(string password, byte[] iv) { using var rfc = new Rfc2898DeriveBytes(password, SaltBytes, 1000); // 添加1000迭代次数 var aesKey = rfc.GetBytes(16); var aes = Aes.Create(); aes.Padding = PaddingMode.PKCS7; aes.Key = aesKey; aes.IV = iv; return aes; }
4. 优化EncryptData方法的空值判断
当前EncryptData中如果encryptor为null会返回空数组,不符合逻辑,应直接返回原输入:
public static byte[] EncryptData(ICryptoTransform encryptor, byte[] input) { if (encryptor == null) return input; // 直接返回原数据,而非空数组 using var encryptedStream = new MemoryStream(); using var cs = new CryptoStream(encryptedStream, encryptor, CryptoStreamMode.Write); cs.Write(input, 0, input.Length); cs.FlushFinalBlock(); return encryptedStream.ToArray(); }
额外注意事项
- 确保
SaltBytes是固定且全局一致的常量,否则不同实例生成的密钥会不同。 - 日志头的序列化/反序列化要保证前后一致,避免因格式问题导致读取失败。
- 加密操作完成后,务必妥善释放
ICryptoTransform和Aes对象,避免资源泄漏。
内容的提问来源于stack exchange,提问作者Panda153
相关产品推荐
相关产品推荐

