使用Delphi的TZipFile与Crypto API解密AES-256加密Zip文件报错求助
Delphi中使用TZipFile和CryptoAPI解压AES-256加密Zip文件的错误修复
你的代码出现"坏数据"错误的核心原因是未遵循WinZip AES加密规范的关键步骤,以下是针对性的修改方案:
核心错误点
- 未读取并使用WinZip AES-256要求的16字节盐值
- 密码编码错误:使用WideString(Unicode)而非规范要求的UTF-8编码
- 密钥派生未执行1000次PBKDF2迭代
- 未设置AES的CBC模式和对应IV
- 未处理加密数据末尾的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
相关产品推荐
相关产品推荐

