Setup.FileExtractor.pas 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365
  1. unit Setup.FileExtractor;
  2. {
  3. Inno Setup
  4. Copyright (C) 1997-2025 Jordan Russell
  5. Portions by Martijn Laan
  6. For conditions of distribution and use, see LICENSE.TXT.
  7. TFileExtractor class to extract (=decrypt, decompress, and/or verify) Setup files
  8. }
  9. interface
  10. uses
  11. Windows, SysUtils, Shared.FileClass, Compression.Base,
  12. Shared.Struct, ChaCha20;
  13. type
  14. TExtractorProgressProc = procedure(const Bytes: Cardinal);
  15. TFileExtractor = class
  16. private
  17. FDecompressor: array[Boolean] of TCustomDecompressor;
  18. FSourceF: TFile;
  19. FOpenedSlice, FChunkFirstSlice, FChunkLastSlice: Integer;
  20. FChunkStartOffset: Int64;
  21. FChunkBytesLeft, FChunkDecompressedBytesRead: Int64;
  22. FNeedReset: Boolean;
  23. FChunkCompressed, FChunkEncrypted: Boolean;
  24. FCryptContext: TChaCha20Context;
  25. FCryptKey: TSetupEncryptionKey;
  26. FCryptKeySet: Boolean;
  27. FEntered: Integer;
  28. procedure DecompressBytes(var Buffer; Count: Cardinal);
  29. class function FindSliceFilename(const ASlice: Integer): String;
  30. procedure OpenSlice(const ASlice: Integer);
  31. function ReadProc(var Buf; Count: Longint): Longint;
  32. procedure SetCryptKey(const Value: TSetupEncryptionKey);
  33. public
  34. constructor Create(ADecompressorClass: TCustomDecompressorClass);
  35. destructor Destroy; override;
  36. procedure DecompressFile(const FL: TSetupFileLocationEntry; const DestF: TFile;
  37. const ProgressProc: TExtractorProgressProc; const VerifyChecksum: Boolean);
  38. procedure SeekTo(const FL: TSetupFileLocationEntry;
  39. const ProgressProc: TExtractorProgressProc);
  40. property CryptKey: TSetupEncryptionKey write SetCryptKey;
  41. end;
  42. function FileExtractor: TFileExtractor;
  43. procedure FreeFileExtractor;
  44. implementation
  45. uses
  46. PathFunc, Shared.CommonFunc, Setup.MainFunc, SetupLdrAndSetup.Messages,
  47. Shared.SetupMessageIDs, Setup.InstFunc, Compression.Zlib, Compression.bzlib,
  48. Compression.LZMADecompressor, SHA256, Setup.LoggingFunc, Setup.NewDiskForm;
  49. var
  50. FFileExtractor: TFileExtractor;
  51. function FileExtractor: TFileExtractor;
  52. const
  53. DecompClasses: array[TSetupCompressMethod] of TCustomDecompressorClass =
  54. (TStoredDecompressor, TZDecompressor, TBZDecompressor, TLZMA1Decompressor,
  55. TLZMA2Decompressor);
  56. begin
  57. if FFileExtractor = nil then
  58. FFileExtractor := TFileExtractor.Create(DecompClasses[SetupHeader.CompressMethod]);
  59. Result := FFileExtractor;
  60. end;
  61. procedure FreeFileExtractor;
  62. begin
  63. FreeAndNil(FFileExtractor);
  64. end;
  65. procedure SourceIsCorrupted(const AReason: String);
  66. begin
  67. Log('Source file corrupted: ' + AddPeriod(AReason));
  68. raise Exception.Create(SetupMessages[msgSourceIsCorrupted]);
  69. end;
  70. { TFileExtractor }
  71. constructor TFileExtractor.Create(ADecompressorClass: TCustomDecompressorClass);
  72. begin
  73. inherited Create;
  74. FOpenedSlice := -1;
  75. FChunkFirstSlice := -1;
  76. { Create one 'decompressor' for use with uncompressed chunks, and another
  77. for use with compressed chunks }
  78. FDecompressor[False] := TStoredDecompressor.Create(ReadProc);
  79. FDecompressor[True] := ADecompressorClass.Create(ReadProc);
  80. end;
  81. destructor TFileExtractor.Destroy;
  82. begin
  83. FSourceF.Free;
  84. FDecompressor[True].Free;
  85. FDecompressor[False].Free;
  86. inherited;
  87. end;
  88. procedure TFileExtractor.SetCryptKey(const Value: TSetupEncryptionKey);
  89. begin
  90. FCryptKey := Value;
  91. FCryptKeySet := True;
  92. end;
  93. var
  94. LastSourceDir: String;
  95. class function TFileExtractor.FindSliceFilename(const ASlice: Integer): String;
  96. var
  97. Major, Minor: Integer;
  98. Prefix, F1, Path: String;
  99. begin
  100. Prefix := PathChangeExt(PathExtractName(SetupLdrOriginalFilename), '');
  101. Major := ASlice div SetupHeader.SlicesPerDisk + 1;
  102. Minor := ASlice mod SetupHeader.SlicesPerDisk;
  103. if SetupHeader.SlicesPerDisk = 1 then
  104. F1 := Format('%s-%d.bin', [Prefix, Major])
  105. else
  106. F1 := Format('%s-%d%s.bin', [Prefix, Major, Chr(Ord('a') + Minor)]);
  107. if LastSourceDir <> '' then begin
  108. Result := AddBackslash(LastSourceDir) + F1;
  109. if NewFileExists(Result) then Exit;
  110. end;
  111. Result := AddBackslash(SourceDir) + F1;
  112. if NewFileExists(Result) then Exit;
  113. Path := SourceDir;
  114. LogFmt('Asking user for new disk containing "%s".', [F1]);
  115. if SelectDisk(Major, F1, Path) then begin
  116. LastSourceDir := Path;
  117. Result := AddBackslash(Path) + F1;
  118. end
  119. else
  120. Abort;
  121. end;
  122. procedure TFileExtractor.OpenSlice(const ASlice: Integer);
  123. var
  124. Filename: String;
  125. TestDiskSliceID: TDiskSliceID;
  126. DiskSliceHeader: TDiskSliceHeader;
  127. begin
  128. if FOpenedSlice = ASlice then
  129. Exit;
  130. FOpenedSlice := -1;
  131. FreeAndNil(FSourceF);
  132. if SetupLdrOffset1 = 0 then
  133. Filename := FindSliceFilename(ASlice)
  134. else
  135. Filename := SetupLdrOriginalFilename;
  136. FSourceF := TFile.Create(Filename, fdOpenExisting, faRead, fsRead);
  137. if SetupLdrOffset1 = 0 then begin
  138. if FSourceF.Read(TestDiskSliceID, SizeOf(TestDiskSliceID)) <> SizeOf(TestDiskSliceID) then
  139. SourceIsCorrupted('Invalid slice header (1)');
  140. if TestDiskSliceID <> DiskSliceID then
  141. SourceIsCorrupted('Invalid slice header (2)');
  142. if FSourceF.Read(DiskSliceHeader, SizeOf(DiskSliceHeader)) <> SizeOf(DiskSliceHeader) then
  143. SourceIsCorrupted('Invalid slice header (3)');
  144. if FSourceF.Size <> DiskSliceHeader.TotalSize then
  145. SourceIsCorrupted('Invalid slice header (4)');
  146. end;
  147. FOpenedSlice := ASlice;
  148. end;
  149. procedure TFileExtractor.DecompressBytes(var Buffer; Count: Cardinal);
  150. begin
  151. try
  152. FDecompressor[FChunkCompressed].DecompressInto(Buffer, Count);
  153. except
  154. { If DecompressInto raises an exception, force a decompressor reset &
  155. re-seek the next time SeekTo is called by setting FNeedReset to True.
  156. We don't want to get stuck in an endless loop with the decompressor
  157. in e.g. a data error state. Also, we have no way of knowing if
  158. DecompressInto successfully decompressed some of the requested bytes
  159. before the exception was raised. }
  160. FNeedReset := True;
  161. raise;
  162. end;
  163. Inc(FChunkDecompressedBytesRead, Count);
  164. end;
  165. procedure TFileExtractor.SeekTo(const FL: TSetupFileLocationEntry;
  166. const ProgressProc: TExtractorProgressProc);
  167. procedure InitDecryption;
  168. begin
  169. { Recreate the unique nonce from the base nonce }
  170. var Nonce := SetupEncryptionHeader.BaseNonce;
  171. Nonce.RandomXorStartOffset := Nonce.RandomXorStartOffset xor FChunkStartOffset;
  172. Nonce.RandomXorFirstSlice := Nonce.RandomXorFirstSlice xor FChunkFirstSlice;
  173. XChaCha20Init(FCryptContext, FCryptKey[0], Length(FCryptKey), Nonce, SizeOf(Nonce), 0);
  174. end;
  175. procedure Discard(Count: Int64);
  176. var
  177. Buf: array[0..65535] of Byte;
  178. begin
  179. try
  180. while Count > 0 do begin
  181. var BufSize: Cardinal := SizeOf(Buf);
  182. if Count < BufSize then
  183. BufSize := Cardinal(Count);
  184. DecompressBytes(Buf, BufSize);
  185. Dec(Count, BufSize);
  186. if Assigned(ProgressProc) then
  187. ProgressProc(0);
  188. end;
  189. except
  190. on E: ECompressDataError do
  191. SourceIsCorrupted(E.Message);
  192. end;
  193. end;
  194. var
  195. TestCompID: TCompID;
  196. begin
  197. if FEntered <> 0 then
  198. InternalError('Cannot call file extractor recursively');
  199. Inc(FEntered);
  200. try
  201. if (floChunkEncrypted in FL.Flags) and not FCryptKeySet then
  202. InternalError('Cannot read an encrypted file before the key has been set');
  203. { Is the file in a different chunk than the current one?
  204. Or, is the file in a part of the current chunk that we've already passed?
  205. Or, did a previous decompression operation fail, necessitating a reset? }
  206. if (FChunkFirstSlice <> FL.FirstSlice) or
  207. (FChunkStartOffset <> FL.StartOffset) or
  208. (FL.ChunkSuboffset < FChunkDecompressedBytesRead) or
  209. FNeedReset then begin
  210. FChunkFirstSlice := -1;
  211. FDecompressor[floChunkCompressed in FL.Flags].Reset;
  212. FNeedReset := False;
  213. OpenSlice(FL.FirstSlice);
  214. FSourceF.Seek(SetupLdrOffset1 + FL.StartOffset);
  215. if FSourceF.Read(TestCompID, SizeOf(TestCompID)) <> SizeOf(TestCompID) then
  216. SourceIsCorrupted('Failed to read CompID');
  217. if Longint(TestCompID) <> Longint(ZLIBID) then
  218. SourceIsCorrupted('Invalid CompID');
  219. FChunkFirstSlice := FL.FirstSlice;
  220. FChunkLastSlice := FL.LastSlice;
  221. FChunkStartOffset := FL.StartOffset;
  222. FChunkBytesLeft := FL.ChunkCompressedSize;
  223. FChunkDecompressedBytesRead := 0;
  224. FChunkCompressed := floChunkCompressed in FL.Flags;
  225. FChunkEncrypted := floChunkEncrypted in FL.Flags;
  226. if floChunkEncrypted in FL.Flags then
  227. InitDecryption;
  228. end;
  229. { Need to seek forward in the chunk? }
  230. if FL.ChunkSuboffset > FChunkDecompressedBytesRead then begin
  231. var Diff := FL.ChunkSuboffset;
  232. Dec(Diff, FChunkDecompressedBytesRead);
  233. Discard(Diff);
  234. end;
  235. finally
  236. Dec(FEntered);
  237. end;
  238. end;
  239. function TFileExtractor.ReadProc(var Buf; Count: Longint): Longint;
  240. var
  241. Buffer: Pointer;
  242. Left, Res: Cardinal;
  243. begin
  244. if (Count < 0) or (FChunkBytesLeft < 0) then { sanity checks }
  245. InternalError('TFileExtractor.ReadProc: Negative count');
  246. Buffer := @Buf;
  247. Left := Count;
  248. if FChunkBytesLeft < Left then
  249. Left := FChunkBytesLeft;
  250. Result := Left;
  251. while Left <> 0 do begin
  252. Res := FSourceF.Read(Buffer^, Left);
  253. Dec(FChunkBytesLeft, Res);
  254. { Decrypt the data after reading from the file }
  255. if FChunkEncrypted then
  256. XChaCha20Crypt(FCryptContext, Buffer^, Buffer^, Res);
  257. if Left = Res then
  258. Break
  259. else begin
  260. Dec(Left, Res);
  261. Inc(Longint(Buffer), Res);
  262. { Go to next disk }
  263. if FOpenedSlice >= FChunkLastSlice then
  264. { Already on the last slice, so the file must be corrupted... }
  265. SourceIsCorrupted('Already on last slice');
  266. OpenSlice(FOpenedSlice + 1);
  267. end;
  268. end;
  269. end;
  270. procedure TFileExtractor.DecompressFile(const FL: TSetupFileLocationEntry;
  271. const DestF: TFile; const ProgressProc: TExtractorProgressProc;
  272. const VerifyChecksum: Boolean);
  273. var
  274. Context: TSHA256Context;
  275. AddrOffset: LongWord;
  276. Buf: array[0..65535] of Byte;
  277. { ^ *must* be the same buffer size used by the compiler (TCompressionHandler),
  278. otherwise the TransformCallInstructions call will break }
  279. begin
  280. if FEntered <> 0 then
  281. InternalError('Cannot call file extractor recursively');
  282. Inc(FEntered);
  283. try
  284. var BytesLeft := FL.OriginalSize;
  285. if BytesLeft < 0 then { sanity check }
  286. InternalError('TFileExtractor.DecompressFile: Negative size');
  287. { To avoid file system fragmentation, preallocate all of the bytes in the
  288. destination file }
  289. DestF.Seek(BytesLeft);
  290. DestF.Truncate;
  291. DestF.Seek(0);
  292. SHA256Init(Context);
  293. try
  294. AddrOffset := 0;
  295. while BytesLeft > 0 do begin
  296. var BufSize: Cardinal := SizeOf(Buf);
  297. if BytesLeft < BufSize then
  298. BufSize := Cardinal(BytesLeft);
  299. DecompressBytes(Buf, BufSize);
  300. if floCallInstructionOptimized in FL.Flags then begin
  301. TransformCallInstructions(Buf, BufSize, False, AddrOffset);
  302. Inc(AddrOffset, BufSize); { may wrap, but OK }
  303. end;
  304. Dec(BytesLeft, BufSize);
  305. SHA256Update(Context, Buf, BufSize);
  306. DestF.WriteBuffer(Buf, BufSize);
  307. if Assigned(ProgressProc) then
  308. ProgressProc(BufSize);
  309. end;
  310. except
  311. on E: ECompressDataError do
  312. SourceIsCorrupted(E.Message);
  313. end;
  314. if VerifyChecksum and not SHA256DigestsEqual(SHA256Final(Context), FL.SHA256Sum) then
  315. SourceIsCorrupted('SHA-256 hash mismatch');
  316. finally
  317. Dec(FEntered);
  318. end;
  319. end;
  320. end.