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

使用Delphi的TZipFile与Crypto API解密AES-256加密Zip文件报错求助

Delphi中使用TZipFile和CryptoAPI解压AES-256加密Zip文件的错误修复

你的代码出现"坏数据"错误的核心原因是未遵循WinZip AES加密规范的关键步骤,以下是针对性的修改方案:

核心错误点

  1. 未读取并使用WinZip AES-256要求的16字节盐值
  2. 密码编码错误:使用WideString(Unicode)而非规范要求的UTF-8编码
  3. 密钥派生未执行1000次PBKDF2迭代
  4. 未设置AES的CBC模式和对应IV
  5. 未处理加密数据末尾的10字节认证码(用于验证密码正确性)

修改后的实现代码

1. 添加PBKDF2-HMAC-SHA1密钥派生函数

WinZip AES使用PBKDF2算法派生密钥,CryptoAPI无直接实现,需手动编写:

function PBKDF2_HMAC_SHA1(const Password: TBytes; const Salt: TBytes; Iterations: Integer; OutputSize: Integer): TBytes;
var
  hProv: HCRYPTPROV;
  hHash: HCRYPTHASH;
  I, J, K: Integer;
  U, T: TBytes;
  TempHash: array[0..19] of Byte; // SHA1哈希长度为20字节
  BytesRemaining: Integer;
begin
  SetLength(Result, OutputSize);
  BytesRemaining := OutputSize;

  if not CryptAcquireContext(@hProv, nil, nil, PROV_RSA_FULL, CRYPT_VERIFYCONTEXT) then
    RaiseLastOSError;
  try
    if not CryptCreateHash(hProv, CALG_HMAC, 0, 0, @hHash) then
      RaiseLastOSError;
    try
      // 设置HMAC密钥为密码字节
      if not CryptSetHashParam(hHash, HP_HMAC_INFO, @Password[0], Length(Password)) then
        RaiseLastOSError;

      K := 0;
      while BytesRemaining > 0 do
      begin
        Inc(K);
        // 构造盐值+迭代次数的大端序数据
        SetLength(U, Length(Salt) + 4);
        Move(Salt[0], U[0], Length(Salt));
        PInteger(@U[Length(Salt)])^ := ((K shr 24) and $FF) or ((K shr 8) and $FF00) or ((K shl 8) and $FF0000) or ((K shl 24) and $FF000000);

        // 计算U1
        CryptHashData(hHash, @U[0], Length(U), 0);
        CryptGetHashParam(hHash, HP_HASHVAL, @TempHash[0], SizeOf(TempHash), 0);
        CryptResetHash(hHash);
        SetLength(U, SizeOf(TempHash));
        Move(TempHash[0], U[0], SizeOf(TempHash));

        // 初始化T为U1
        SetLength(T, SizeOf(TempHash));
        Move(U[0], T[0], SizeOf(TempHash));

        // 迭代计算U2到Uc
        for I := 2 to Iterations do
        begin
          CryptHashData(hHash, @U[0], Length(U), 0);
          CryptGetHashParam(hHash, HP_HASHVAL, @TempHash[0], SizeOf(TempHash), 0);
          CryptResetHash(hHash);
          // T = T XOR Ui
          for J := 0 to Length(T)-1 do
            T[J] := T[J] xor TempHash[J];
          Move(TempHash[0], U[0], SizeOf(TempHash));
        end;

        // 将T写入结果缓冲区
        if BytesRemaining >= Length(T) then
        begin
          Move(T[0], Result[OutputSize - BytesRemaining], Length(T));
          Dec(BytesRemaining, Length(T));
        end
        else
        begin
          Move(T[0], Result[OutputSize - BytesRemaining], BytesRemaining);
          BytesRemaining := 0;
        end;
      end;
    finally
      CryptDestroyHash(hHash);
    end;
  finally
    CryptReleaseContext(hProv, 0);
  end;
end;

2. 修改AES解密流函数

function AESDecryptStream(InStream: TStream; const Password: string; FileSize: Integer): TStream;
const
  AES_256_KEY_SIZE = 32;
  AES_IV_SIZE = 16;
  AUTH_CODE_SIZE = 10;
  BUFFER_SIZE = 4096;
var
  Salt: array[0..15] of Byte;
  PasswordBytes: TBytes;
  DerivedBytes: TBytes;
  Key: array[0..31] of Byte;
  IV: array[0..15] of Byte;
  hProv: HCRYPTPROV;
  hKey: HCRYPTKEY;
  lpBuffer: PByte;
  ReadBytes, OutBytes: Integer;
  EncryptedDataSize: Integer;
  ReadTotal: Integer;
  AuthCode: array[0..9] of Byte;
begin
  // 读取16字节盐值(AES-256规范要求)
  if InStream.Read(Salt, SizeOf(Salt)) <> SizeOf(Salt) then
    raise Exception.Create('无法读取盐值');

  // 计算实际加密数据长度:总大小 - 盐值 - 认证码
  EncryptedDataSize := FileSize - SizeOf(Salt) - AUTH_CODE_SIZE;
  if EncryptedDataSize <= 0 then
    raise Exception.Create('无效的加密数据长度');

  // 读取末尾的10字节认证码(先跳转位置,读完加密数据后验证)
  InStream.Position := InStream.Position + EncryptedDataSize;
  if InStream.Read(AuthCode, SizeOf(AuthCode)) <> SizeOf(AuthCode) then
    raise Exception.Create('无法读取认证码');
  InStream.Position := SizeOf(Salt); // 回到加密数据起始位置

  // 转换密码为UTF-8字节(WinZip AES规范要求)
  PasswordBytes := TEncoding.UTF8.GetBytes(Password);
  try
    // 执行PBKDF2派生32字节密钥+16字节IV,1000次迭代
    DerivedBytes := PBKDF2_HMAC_SHA1(PasswordBytes, Salt, 1000, AES_256_KEY_SIZE + AES_IV_SIZE);
    try
      Move(DerivedBytes[0], Key, AES_256_KEY_SIZE);
      Move(DerivedBytes[AES_256_KEY_SIZE], IV, AES_IV_SIZE);

      // 获取CryptoAPI上下文
      if not CryptAcquireContext(@hProv, nil, nil, PROV_RSA_AES, CRYPT_VERIFYCONTEXT) then
        RaiseLastOSError;
      try
        // 导入AES-256密钥
        if not CryptImportKey(hProv, @Key, AES_256_KEY_SIZE, 0, 0, @hKey) then
          RaiseLastOSError;
        try
          // 设置AES为CBC模式并指定IV
          if not CryptSetKeyParam(hKey, KP_MODE, @CRYPT_MODE_CBC, 0) then
            RaiseLastOSError;
          if not CryptSetKeyParam(hKey, KP_IV, @IV, 0) then
            RaiseLastOSError;

          // 初始化输出流
          Result := TMemoryStream.Create;
          try
            lpBuffer := AllocMem(BUFFER_SIZE);
            try
              ReadTotal := 0;
              while ReadTotal < EncryptedDataSize do
              begin
                ReadBytes := Min(BUFFER_SIZE, EncryptedDataSize - ReadTotal);
                ReadBytes := InStream.Read(lpBuffer^, ReadBytes);
                if ReadBytes = 0 then
                  Break;

                OutBytes := ReadBytes;
                // 最后一块解密需设置FINAL标志(处理PKCS#7填充)
                if (ReadTotal + ReadBytes) >= EncryptedDataSize then
                begin
                  if not CryptDecrypt(hKey, 0, True, 0, lpBuffer, @OutBytes) then
                    RaiseLastOSError;
                end
                else
                begin
                  if not CryptDecrypt(hKey, 0, False, 0, lpBuffer, @OutBytes) then
                    RaiseLastOSError;
                end;

                Result.Write(lpBuffer^, OutBytes);
                Inc(ReadTotal, ReadBytes);
              end;

              // 可选:验证认证码(确保密码正确,避免乱码输出)
              // 此处可添加解密后数据的HMAC-SHA1计算,与AuthCode对比
            finally
              FreeMem(lpBuffer);
            end;
            Result.Position := 0;
          except
            Result.Free;
            raise;
          end;
        finally
          CryptDestroyKey(hKey);
        end;
      finally
        CryptReleaseContext(hProv, 0);
      end;
    finally
      SetLength(DerivedBytes, 0); // 清空敏感密钥数据
    end;
  finally
    SetLength(PasswordBytes, 0); // 清空敏感密码数据
  end;
end;

3. 修正加密标志判断

修改DecompressStream中的加密判断逻辑,适配WinZip AES的标志位:

function DecompressStream(InStream: TStream; const ZipFile: TZipFile; const Item: TZipHeader): TStream;
var
  LStream : TStream;
  isEncrypted: Boolean;
begin
  // WinZip AES加密使用$0008标志位,而非传统Zip的0x01
  isEncrypted := (Item.Flag and $0008) <> 0;

  if isEncrypted and (ZipFile is TEncryptedZipFile) and (Item.CompressionMethod = zcAESEncrypted) then
    LStream := AESDecryptStream(InStream, TEncryptedZipFile(ZipFile).Password, Item.CompressedSize )
  else
    LStream := InStream;
  Result := TZDecompressionStream.Create(LStream, -15, LStream <> InStream);
end;

关键修改说明

  • 盐值读取:WinZip AES-256每个加密文件开头固定16字节盐值,必须用于密钥派生,否则无法生成正确密钥
  • 密码编码:规范明确要求密码以UTF-8编码处理,之前的WideString会导致密钥派生错误
  • PBKDF2迭代:1000次迭代是WinZip AES的强制要求,直接哈希密码无法通过验证
  • CBC模式设置:WinZip AES使用CBC加密模式,必须指定对应的IV才能正确解密
  • 认证码处理:读取末尾的10字节认证码可用于验证密码正确性,避免因密码错误导致的乱码输出

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.27 08:57:01