370 lines
12 KiB
ObjectPascal
370 lines
12 KiB
ObjectPascal
unit Setup.FileExtractor;
|
|
|
|
{
|
|
Inno Setup
|
|
Copyright (C) 1997-2025 Jordan Russell
|
|
Portions by Martijn Laan
|
|
For conditions of distribution and use, see LICENSE.TXT.
|
|
|
|
TFileExtractor class to extract (=decrypt, decompress, and/or verify) Setup files
|
|
}
|
|
|
|
interface
|
|
|
|
uses
|
|
Windows, SysUtils, Shared.Int64Em, Shared.FileClass, Compression.Base,
|
|
Shared.Struct, ChaCha20;
|
|
|
|
type
|
|
TExtractorProgressProc = procedure(const Bytes: Cardinal);
|
|
|
|
TFileExtractor = class
|
|
private
|
|
FDecompressor: array[Boolean] of TCustomDecompressor;
|
|
FSourceF: TFile;
|
|
FOpenedSlice, FChunkFirstSlice, FChunkLastSlice: Integer;
|
|
FChunkStartOffset: Longint;
|
|
FChunkBytesLeft, FChunkDecompressedBytesRead: Integer64;
|
|
FNeedReset: Boolean;
|
|
FChunkCompressed, FChunkEncrypted: Boolean;
|
|
FCryptContext: TChaCha20Context;
|
|
FCryptKey: TSetupEncryptionKey;
|
|
FCryptKeySet: Boolean;
|
|
FEntered: Integer;
|
|
procedure DecompressBytes(var Buffer; Count: Cardinal);
|
|
class function FindSliceFilename(const ASlice: Integer): String;
|
|
procedure OpenSlice(const ASlice: Integer);
|
|
function ReadProc(var Buf; Count: Longint): Longint;
|
|
procedure SetCryptKey(const Value: TSetupEncryptionKey);
|
|
public
|
|
constructor Create(ADecompressorClass: TCustomDecompressorClass);
|
|
destructor Destroy; override;
|
|
procedure DecompressFile(const FL: TSetupFileLocationEntry; const DestF: TFile;
|
|
const ProgressProc: TExtractorProgressProc; const VerifyChecksum: Boolean);
|
|
procedure SeekTo(const FL: TSetupFileLocationEntry;
|
|
const ProgressProc: TExtractorProgressProc);
|
|
property CryptKey: TSetupEncryptionKey write SetCryptKey;
|
|
end;
|
|
|
|
function FileExtractor: TFileExtractor;
|
|
procedure FreeFileExtractor;
|
|
|
|
implementation
|
|
|
|
uses
|
|
PathFunc, Shared.CommonFunc, Setup.MainFunc, SetupLdrAndSetup.Messages,
|
|
Shared.SetupMessageIDs, Setup.InstFunc, Compression.Zlib, Compression.bzlib,
|
|
Compression.LZMADecompressor, SHA256, Setup.LoggingFunc, Setup.NewDiskForm;
|
|
|
|
var
|
|
FFileExtractor: TFileExtractor;
|
|
|
|
function FileExtractor: TFileExtractor;
|
|
const
|
|
DecompClasses: array[TSetupCompressMethod] of TCustomDecompressorClass =
|
|
(TStoredDecompressor, TZDecompressor, TBZDecompressor, TLZMA1Decompressor,
|
|
TLZMA2Decompressor);
|
|
begin
|
|
if FFileExtractor = nil then
|
|
FFileExtractor := TFileExtractor.Create(DecompClasses[SetupHeader.CompressMethod]);
|
|
Result := FFileExtractor;
|
|
end;
|
|
|
|
procedure FreeFileExtractor;
|
|
begin
|
|
FreeAndNil(FFileExtractor);
|
|
end;
|
|
|
|
procedure SourceIsCorrupted(const AReason: String);
|
|
begin
|
|
Log('Source file corrupted: ' + AddPeriod(AReason));
|
|
raise Exception.Create(SetupMessages[msgSourceIsCorrupted]);
|
|
end;
|
|
|
|
{ TFileExtractor }
|
|
|
|
constructor TFileExtractor.Create(ADecompressorClass: TCustomDecompressorClass);
|
|
begin
|
|
inherited Create;
|
|
FOpenedSlice := -1;
|
|
FChunkFirstSlice := -1;
|
|
{ Create one 'decompressor' for use with uncompressed chunks, and another
|
|
for use with compressed chunks }
|
|
FDecompressor[False] := TStoredDecompressor.Create(ReadProc);
|
|
FDecompressor[True] := ADecompressorClass.Create(ReadProc);
|
|
end;
|
|
|
|
destructor TFileExtractor.Destroy;
|
|
begin
|
|
FSourceF.Free;
|
|
FDecompressor[True].Free;
|
|
FDecompressor[False].Free;
|
|
inherited;
|
|
end;
|
|
|
|
procedure TFileExtractor.SetCryptKey(const Value: TSetupEncryptionKey);
|
|
begin
|
|
FCryptKey := Value;
|
|
FCryptKeySet := True;
|
|
end;
|
|
|
|
var
|
|
LastSourceDir: String;
|
|
|
|
class function TFileExtractor.FindSliceFilename(const ASlice: Integer): String;
|
|
var
|
|
Major, Minor: Integer;
|
|
Prefix, F1, Path: String;
|
|
begin
|
|
Prefix := PathChangeExt(PathExtractName(SetupLdrOriginalFilename), '');
|
|
Major := ASlice div SetupHeader.SlicesPerDisk + 1;
|
|
Minor := ASlice mod SetupHeader.SlicesPerDisk;
|
|
if SetupHeader.SlicesPerDisk = 1 then
|
|
F1 := Format('%s-%d.bin', [Prefix, Major])
|
|
else
|
|
F1 := Format('%s-%d%s.bin', [Prefix, Major, Chr(Ord('a') + Minor)]);
|
|
if LastSourceDir <> '' then begin
|
|
Result := AddBackslash(LastSourceDir) + F1;
|
|
if NewFileExists(Result) then Exit;
|
|
end;
|
|
Result := AddBackslash(SourceDir) + F1;
|
|
if NewFileExists(Result) then Exit;
|
|
Path := SourceDir;
|
|
LogFmt('Asking user for new disk containing "%s".', [F1]);
|
|
if SelectDisk(Major, F1, Path) then begin
|
|
LastSourceDir := Path;
|
|
Result := AddBackslash(Path) + F1;
|
|
end
|
|
else
|
|
Abort;
|
|
end;
|
|
|
|
procedure TFileExtractor.OpenSlice(const ASlice: Integer);
|
|
var
|
|
Filename: String;
|
|
TestDiskSliceID: TDiskSliceID;
|
|
DiskSliceHeader: TDiskSliceHeader;
|
|
begin
|
|
if FOpenedSlice = ASlice then
|
|
Exit;
|
|
|
|
FOpenedSlice := -1;
|
|
FreeAndNil(FSourceF);
|
|
|
|
if SetupLdrOffset1 = 0 then
|
|
Filename := FindSliceFilename(ASlice)
|
|
else
|
|
Filename := SetupLdrOriginalFilename;
|
|
FSourceF := TFile.Create(Filename, fdOpenExisting, faRead, fsRead);
|
|
if SetupLdrOffset1 = 0 then begin
|
|
if FSourceF.Read(TestDiskSliceID, SizeOf(TestDiskSliceID)) <> SizeOf(TestDiskSliceID) then
|
|
SourceIsCorrupted('Invalid slice header (1)');
|
|
if TestDiskSliceID <> DiskSliceID then
|
|
SourceIsCorrupted('Invalid slice header (2)');
|
|
if FSourceF.Read(DiskSliceHeader, SizeOf(DiskSliceHeader)) <> SizeOf(DiskSliceHeader) then
|
|
SourceIsCorrupted('Invalid slice header (3)');
|
|
if FSourceF.Size.Lo <> DiskSliceHeader.TotalSize then
|
|
SourceIsCorrupted('Invalid slice header (4)');
|
|
end;
|
|
FOpenedSlice := ASlice;
|
|
end;
|
|
|
|
procedure TFileExtractor.DecompressBytes(var Buffer; Count: Cardinal);
|
|
begin
|
|
try
|
|
FDecompressor[FChunkCompressed].DecompressInto(Buffer, Count);
|
|
except
|
|
{ If DecompressInto raises an exception, force a decompressor reset &
|
|
re-seek the next time SeekTo is called by setting FNeedReset to True.
|
|
We don't want to get stuck in an endless loop with the decompressor
|
|
in e.g. a data error state. Also, we have no way of knowing if
|
|
DecompressInto successfully decompressed some of the requested bytes
|
|
before the exception was raised. }
|
|
FNeedReset := True;
|
|
raise;
|
|
end;
|
|
Inc64(FChunkDecompressedBytesRead, Count);
|
|
end;
|
|
|
|
procedure TFileExtractor.SeekTo(const FL: TSetupFileLocationEntry;
|
|
const ProgressProc: TExtractorProgressProc);
|
|
|
|
procedure InitDecryption;
|
|
begin
|
|
{ Recreate the unique nonce from the base nonce }
|
|
var Nonce := SetupHeader.EncryptionBaseNonce;
|
|
Nonce.RandomXorStartOffset := Nonce.RandomXorStartOffset xor FChunkStartOffset;
|
|
Nonce.RandomXorFirstSlice := Nonce.RandomXorFirstSlice xor FChunkFirstSlice;
|
|
|
|
XChaCha20Init(FCryptContext, FCryptKey[0], Length(FCryptKey), Nonce, SizeOf(Nonce), 0);
|
|
end;
|
|
|
|
procedure Discard(Count: Integer64);
|
|
var
|
|
Buf: array[0..65535] of Byte;
|
|
BufSize: Cardinal;
|
|
begin
|
|
try
|
|
while True do begin
|
|
BufSize := SizeOf(Buf);
|
|
if (Count.Hi = 0) and (Count.Lo < BufSize) then
|
|
BufSize := Count.Lo;
|
|
if BufSize = 0 then
|
|
Break;
|
|
DecompressBytes(Buf, BufSize);
|
|
Dec64(Count, BufSize);
|
|
if Assigned(ProgressProc) then
|
|
ProgressProc(0);
|
|
end;
|
|
except
|
|
on E: ECompressDataError do
|
|
SourceIsCorrupted(E.Message);
|
|
end;
|
|
end;
|
|
|
|
var
|
|
TestCompID: TCompID;
|
|
Diff: Integer64;
|
|
begin
|
|
if FEntered <> 0 then
|
|
InternalError('Cannot call file extractor recursively');
|
|
Inc(FEntered);
|
|
try
|
|
if (floChunkEncrypted in FL.Flags) and not FCryptKeySet then
|
|
InternalError('Cannot read an encrypted file before the key has been set');
|
|
|
|
{ Is the file in a different chunk than the current one?
|
|
Or, is the file in a part of the current chunk that we've already passed?
|
|
Or, did a previous decompression operation fail, necessitating a reset? }
|
|
if (FChunkFirstSlice <> FL.FirstSlice) or
|
|
(FChunkStartOffset <> FL.StartOffset) or
|
|
(Compare64(FL.ChunkSuboffset, FChunkDecompressedBytesRead) < 0) or
|
|
FNeedReset then begin
|
|
FChunkFirstSlice := -1;
|
|
FDecompressor[floChunkCompressed in FL.Flags].Reset;
|
|
FNeedReset := False;
|
|
|
|
OpenSlice(FL.FirstSlice);
|
|
|
|
FSourceF.Seek(SetupLdrOffset1 + FL.StartOffset);
|
|
if FSourceF.Read(TestCompID, SizeOf(TestCompID)) <> SizeOf(TestCompID) then
|
|
SourceIsCorrupted('Failed to read CompID');
|
|
if Longint(TestCompID) <> Longint(ZLIBID) then
|
|
SourceIsCorrupted('Invalid CompID');
|
|
|
|
FChunkFirstSlice := FL.FirstSlice;
|
|
FChunkLastSlice := FL.LastSlice;
|
|
FChunkStartOffset := FL.StartOffset;
|
|
FChunkBytesLeft := FL.ChunkCompressedSize;
|
|
FChunkDecompressedBytesRead := To64(0);
|
|
FChunkCompressed := floChunkCompressed in FL.Flags;
|
|
FChunkEncrypted := floChunkEncrypted in FL.Flags;
|
|
|
|
if floChunkEncrypted in FL.Flags then
|
|
InitDecryption;
|
|
end;
|
|
|
|
{ Need to seek forward in the chunk? }
|
|
if Compare64(FL.ChunkSuboffset, FChunkDecompressedBytesRead) > 0 then begin
|
|
Diff := FL.ChunkSuboffset;
|
|
Dec6464(Diff, FChunkDecompressedBytesRead);
|
|
Discard(Diff);
|
|
end;
|
|
finally
|
|
Dec(FEntered);
|
|
end;
|
|
end;
|
|
|
|
function TFileExtractor.ReadProc(var Buf; Count: Longint): Longint;
|
|
var
|
|
Buffer: Pointer;
|
|
Left, Res: Cardinal;
|
|
begin
|
|
Buffer := @Buf;
|
|
Left := Count;
|
|
if (FChunkBytesLeft.Hi = 0) and (FChunkBytesLeft.Lo < Left) then
|
|
Left := FChunkBytesLeft.Lo;
|
|
Result := Left;
|
|
while Left <> 0 do begin
|
|
Res := FSourceF.Read(Buffer^, Left);
|
|
Dec64(FChunkBytesLeft, Res);
|
|
|
|
{ Decrypt the data after reading from the file }
|
|
if FChunkEncrypted then
|
|
XChaCha20Crypt(FCryptContext, Buffer^, Buffer^, Res);
|
|
|
|
if Left = Res then
|
|
Break
|
|
else begin
|
|
Dec(Left, Res);
|
|
Inc(Longint(Buffer), Res);
|
|
{ Go to next disk }
|
|
if FOpenedSlice >= FChunkLastSlice then
|
|
{ Already on the last slice, so the file must be corrupted... }
|
|
SourceIsCorrupted('Already on last slice');
|
|
OpenSlice(FOpenedSlice + 1);
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
procedure TFileExtractor.DecompressFile(const FL: TSetupFileLocationEntry;
|
|
const DestF: TFile; const ProgressProc: TExtractorProgressProc;
|
|
const VerifyChecksum: Boolean);
|
|
var
|
|
BytesLeft: Integer64;
|
|
Context: TSHA256Context;
|
|
AddrOffset: LongWord;
|
|
BufSize: Cardinal;
|
|
Buf: array[0..65535] of Byte;
|
|
{ ^ *must* be the same buffer size used by the compiler (TCompressionHandler),
|
|
otherwise the TransformCallInstructions call will break }
|
|
begin
|
|
if FEntered <> 0 then
|
|
InternalError('Cannot call file extractor recursively');
|
|
Inc(FEntered);
|
|
try
|
|
BytesLeft := FL.OriginalSize;
|
|
|
|
{ To avoid file system fragmentation, preallocate all of the bytes in the
|
|
destination file }
|
|
DestF.Seek64(BytesLeft);
|
|
DestF.Truncate;
|
|
DestF.Seek(0);
|
|
|
|
SHA256Init(Context);
|
|
|
|
try
|
|
AddrOffset := 0;
|
|
while True do begin
|
|
BufSize := SizeOf(Buf);
|
|
if (BytesLeft.Hi = 0) and (BytesLeft.Lo < BufSize) then
|
|
BufSize := BytesLeft.Lo;
|
|
if BufSize = 0 then
|
|
Break;
|
|
|
|
DecompressBytes(Buf, BufSize);
|
|
if floCallInstructionOptimized in FL.Flags then begin
|
|
TransformCallInstructions(Buf, BufSize, False, AddrOffset);
|
|
Inc(AddrOffset, BufSize); { may wrap, but OK }
|
|
end;
|
|
Dec64(BytesLeft, BufSize);
|
|
SHA256Update(Context, Buf, BufSize);
|
|
DestF.WriteBuffer(Buf, BufSize);
|
|
|
|
if Assigned(ProgressProc) then
|
|
ProgressProc(BufSize);
|
|
end;
|
|
except
|
|
on E: ECompressDataError do
|
|
SourceIsCorrupted(E.Message);
|
|
end;
|
|
|
|
if VerifyChecksum and not SHA256DigestsEqual(SHA256Final(Context), FL.SHA256Sum) then
|
|
SourceIsCorrupted('SHA-256 hash mismatch');
|
|
finally
|
|
Dec(FEntered);
|
|
end;
|
|
end;
|
|
|
|
end.
|