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

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);
}

核心错误分析

  1. Decryptor提前被释放:Load方法中创建LogManager实例后立即调用manager.Close(),而Close方法会释放Decryptor对象。后续TryReadRow尝试使用已释放的Decryptor,就会触发SafeHandle cannot be null错误。
  2. 读取逻辑错误:ReadRow方法将整个文件(包括未加密的日志头)当作加密数据解密,但实际文件结构是未加密的日志头 + 加密的行数据,直接解密整个文件会导致数据格式错误。
  3. 密钥生成不一致:CreateEncryptor使用了1000次迭代的Rfc2898DeriveBytes,但GetDecryptor未指定迭代次数,虽然后续版本默认值也是1000,但显式统一更稳妥,避免因环境差异导致密钥不匹配。
  4. 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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.07 18:24:52