The first part of this article describes a simple but limited way to read password encrypted zip files with a derived TZipFile class. The limitations are

it can only read but not create encrypted zip files the size of the zip files is limited by the available memory

While this solution does the job in the scenario it was built for, I cannot say that I was really satisfied with that. So I investigated ways how these limitations can be overcome.

It turned out that it was not that easy as I anticipated.

As we found out in the first part TZipFile exposes some hooks for us to intercept the reading and inject our encryption routine. For unknown reasons (at least to me) the corresponding hooks for writing the zip file are missing.

My first idea was to replace the compression handler with a routine similar to the decompression handler to add these hooks, but the underlying field holding these handlers (FCompressionHandler) is private. At least I could register an alternative pair of compression/decompression handler for the deflate method, even if I had to copy most of the original registration code. Luckily this allowed me to put my decompression hook directly into the code without using the CreateDecompressStreamCallBack – something that had already disturbed my feelings about the first implementation.

The new class constructor turns out to be a bit longer than the previous one where a large part is mostly just copied from the original class constructor.

class constructor TEncryptedZipFile.Create; begin RegisterCompressionHandler(zcDeflate, function(InStream: TStream; const ZipFile: TZipFile; const Item: TZipHeader): TStream var PItem: ^TZipHeader; LStream : TStream; LIsEncrypted: Boolean; begin LIsEncrypted := HasPassword(ZipFile); if LIsEncrypted then begin PItem := @Item; LStream := TEncryptStream.Create(InStream, TEncryptedZipFile(ZipFile).Password, Item); PItem.Flag := PItem.Flag or 1; end else LStream := InStream; Result := TZCompressionStream.Create(LStream, zcDefault, -15); end, function(InStream: TStream; const ZipFile: TZipFile; const Item: TZipHeader): TStream var LStream : TStream; LIsEncrypted: Boolean; begin // From https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT // Section 4.4.4 general purpose bit flag: (2 bytes) // Bit 0: If set, indicates that the file is encrypted. LIsEncrypted := (Item.Flag and 1) = 1; if Assigned(TZipFile.OnCreateDecompressStream) then LStream := TZipFile.OnCreateDecompressStream(InStream, ZipFile, Item, LIsEncrypted) else if Assigned(TZipFile.CreateDecompressStreamCallBack) then LStream := TZipFile.CreateDecompressStreamCallBack(InStream, ZipFile, Item, LIsEncrypted) else if LIsEncrypted and (ZipFile is TEncryptedZipFile) then LStream := TDecryptStream.Create(InStream, TEncryptedZipFile(ZipFile).Password, Item) else LStream := InStream; Result := TZDecompressionStream.Create(LStream, -15, LStream <> InStream); end); end; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 class constructor TEncryptedZipFile . Create ; begin RegisterCompressionHandler ( zcDeflate , function ( InStream : TStream ; const ZipFile : TZipFile ; const Item : TZipHeader ) : TStream var PItem : ^ TZipHeader ; LStream : TStream ; LIsEncrypted : Boolean ; begin LIsEncrypted : = HasPassword ( ZipFile ) ; if LIsEncrypted then begin PItem : = @Item ; LStream : = TEncryptStream . Create ( InStream , TEncryptedZipFile ( ZipFile ) . Password , Item ) ; PItem . Flag : = PItem . Flag or 1 ; end else LStream : = InStream ; Result : = TZCompressionStream . Create ( LStream , zcDefault , - 15 ) ; end , function ( InStream : TStream ; const ZipFile : TZipFile ; const Item : TZipHeader ) : TStream var LStream : TStream ; LIsEncrypted : Boolean ; begin // From https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT // Section 4.4.4 general purpose bit flag: (2 bytes) // Bit 0: If set, indicates that the file is encrypted. LIsEncrypted : = ( Item . Flag and 1 ) = 1 ; if Assigned ( TZipFile . OnCreateDecompressStream ) then LStream : = TZipFile . OnCreateDecompressStream ( InStream , ZipFile , Item , LIsEncrypted ) else if Assigned ( TZipFile . CreateDecompressStreamCallBack ) then LStream : = TZipFile . CreateDecompressStreamCallBack ( InStream , ZipFile , Item , LIsEncrypted ) else if LIsEncrypted and ( ZipFile is TEncryptedZipFile ) then LStream : = TDecryptStream . Create ( InStream , TEncryptedZipFile ( ZipFile ) . Password , Item ) else LStream : = InStream ; Result : = TZDecompressionStream . Create ( LStream , - 15 , LStream < > InStream ) ; end ) ; end ;

As some of you may have noticed there is a little hack needed to get the encryption working: the Item parameter is declared as constant, but we need to manipulate the Flag field to indicate encryption. To outsmart persuade the compiler to let us change the Flag field we make use of the fact that a const parameter of a record type is nothing else than a pointer.

After all this was not that difficult as it looked in the first place, so we can now move on to the implementation of the encryption stream. Referring to the original link of the PKWARE APPNOTE.TXT was a bit disappointing. The encryption algorithm was not explained in the same detail as the decryption steps. Seems I had to step back and try to understand the decryption sequence in more detail to deduce the encryption steps from it.

Having a TDecryptStream and TEncryptStream class sort of demanded a common ancestor class. In addition I extracted the plain decryption and encryption steps into a separate TCryptor class (one class – one purpose). This is the public interface:

type TCryptor = class ... public procedure InitKeys(const APassword: string); procedure DecryptByte(var Value: Byte); procedure EncryptByte(var Value: Byte); end; 1 2 3 4 5 6 7 8 type TCryptor = class . . . public procedure InitKeys ( const APassword : string ) ; procedure DecryptByte ( var Value : Byte ) ; procedure EncryptByte ( var Value : Byte ) ; end ;

The base crypto stream class looks like this:

type TCryptStream = class(TStream) private FCryptor: TCryptor; FPassword: string; FStream: TStream; FStreamStartPos: Int64; FStreamSize: Int64; FZipHeader: TZipHeader; protected procedure InitHeader; virtual; abstract; procedure InitKeys; procedure ResetStream; property Stream: TStream read FStream; property ZipHeader: TZipHeader read FZipHeader; public constructor Create(AStream: TStream; const APassword: string; const AZipHeader: TZipHeader); destructor Destroy; override; function Read(var Buffer; Count: Integer): Integer; override; function Seek(const Offset: Int64; Origin: TSeekOrigin): Int64; override; function Write(const Buffer; Count: Integer): Integer; override; end; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 type TCryptStream = class ( TStream ) private FCryptor : TCryptor ; FPassword : string ; FStream : TStream ; FStreamStartPos : Int64 ; FStreamSize : Int64 ; FZipHeader : TZipHeader ; protected procedure InitHeader ; virtual ; abstract ; procedure InitKeys ; procedure ResetStream ; property Stream : TStream read FStream ; property ZipHeader : TZipHeader read FZipHeader ; public constructor Create ( AStream : TStream ; const APassword : string ; const AZipHeader : TZipHeader ) ; destructor Destroy ; override ; function Read ( var Buffer ; Count : Integer ) : Integer ; override ; function Seek ( const Offset : Int64 ; Origin : TSeekOrigin ) : Int64 ; override ; function Write ( const Buffer ; Count : Integer ) : Integer ; override ; end ;

Both methods Read and Write just raise in EZipInvalidOperation exception. The TEncryptStream and TDecryptStream each override one of these methods leaving the other one raising that exception.

TDecryptStream = class(TCryptStream) protected procedure InitHeader; override; function Skip(Value: Int64): Int64; public function Read(var Buffer; Count: Integer): Integer; override; function Seek(const Offset: Int64; Origin: TSeekOrigin): Int64; override; end; TEncryptStream = class(TCryptStream) protected procedure InitHeader; override; public function Write(const Buffer; Count: Integer): Integer; override; end; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 TDecryptStream = class ( TCryptStream ) protected procedure InitHeader ; override ; function Skip ( Value : Int64 ) : Int64 ; public function Read ( var Buffer ; Count : Integer ) : Integer ; override ; function Seek ( const Offset : Int64 ; Origin : TSeekOrigin ) : Int64 ; override ; end ; TEncryptStream = class ( TCryptStream ) protected procedure InitHeader ; override ; public function Write ( const Buffer ; Count : Integer ) : Integer ; override ; end ;

One of the problems I encountered was the fact that the original TZipFile implementation uses Seek during decrytion to rewind the stream to a previous position. This didn’t any harm in the previous part as we used a memory stream there, but here we don’t have this luxury. Just moving the stream position will not work in this case because the decryption algorithm uses a state which is updated with each byte decrypted.

The current implementation uses a brute force approach and re-reads the whole stream from the beginning when a Seek wants to move backwards. This may be optimized for better performance in a future implementation.

I would like to note that things could be implemented much easier when done inside the original TZipFile class. Particularly the Seek operation would be one of the first I would try to eliminate. I might as well redesign the class var hooks approach into a more old fashioned virtual method or standard event approach (just more stress free in multi-thread scenarios). Perhaps someday we will see encryption being part of the stock implementation.

As before, the complete source can be downloaded from here: EncryptedZipFile

BTW, please notify me about any bugs and issues you may find.