Browse Source

Merge branch 'fullencryption'

Martijn Laan 1 month ago
parent
commit
a0ef57a386

+ 6 - 3
ISHelp/isetup.xml

@@ -5833,12 +5833,16 @@ DiskSliceSize=1457664
 </setuptopic>
 
 <setuptopic directive="Encryption">
-<setupvalid><link topic="yesnonotes"><tt>yes</tt> or <tt>no</tt></link></setupvalid>
+<setupvalid><link topic="yesnonotes"><tt>yes</tt> or <tt>no</tt></link> or <tt>full</tt></setupvalid>
 <setupdefault><tt>no</tt></setupdefault>
 <body>
 <p>If set to <tt>yes</tt>, files that are compiled into the installation (via [Files] section entries) will be encrypted using XChaCha20 encryption.</p>
 <p>If encryption is enabled and you call the <link topic="isxfunc_ExtractTemporaryFile">ExtractTemporaryFile</link> function from the [Code] section prior to the user entering the correct password, the function will fail unless the <tt>noencryption</tt> flag is used on the [Files] section entry for the file.</p>
 <p>The 256-bit XChaCha20 encryption key is derived from the value of <link topic="setup_password">Password</link> using the function specified by <link topic="setup_encryptionkeyderivation">EncryptionKeyDerivation</link>, and the 192-bit XChaCha20 encryption nonce is a random base nonce, appending the index of the first file in the chunk for unique encryption nonces.</p>
+<p>If set to <tt>full</tt>, the entire installation will be encrypted, and the password must be specified using the <link topic="setupcmdline" anchor="PASSWORD">/PASSWORD=</link> command line parameter.</p>
+<p><b>See also:</b><br/>
+<link topic="setup_password">Password</link><br/>
+<link topic="setup_encryptionkeyderivation">EncryptionKeyDerivation</link></p>
 </body>
 </setuptopic>
 
@@ -5851,8 +5855,7 @@ DiskSliceSize=1457664
 <p>This specifies the key derivation function to use to derive the encryption key from the value of <link topic="setup_password">Password</link>, and optionally its parameters.</p>
 <p><tt>pbkdf2</tt> is the PBKDF2-HMAC-SHA256 function with a 128-bit random salt, and optionally allows to increase its number of iterations for extra security. If the number of iterations isn't specified, it defaults to 220000.</p>
 <p><b>See also:</b><br/>
-<link topic="setup_encryption">Encryption</link>
-</p>
+<link topic="setup_encryption">Encryption</link></p>
 </body>
 </setuptopic>
 

+ 2 - 1
Projects/ISCmplr.dpr

@@ -52,7 +52,8 @@ uses
   PBKDF2 in '..\Components\PBKDF2.pas',
   ECDSA in '..\Components\ECDSA.pas',
   ISSigFunc in '..\Components\ISSigFunc.pas',
-  StringScanner in '..\Components\StringScanner.pas';
+  StringScanner in '..\Components\StringScanner.pas',
+  Shared.EncryptionFunc in 'Src\Shared.EncryptionFunc.pas';
 
 {$IMAGEBASE $00800000}
 {$SETPEOSVERSION 6.1}

+ 1 - 0
Projects/ISCmplr.dproj

@@ -121,6 +121,7 @@
         <DCCReference Include="..\Components\ECDSA.pas"/>
         <DCCReference Include="..\Components\ISSigFunc.pas"/>
         <DCCReference Include="..\Components\StringScanner.pas"/>
+        <DCCReference Include="Src\Shared.EncryptionFunc.pas"/>
         <BuildConfiguration Include="Base">
             <Key>Base</Key>
         </BuildConfiguration>

+ 2 - 1
Projects/Setup.dpr

@@ -101,7 +101,8 @@ uses
   ISSigFunc in '..\Components\ISSigFunc.pas',
   StringScanner in '..\Components\StringScanner.pas',
   Compression.SevenZipDLLDecoder in 'Src\Compression.SevenZipDLLDecoder.pas',
-  Compression.SevenZipDLLDecoder.Interfaces in 'Src\Compression.SevenZipDLLDecoder.Interfaces.pas';
+  Compression.SevenZipDLLDecoder.Interfaces in 'Src\Compression.SevenZipDLLDecoder.Interfaces.pas',
+  Shared.EncryptionFunc in 'Src\Shared.EncryptionFunc.pas';
 
 {$SETPEOSVERSION 6.1}
 {$SETPESUBSYSVERSION 6.1}

+ 1 - 0
Projects/Setup.dproj

@@ -175,6 +175,7 @@
         <DCCReference Include="..\Components\StringScanner.pas"/>
         <DCCReference Include="Src\Compression.SevenZipDLLDecoder.pas"/>
         <DCCReference Include="Src\Compression.SevenZipDLLDecoder.Interfaces.pas"/>
+        <DCCReference Include="Src\Shared.EncryptionFunc.pas"/>
         <BuildConfiguration Include="Base">
             <Key>Base</Key>
         </BuildConfiguration>

+ 28 - 3
Projects/SetupLdr.dpr

@@ -2,7 +2,7 @@ program SetupLdr;
 
 {
   Inno Setup
-  Copyright (C) 1997-2024 Jordan Russell
+  Copyright (C) 1997-2025 Jordan Russell
   Portions by Martijn Laan
   For conditions of distribution and use, see LICENSE.TXT.
 
@@ -28,7 +28,10 @@ uses
   Shared.Int64Em in 'Src\Shared.Int64Em.pas',
   SHA256 in '..\Components\SHA256.pas',
   SetupLdrAndSetup.RedirFunc in 'Src\SetupLdrAndSetup.RedirFunc.pas',
-  Shared.VerInfoFunc in 'Src\Shared.VerInfoFunc.pas';
+  Shared.VerInfoFunc in 'Src\Shared.VerInfoFunc.pas',
+  Shared.EncryptionFunc in 'Src\Shared.EncryptionFunc.pas',
+  ChaCha20 in '..\Components\ChaCha20.pas',
+  PBKDF2 in '..\Components\PBKDF2.pas';
 
 {$SETPEOSVERSION 6.1}
 {$SETPESUBSYSVERSION 6.1}
@@ -73,9 +76,10 @@ type
 var
   InitShowHelp: Boolean = False;
   InitDisableStartupPrompt: Boolean = False;
-  InitLang: String;
+  InitLang, InitPassword: String;
   ActiveLanguage: Integer = -1;
   PendingNewLanguage: Integer = -1;
+  SetupEncryptionHeader: TSetupEncryptionHeader;
   SetupHeader: TSetupHeader;
   LanguageEntries: PLanguageEntryArray;
   LanguageEntryCount: Integer;
@@ -96,6 +100,8 @@ begin
       InitDisableStartupPrompt := True
     else if CompareText(Copy(Name, 1, 6), '/Lang=') = 0 then
       InitLang := Copy(Name, 7, Maxint)
+    else if CompareText(Copy(Name, 1, 10), '/Password=') = 0 then
+      InitPassword := Copy(Name, 11, Maxint)
     else if (CompareText(Name, '/HELP') = 0) or
             (CompareText(Name, '/?') = 0) then
       InitShowHelp := True;
@@ -403,9 +409,28 @@ begin
       SourceF.ReadBuffer(TestID, SizeOf(TestID));
       if TestID <> SetupID then
         SetupCorruptError;
+
+      var SetupEncryptionHeaderCRC: Longint;
+      SourceF.Read(SetupEncryptionHeaderCRC, SizeOf(SetupEncryptionHeaderCRC));
+      SourceF.Read(SetupEncryptionHeader, SizeOf(SetupEncryptionHeader));
+      if SetupEncryptionHeaderCRC <> GetCRC32(SetupEncryptionHeader, SizeOf(SetupEncryptionHeader)) then
+         SetupCorruptError;
+
+      var CryptKey: TSetupEncryptionKey;
+      if SetupEncryptionHeader.EncryptionUse = euFull then begin
+        if InitPassword = '' then
+          raise Exception.Create(SMissingPassword);
+        GenerateEncryptionKey(InitPassword, SetupEncryptionHeader.KDFSalt, SetupEncryptionHeader.KDFIterations, CryptKey);
+        if not TestPassword(CryptKey, SetupEncryptionHeader.BaseNonce, SetupEncryptionHeader.PasswordTest) then
+          raise Exception.Create(SIncorrectPassword);
+      end;
+
       try
         Reader := TCompressedBlockReader.Create(SourceF, TLZMA1SmallDecompressor);
         try
+          if SetupEncryptionHeader.EncryptionUse = euFull then
+            Reader.InitDecryption(CryptKey, SetupEncryptionHeader.BaseNonce, sccCompressedBlocks1);
+
           SECompressedBlockRead(Reader, SetupHeader, SizeOf(SetupHeader),
             SetupHeaderStrings, SetupHeaderAnsiStrings);
 

+ 3 - 0
Projects/SetupLdr.dproj

@@ -88,6 +88,9 @@
         <DCCReference Include="..\Components\SHA256.pas"/>
         <DCCReference Include="Src\SetupLdrAndSetup.RedirFunc.pas"/>
         <DCCReference Include="Src\Shared.VerInfoFunc.pas"/>
+        <DCCReference Include="Src\Shared.EncryptionFunc.pas"/>
+        <DCCReference Include="..\Components\ChaCha20.pas"/>
+        <DCCReference Include="..\Components\PBKDF2.pas"/>
         <BuildConfiguration Include="Base">
             <Key>Base</Key>
         </BuildConfiguration>

+ 2 - 12
Projects/Src/Compiler.CompressionHandler.pas

@@ -62,7 +62,7 @@ type
 implementation
 
 uses
-  SysUtils, Compiler.Messages, Compiler.HelperFunc;
+  SysUtils, Shared.EncryptionFunc, Compiler.Messages, Compiler.HelperFunc;
 
 constructor TCompressionHandler.Create(ACompiler: TSetupCompiler;
   const InitialSliceFilename: String);
@@ -189,16 +189,6 @@ procedure TCompressionHandler.NewChunk(const ACompressorClass: TCustomCompressor
     end;
   end;
 
-  procedure InitEncryption;
-  begin
-    { Create a unique nonce from the base nonce }
-    var Nonce := FCompiler.GetEncryptionBaseNonce;
-    Nonce.RandomXorStartOffset := Nonce.RandomXorStartOffset xor FChunkStartOffset;
-    Nonce.RandomXorFirstSlice := Nonce.RandomXorFirstSlice xor FChunkFirstSlice;
-
-    XChaCha20Init(FCryptContext, ACryptKey[0], Length(ACryptKey), Nonce, SizeOf(Nonce), 0);
-  end;
-
 var
   MinBytesLeft: Cardinal;
 begin
@@ -223,7 +213,7 @@ begin
 
   FChunkEncrypted := AUseEncryption;
   if AUseEncryption then
-    InitEncryption;
+    InitCryptContext(ACryptKey, FCompiler.GetEncryptionBaseNonce, FChunkStartOffset, FChunkFirstSlice, FCryptContext);
 
   FChunkStarted := True;
 end;

+ 29 - 27
Projects/Src/Compiler.SetupCompiler.pas

@@ -127,6 +127,7 @@ type
     TouchTimeOption: (ttCurrent, ttNone, ttExplicit);
     TouchTimeHour, TouchTimeMinute, TouchTimeSecond: Integer;
 
+    SetupEncryptionHeader: TSetupEncryptionHeader;
     SetupHeader: TSetupHeader;
 
     SetupDirectiveLines: array[TSetupSectionDirective] of Integer;
@@ -300,7 +301,7 @@ implementation
 uses
   Commctrl, TypInfo, AnsiStrings, Math, WideStrUtils,
   PathFunc, TrustFunc, ISSigFunc, ECDSA, Shared.CommonFunc, Compiler.Messages, Shared.SetupEntFunc,
-  Shared.FileClass, Compression.Base, Compression.Zlib, Compression.bzlib,
+  Shared.FileClass, Shared.EncryptionFunc, Compression.Base, Compression.Zlib, Compression.bzlib,
   Shared.LangOptionsSectionDirectives, Shared.ResUpdateFunc, Compiler.ExeUpdateFunc,
 {$IFDEF STATICPREPROC}
   ISPP.Preprocess,
@@ -631,7 +632,7 @@ end;
 
 function TSetupCompiler.GetEncryptionBaseNonce: TSetupEncryptionNonce;
 begin
-  Result := SetupHeader.EncryptionBaseNonce;
+  Result := SetupEncryptionHeader.BaseNonce;
 end;
 
 function TSetupCompiler.GetExeFilename: String;
@@ -2836,16 +2837,21 @@ begin
         SetSetupHeaderOption(shEnableDirDoesntExistWarning);
       end;
     ssEncryption: begin
-        SetSetupHeaderOption(shEncryptionUsed);
+        if CompareText(Value, 'full') = 0 then
+          SetupEncryptionHeader.EncryptionUse := euFull
+        else if StrToBool(Value) then
+          SetupEncryptionHeader.EncryptionUse := euFiles
+        else
+          SetupEncryptionHeader.EncryptionUse := euNone;
       end;
     ssEncryptionKeyDerivation: begin
         if Value = 'pbkdf2' then
-          SetupHeader.EncryptionKDFIterations := 200000
+          SetupEncryptionHeader.KDFIterations := 200000
         else if Copy(Value, 1, 7) = 'pbkdf2/' then begin
           I := StrToIntDef(Copy(Value, 8, Maxint), -1);
           if I < 1 then
             Invalid;
-          SetupHeader.EncryptionKDFIterations := I;
+          SetupEncryptionHeader.KDFIterations := I;
         end else
           Invalid;
       end;
@@ -5015,7 +5021,7 @@ type
           Inc6464(TotalBytesToCompress, FileListRec.Size);
           if SetupHeader.CompressMethod <> cmStored then
             Include(NewFileLocationEntry^.Flags, floChunkCompressed);
-          if shEncryptionUsed in SetupHeader.Options then
+          if SetupEncryptionHeader.EncryptionUse <> euNone then
             Include(NewFileLocationEntry^.Flags, floChunkEncrypted);
           if SolidBreak and UseSolidCompression then begin
             Include(NewFileLocationEntryExtraInfo^.Flags, floSolidBreak);
@@ -6887,6 +6893,10 @@ var
 
     F.WriteBuffer(SetupID, SizeOf(SetupID));
 
+    const SetupEncryptionHeaderCRC = GetCRC32(SetupEncryptionHeader, SizeOf(SetupEncryptionHeader));
+    F.WriteBuffer(SetupEncryptionHeaderCRC, SizeOf(SetupEncryptionHeaderCRC));
+    F.WriteBuffer(SetupEncryptionHeader, SizeOf(SetupEncryptionHeader));
+
     SetupHeader.NumLanguageEntries := LanguageEntries.Count;
     SetupHeader.NumCustomMessageEntries := CustomMessageEntries.Count;
     SetupHeader.NumPermissionEntries := PermissionEntries.Count;
@@ -6912,6 +6922,9 @@ var
     W := TCompressedBlockWriter.Create(F, TLZMACompressor, InternalCompressLevel,
       InternalCompressProps);
     try
+      if SetupEncryptionHeader.EncryptionUse = euFull then
+        W.InitEncryption(CryptKey, SetupEncryptionHeader.BaseNonce, sccCompressedBlocks1);
+
       SECompressedBlockWrite(W, SetupHeader, SizeOf(SetupHeader),
         SetupHeaderStrings, SetupHeaderAnsiStrings);
 
@@ -6988,6 +7001,8 @@ var
       { ^ When disk spanning is enabled, the Setup Compiler requires that
         FileLocationEntries be a fixed size, so don't compress them }
     try
+      if SetupEncryptionHeader.EncryptionUse = euFull then
+        W.InitEncryption(CryptKey, SetupEncryptionHeader.BaseNonce, sccCompressedBlocks2);
       for J := 0 to FileLocationEntries.Count-1 do
         W.Write(FileLocationEntries[J]^, SizeOf(TSetupFileLocationEntry));
       W.Finish;
@@ -7647,21 +7662,6 @@ var
     GenerateRandomBytes(Nonce, SizeOf(Nonce));
   end;
 
-  { This function assumes EncryptionKey is based on the password }
-  procedure GeneratePasswordTest(const EncryptionKey: TSetupEncryptionKey;
-    const EncryptionBaseNonce: TSetupEncryptionNonce; out PasswordTest: Integer);
-  begin
-    { Create a special nonce that cannot collide with encrypted-file nonces }
-    var Nonce := EncryptionBaseNonce;
-    Nonce.RandomXorFirstSlice := Nonce.RandomXorFirstSlice xor -1;
-
-    { Encrypt a value of 0 so Setup can do same and compare the results to test the password }
-    var Context: TChaCha20Context;
-    XChaCha20Init(Context, EncryptionKey[0], Length(EncryptionKey), Nonce, SizeOf(Nonce), 0);
-    PasswordTest := 0;
-    XChaCha20Crypt(Context, PasswordTest, PasswordTest, SizeOf(PasswordTest));
-  end;
-
 const
   BadFilePathChars = '/*?"<>|';
   BadFileNameChars = BadFilePathChars + ':';
@@ -7692,6 +7692,7 @@ begin
   SevenZipDLL := nil;
 
   try
+    FillChar(SetupEncryptionHeader, SizeOf(SetupEncryptionHeader), 0);
     Finalize(SetupHeader);
     FillChar(SetupHeader, SizeOf(SetupHeader), 0);
     InitDebugInfo;
@@ -7728,6 +7729,8 @@ begin
     SlicesPerDisk := 1;
     ReserveBytes := 0;
     TimeStampRounding := 2;
+    SetupEncryptionHeader.EncryptionUse := euNone;
+    SetupEncryptionHeader.KDFIterations := 220000;
     SetupHeader.MinVersion.WinVersion := 0;
     SetupHeader.MinVersion.NTVersion := $06010000;
     SetupHeader.MinVersion.NTServicePack := $100;
@@ -7760,7 +7763,6 @@ begin
     NotRecognizedMessagesWarning := True;
     UsedUserAreasWarning := True;
     SetupHeader.WizardStyle := wsClassic;
-    SetupHeader.EncryptionKDFIterations := 220000;
 
     { Read [Setup] section }
     EnumIniSection(EnumSetupProc, 'Setup', 0, True, True, '', False, False);
@@ -7907,7 +7909,7 @@ begin
       else
         VersionInfoProductTextVersion := VersionInfoProductVersionOriginalValue;
     end;
-    if (shEncryptionUsed in SetupHeader.Options) and (Password = '') then begin
+    if (SetupEncryptionHeader.EncryptionUse <> euNone) and (Password = '') then begin
       LineNumber := SetupDirectiveLines[ssEncryption];
       AbortCompileFmt(SCompilerEntryMissing2, ['Setup', 'Password']);
     end;
@@ -7978,10 +7980,10 @@ begin
     end;
 
     if Password <> '' then begin
-      GenerateEncryptionKDFSalt(SetupHeader.EncryptionKDFSalt);
-      GenerateEncryptionKey(Password,  SetupHeader.EncryptionKDFSalt, SetupHeader.EncryptionKDFIterations, CryptKey);
-      GenerateEncryptionBaseNonce(SetupHeader.EncryptionBaseNonce);
-      GeneratePasswordTest(CryptKey, SetupHeader.EncryptionBaseNonce, SetupHeader.PasswordTest);
+      GenerateEncryptionKDFSalt(SetupEncryptionHeader.KDFSalt);
+      GenerateEncryptionKey(Password,  SetupEncryptionHeader.KDFSalt, SetupEncryptionHeader.KDFIterations, CryptKey);
+      GenerateEncryptionBaseNonce(SetupEncryptionHeader.BaseNonce);
+      GeneratePasswordTest(CryptKey, SetupEncryptionHeader.BaseNonce, SetupEncryptionHeader.PasswordTest);
       Include(SetupHeader.Options, shPassword);
     end;
 

+ 30 - 2
Projects/Src/Compression.Base.pas

@@ -2,7 +2,7 @@ unit Compression.Base;
 
 {
   Inno Setup
-  Copyright (C) 1997-2010 Jordan Russell
+  Copyright (C) 1997-2025 Jordan Russell
   Portions by Martijn Laan
   For conditions of distribution and use, see LICENSE.TXT.
 
@@ -12,7 +12,7 @@ unit Compression.Base;
 interface
 
 uses
-  Windows, SysUtils, Shared.Int64Em, Shared.FileClass;
+  Windows, SysUtils, ChaCha20, Shared.Int64Em, Shared.FileClass, Shared.Struct, Shared.EncryptionFunc;
 
 type
   ECompressError = class(Exception);
@@ -77,6 +77,8 @@ type
     FTotalBytesStored: Cardinal;
     FInBufferCount, FOutBufferCount: Cardinal;
     FInBuffer, FOutBuffer: array[0..4095] of Byte;
+    FEncrypt: Boolean;
+    FCryptContext: TChaCha20Context;
     procedure CompressorWriteProc(const Buffer; Count: Longint);
     procedure DoCompress(const Buf; var Count: Cardinal);
     procedure FlushOutputBuffer;
@@ -84,6 +86,8 @@ type
     constructor Create(AFile: TFile; ACompressorClass: TCustomCompressorClass;
       CompressionLevel: Integer; ACompressorProps: TCompressorProps);
     destructor Destroy; override;
+    procedure InitEncryption(const CryptKey: TSetupEncryptionKey;
+      const EncryptionBaseNonce: TSetupEncryptionNonce; const SpecialCryptContextType: TSpecialCryptContextType);
     procedure Finish;
     procedure Write(const Buffer; Count: Cardinal);
   end;
@@ -97,11 +101,15 @@ type
     FInBufferNext: Cardinal;
     FInBufferAvail: Cardinal;
     FInBuffer: array[0..4095] of Byte;
+    FDecrypt: Boolean;
+    FCryptContext: TChaCha20Context;
     function DecompressorReadProc(var Buffer; Count: Longint): Longint;
     procedure ReadChunk;
   public
     constructor Create(AFile: TFile; ADecompressorClass: TCustomDecompressorClass);
     destructor Destroy; override;
+    procedure InitDecryption(const CryptKey: TSetupEncryptionKey;
+      const EncryptionBaseNonce: TSetupEncryptionNonce; const SpecialCryptContextType: TSpecialCryptContextType);
     procedure Read(var Buffer; Count: Cardinal);
   end;
 
@@ -319,11 +327,21 @@ begin
   inherited;
 end;
 
+procedure TCompressedBlockWriter.InitEncryption(const CryptKey: TSetupEncryptionKey;
+  const EncryptionBaseNonce: TSetupEncryptionNonce; const SpecialCryptContextType: TSpecialCryptContextType);
+begin
+  InitCryptContext(CryptKey, EncryptionBaseNonce, SpecialCryptContextType, FCryptContext);
+  FEncrypt := True;
+end;
+
 procedure TCompressedBlockWriter.FlushOutputBuffer;
 { Flushes contents of FOutBuffer into the file, with a preceding CRC }
 var
   CRC: Longint;
 begin
+  if FEncrypt then
+    XChaCha20Crypt(FCryptContext, FOutBuffer, FOutBuffer, FOutBufferCount);
+
   CRC := GetCRC32(FOutBuffer, FOutBufferCount);
   FFile.WriteBuffer(CRC, SizeOf(CRC));
   Inc(FTotalBytesStored, SizeOf(CRC));
@@ -450,6 +468,13 @@ begin
   inherited;
 end;
 
+procedure TCompressedBlockReader.InitDecryption(const CryptKey: TSetupEncryptionKey;
+  const EncryptionBaseNonce: TSetupEncryptionNonce; const SpecialCryptContextType: TSpecialCryptContextType);
+begin
+  InitCryptContext(CryptKey, EncryptionBaseNonce, SpecialCryptContextType, FCryptContext);
+  FDecrypt := True;
+end;
+
 procedure TCompressedBlockReader.ReadChunk;
 var
   CRC: Longint;
@@ -471,6 +496,9 @@ begin
   FInBufferAvail := Len;
   if CRC <> GetCRC32(FInBuffer, Len) then
     raise ECompressDataError.Create(SCompressedBlockDataError);
+
+  if FDecrypt then
+    XChaCha20Crypt(FCryptContext, FInBuffer, FInBuffer, Len);
 end;
 
 function TCompressedBlockReader.DecompressorReadProc(var Buffer;

+ 1 - 1
Projects/Src/Setup.FileExtractor.pas

@@ -192,7 +192,7 @@ procedure TFileExtractor.SeekTo(const FL: TSetupFileLocationEntry;
   procedure InitDecryption;
   begin
     { Recreate the unique nonce from the base nonce }
-    var Nonce := SetupHeader.EncryptionBaseNonce;
+    var Nonce := SetupEncryptionHeader.BaseNonce;
     Nonce.RandomXorStartOffset := Nonce.RandomXorStartOffset xor FChunkStartOffset;
     Nonce.RandomXorFirstSlice := Nonce.RandomXorFirstSlice xor FChunkFirstSlice;
 

+ 44 - 23
Projects/Src/Setup.MainFunc.pas

@@ -105,6 +105,7 @@ var
   UninstallSilent: Boolean;
 
   { Variables read in from the SETUP.0 file }
+  SetupEncryptionHeader: TSetupEncryptionHeader;
   SetupHeader: TSetupHeader;
   LangOptions: TSetupLanguageEntry;
   Entries: array[TEntryType] of TList;
@@ -228,7 +229,6 @@ function ShouldProcessIconEntry(const WizardComponents, WizardTasks: TStringList
   const WizardNoIcons: Boolean; const IconEntry: PSetupIconEntry): Boolean;
 function ShouldProcessRunEntry(const WizardComponents, WizardTasks: TStringList;
   const RunEntry: PSetupRunEntry): Boolean;
-function TestPassword(const EncryptionKey: TSetupEncryptionKey): Boolean;
 procedure UnloadSHFolderDLL;
 function WindowsVersionAtLeast(const AMajor, AMinor: Byte; const ABuild: Word = 0): Boolean;
 function IsWindows8: Boolean;
@@ -242,7 +242,7 @@ uses
   SetupLdrAndSetup.Messages, Shared.SetupMessageIDs, Setup.Install, SetupLdrAndSetup.InstFunc,
   Setup.InstFunc, SetupLdrAndSetup.RedirFunc, PathFunc,
   Compression.Base, Compression.Zlib, Compression.bzlib, Compression.LZMADecompressor,
-  Shared.SetupEntFunc, Setup.SelectLanguageForm,
+  Shared.SetupEntFunc, Shared.EncryptionFunc,  Setup.SelectLanguageForm,
   Setup.WizardForm, Setup.DebugClient, Shared.VerInfoFunc, Setup.FileExtractor,
   Shared.FileClass, Setup.LoggingFunc,
   SimpleExpression, Setup.Helper, Setup.SpawnClient, Setup.SpawnServer,
@@ -373,21 +373,6 @@ begin
   Result := -1;
 end;
 
-{ This function assumes EncryptionKey is based on the password }
-function TestPassword(const EncryptionKey: TSetupEncryptionKey): Boolean;
-begin
-  { Do same as compiler did in GeneratePasswordTest and compare results }
-  var Nonce := SetupHeader.EncryptionBaseNonce;
-  Nonce.RandomXorFirstSlice := Nonce.RandomXorFirstSlice xor -1;
-
-  var Context: TChaCha20Context;
-  XChaCha20Init(Context, EncryptionKey[0], Length(EncryptionKey), Nonce, SizeOf(Nonce), 0);
-  var PasswordTest := 0;
-  XChaCha20Crypt(Context, PasswordTest, PasswordTest, SizeOf(PasswordTest));
-
-  Result := PasswordTest = SetupHeader.PasswordTest;
-end;
-
 class function TDummyClass.ExpandCheckOrInstallConstant(Sender: TSimpleExpression;
   const Constant: String): String;
 begin
@@ -2740,7 +2725,8 @@ var
     end;
   end;
 
-  function HandleInitPassword(const NeedPassword: Boolean): Boolean;
+  function HandleInitPassword(const NeedPassword, AllowSetFileExtractorCryptKey: Boolean;
+    out CryptKey: TSetupEncryptionKey): Boolean; overload;
   { Handles InitPassword and returns the updated value of NeedPassword }
   { Also see WizardForm.CheckPassword }
   begin
@@ -2749,21 +2735,26 @@ var
     if NeedPassword and (InitPassword <> '') then begin
       var PasswordOk := False;
       var S := InitPassword;
-      var CryptKey: TSetupEncryptionKey;
-      GenerateEncryptionKey(S, SetupHeader.EncryptionKDFSalt, SetupHeader.EncryptionKDFIterations, CryptKey);
+      GenerateEncryptionKey(S, SetupEncryptionHeader.KDFSalt, SetupEncryptionHeader.KDFIterations, CryptKey);
       if shPassword in SetupHeader.Options then
-        PasswordOk := TestPassword(CryptKey);
+        PasswordOk := TestPassword(CryptKey, SetupEncryptionHeader.BaseNonce, SetupEncryptionHeader.PasswordTest);
       if not PasswordOk and (CodeRunner <> nil) then
         PasswordOk := CodeRunner.RunBooleanFunctions('CheckPassword', [S], bcTrue, False, PasswordOk);
 
       if PasswordOk then begin
         Result := False;
-        if shEncryptionUsed in SetupHeader.Options then
+        if AllowSetFileExtractorCryptKey and (SetupEncryptionHeader.EncryptionUse <> euNone) then
           FileExtractor.CryptKey := CryptKey;
       end;
     end;
   end;
 
+  function HandleInitPassword(const NeedPassword: Boolean): Boolean; overload;
+  begin
+    var CryptKey: TSetupEncryptionKey;
+    Result := HandleInitPassword(NeedPassword, True, CryptKey);
+  end;
+
   procedure SetupInstallMode;
   begin
     if InitVerySilent then
@@ -3088,6 +3079,7 @@ begin
   SetupMessages[msgSetupFileMissing] := SSetupFileMissing;
   SetupMessages[msgSetupFileCorrupt] := SSetupFileCorrupt;
   SetupMessages[msgSetupFileCorruptOrWrongVer] := SSetupFileCorruptOrWrongVer;
+  SetupMessages[msgIncorrectPassword] := SIncorrectPassword;
 
   { Read setup-0.bin, or from EXE }
   if not SetupLdrMode then begin
@@ -3104,12 +3096,38 @@ begin
       AbortInit(msgSetupFileCorruptOrWrongVer);
     if TestID <> SetupID then
       AbortInit(msgSetupFileCorruptOrWrongVer);
+
+    var SetupEncryptionHeaderCRC: Longint;
+    SetupFile.Read(SetupEncryptionHeaderCRC, SizeOf(SetupEncryptionHeaderCRC));
+    SetupFile.Read(SetupEncryptionHeader, SizeOf(SetupEncryptionHeader));
+    if SetupEncryptionHeaderCRC <> GetCRC32(SetupEncryptionHeader, SizeOf(SetupEncryptionHeader)) then
+      AbortInit(msgSetupFileCorruptOrWrongVer);
+
+    var CryptKey: TSetupEncryptionKey;
+    if SetupEncryptionHeader.EncryptionUse = euFull then begin
+      if InitPassword = '' then
+        raise Exception.Create(SMissingPassword);
+      { HandleInitPassword requires this }
+      SetupHeader.Options := SetupHeader.Options + [shPassword];
+      { Specifying False for AllowSetFileExtractorCryptKey because FileExtractor (a function!)
+        requires SetupHeader.CompressMethod to be set, so delaying until SetupHeader is read below }
+      if HandleInitPassword(True, False, CryptKey) then { HandleInitPassword returns True on failure }
+        AbortInit(msgIncorrectPassword)
+    end;
+
     try
       var Reader := TCompressedBlockReader.Create(SetupFile, TLZMA1Decompressor);
       try
+        if SetupEncryptionHeader.EncryptionUse = euFull then
+          Reader.InitDecryption(CryptKey, SetupEncryptionHeader.BaseNonce, sccCompressedBlocks1);
+
         { Header }
         SECompressedBlockRead(Reader, SetupHeader, SizeOf(SetupHeader),
           SetupHeaderStrings, SetupHeaderAnsiStrings);
+
+        if SetupEncryptionHeader.EncryptionUse = euFull then
+          FileExtractor.CryptKey := CryptKey; { See above }
+
         { Language entries }
         ReadEntriesWithoutVersion(Reader, seLanguage, SetupHeader.NumLanguageEntries,
           SizeOf(TSetupLanguageEntry));
@@ -3187,7 +3205,7 @@ begin
         LogCompatibilityMode;
         LogWindowsVersion;
 
-        NeedPassword := shPassword in SetupHeader.Options;
+        NeedPassword := (SetupEncryptionHeader.EncryptionUse <> euFull) and (shPassword in SetupHeader.Options);
         NeedSerial := False;
         NeedsRestart := shAlwaysRestart in SetupHeader.Options;
 
@@ -3259,6 +3277,9 @@ begin
       end;
       Reader := TCompressedBlockReader.Create(SetupFile, TLZMA1Decompressor);
       try
+        if SetupEncryptionHeader.EncryptionUse = euFull then
+          Reader.InitDecryption(CryptKey, SetupEncryptionHeader.BaseNonce, sccCompressedBlocks2);
+
         { File location entries }
         ReadEntriesWithoutVersion(Reader, seFileLocation, SetupHeader.NumFileLocationEntries,
           SizeOf(TSetupFileLocationEntry));

+ 4 - 4
Projects/Src/Setup.WizardForm.pas

@@ -347,7 +347,7 @@ uses
   PathFunc, RestartManager, SHA256,
   SetupLdrAndSetup.Messages, Setup.MainForm, Setup.MainFunc, Shared.CommonFunc.Vcl,
   Shared.CommonFunc, Setup.InstFunc, Setup.SelectFolderForm, Setup.FileExtractor,
-  Setup.LoggingFunc, Setup.ScriptRunner, Shared.SetupTypes, Shared.SetupSteps,
+  Setup.LoggingFunc, Setup.ScriptRunner, Shared.SetupTypes, Shared.EncryptionFunc, Shared.SetupSteps,
   Setup.ScriptDlg, SetupLdrAndSetup.InstFunc, Setup.Install;
 
 {$R *.DFM}
@@ -2432,19 +2432,19 @@ procedure TWizardForm.NextButtonClick(Sender: TObject);
     var SaveCursor := GetCursor;
     SetCursor(LoadCursor(0, IDC_WAIT));
     try
-      GenerateEncryptionKey(S, SetupHeader.EncryptionKDFSalt, SetupHeader.EncryptionKDFIterations, CryptKey);
+      GenerateEncryptionKey(S, SetupEncryptionHeader.KDFSalt, SetupEncryptionHeader.KDFIterations, CryptKey);
     finally
       SetCursor(SaveCursor);
     end;
 
     if shPassword in SetupHeader.Options then
-      Result := TestPassword(CryptKey);
+      Result := TestPassword(CryptKey, SetupEncryptionHeader.BaseNonce, SetupEncryptionHeader.PasswordTest);
     if not Result and (CodeRunner <> nil) then
       Result := CodeRunner.RunBooleanFunctions('CheckPassword', [S], bcTrue, False, Result);
 
     if Result then begin
       NeedPassword := False;
-      if shEncryptionUsed in SetupHeader.Options then
+      if SetupEncryptionHeader.EncryptionUse <> euNone then
         FileExtractor.CryptKey := CryptKey;
       PasswordEdit.Text := '';
     end else begin

+ 3 - 1
Projects/Src/SetupLdrAndSetup.Messages.pas

@@ -2,7 +2,7 @@ unit SetupLdrAndSetup.Messages;
 
 {
   Inno Setup
-  Copyright (C) 1997-2024 Jordan Russell
+  Copyright (C) 1997-2025 Jordan Russell
   Portions by Martijn Laan
   For conditions of distribution and use, see LICENSE.TXT.
 
@@ -47,6 +47,8 @@ const
     'obtain a new copy of the program.';
   SMsgsFileMissing = 'Messages file "%s" is missing. Please correct ' +
     'the problem or obtain a new copy of the program.';
+  SMissingPassword = 'Please specify the password using the /PASSWORD= command line parameter.';
+  SIncorrectPassword = 'The password you specified is not correct. Please try again.';
 
 implementation
 

+ 90 - 0
Projects/Src/Shared.EncryptionFunc.pas

@@ -0,0 +1,90 @@
+unit Shared.EncryptionFunc;
+
+{
+  Inno Setup
+  Copyright (C) 1997-2025 Jordan Russell
+  Portions by Martijn Laan
+  For conditions of distribution and use, see LICENSE.TXT.
+
+  Encryption functions used by ISCmplr, Setup, and SetupLdr
+}
+
+interface
+
+uses
+  ChaCha20, Shared.Struct;
+
+type
+  TSpecialCryptContextType = (sccPasswordTest, sccCompressedBlocks1, sccCompressedBlocks2);
+
+procedure GenerateEncryptionKey(const Password: String; const Salt: TSetupKDFSalt;
+  const Iterations: Integer; out Key: TSetupEncryptionKey);
+procedure InitCryptContext(const CryptKey: TSetupEncryptionKey;
+  const EncryptionBaseNonce: TSetupEncryptionNonce; const StartOffset: Int64; const FirstSlice: Int32;
+  out CryptContext: TChaCha20Context); overload;
+procedure InitCryptContext(const CryptKey: TSetupEncryptionKey;
+  const EncryptionBaseNonce: TSetupEncryptionNonce; const Typ: TSpecialCryptContextType;
+  out CryptContext: TChaCha20Context); overload;
+procedure GeneratePasswordTest(const CryptKey: TSetupEncryptionKey;
+  const EncryptionBaseNonce: TSetupEncryptionNonce; out PasswordTest: Integer);
+function TestPassword(const CryptKey: TSetupEncryptionKey;
+  const EncryptionBaseNonce: TSetupEncryptionNonce; const ExpectedPasswordTest: Integer): Boolean;
+
+implementation
+
+uses
+  SysUtils, PBKDF2;
+
+procedure GenerateEncryptionKey(const Password: String; const Salt: TSetupKDFSalt;
+  const Iterations: Integer; out Key: TSetupEncryptionKey);
+begin
+  var SaltBytes: TBytes;
+  var SaltSize := SizeOf(Salt);
+  SetLength(SaltBytes, SaltSize);
+  Move(Salt[0], SaltBytes[0], SaltSize);
+  var KeyLength := SizeOf(Key);
+  var KeyBytes := PBKDF2SHA256(Password, SaltBytes, Iterations, KeyLength);
+  Move(KeyBytes[0], Key[0], KeyLength);
+end;
+
+procedure InitCryptContext(const CryptKey: TSetupEncryptionKey;
+  const EncryptionBaseNonce: TSetupEncryptionNonce; const StartOffset: Int64; const FirstSlice: Int32;
+  out CryptContext: TChaCha20Context);
+begin
+  { Create the unique nonce from the base nonce }
+  var Nonce := EncryptionBaseNonce;
+  Nonce.RandomXorStartOffset := Nonce.RandomXorStartOffset xor StartOffset;
+  Nonce.RandomXorFirstSlice := Nonce.RandomXorFirstSlice xor FirstSlice;
+
+  XChaCha20Init(CryptContext, CryptKey[0], Length(CryptKey), Nonce, SizeOf(Nonce), 0);
+end;
+
+procedure InitCryptContext(const CryptKey: TSetupEncryptionKey;
+  const EncryptionBaseNonce: TSetupEncryptionNonce; const Typ: TSpecialCryptContextType;
+  out CryptContext: TChaCha20Context); overload;
+begin
+  const SpecialFirstSlice = -1-(Ord(Typ)-Ord(Low(Typ)));
+  InitCryptContext(CryptKey, EncryptionBaseNonce, 0, SpecialFirstSlice, CryptContext);
+end;
+
+{ This function assumes CryptKey is based on the password }
+procedure GeneratePasswordTest(const CryptKey: TSetupEncryptionKey;
+  const EncryptionBaseNonce: TSetupEncryptionNonce; out PasswordTest: Integer);
+begin
+  var Context: TChaCha20Context;
+  InitCryptContext(CryptKey, EncryptionBaseNonce, sccPasswordTest, Context);
+
+  { Encrypt a value of 0 so Setup can do same and compare the results to test the password }
+  PasswordTest := 0;
+  XChaCha20Crypt(Context, PasswordTest, PasswordTest, SizeOf(PasswordTest));
+end;
+
+function TestPassword(const CryptKey: TSetupEncryptionKey;
+  const EncryptionBaseNonce: TSetupEncryptionNonce; const ExpectedPasswordTest: Integer): Boolean;
+begin
+  var ActualPasswordTest: Integer;
+  GeneratePasswordTest(CryptKey, EncryptionBaseNonce, ActualPasswordTest);
+  Result := ActualPasswordTest = ExpectedPasswordTest;
+end;
+
+end.

+ 1 - 15
Projects/Src/Shared.SetupTypes.pas

@@ -56,8 +56,6 @@ function StringsToCommaString(const Strings: TStrings): String;
 procedure SetStringsFromCommaString(const Strings: TStrings; const Value: String);
 function StrToSetupVersionData(const S: String; var VerData: TSetupVersionData): Boolean;
 procedure HandleRenamedConstants(var Cnst: String; const RenamedConstantCallback: TRenamedConstantCallback);
-procedure GenerateEncryptionKey(const Password: String; const Salt: TSetupKDFSalt;
-  const Iterations: Integer; out Key: TSetupEncryptionKey);
 procedure SetISSigAllowedKey(var ISSigAllowedKeys: AnsiString; const KeyIndex: Integer);
 function GetISSigAllowedKeys([ref] const ISSigAvailableKeys: TArrayOfECDSAKey;
   const ISSigAllowedKeys: AnsiString): TArrayOfECDSAKey;
@@ -66,7 +64,7 @@ function IsExcluded(Text: String; const AExcludes: TStrings): Boolean;
 implementation
 
 uses
-  PBKDF2, PathFunc, Shared.CommonFunc;
+  PathFunc, Shared.CommonFunc;
 
 function QuoteStringIfNeeded(const S: String): String;
 { Used internally by StringsToCommaString. Adds quotes around the string if
@@ -303,18 +301,6 @@ begin
   end;
 end;
 
-procedure GenerateEncryptionKey(const Password: String; const Salt: TSetupKDFSalt;
-  const Iterations: Integer; out Key: TSetupEncryptionKey);
-begin
-  var SaltBytes: TBytes;
-  var SaltSize := SizeOf(Salt);
-  SetLength(SaltBytes, SaltSize);
-  Move(Salt[0], SaltBytes[0], SaltSize);
-  var KeyLength := SizeOf(Key);
-  var KeyBytes := PBKDF2SHA256(Password, SaltBytes, Iterations, KeyLength);
-  Move(KeyBytes[0], Key[0], KeyLength);
-end;
-
 procedure SetISSigAllowedKey(var ISSigAllowedKeys: AnsiString; const KeyIndex: Integer);
 { ISSigAllowedKeys should start out empty. If you then only use this function
   to update it, regular string comparison can be used for comparisons. }

+ 11 - 5
Projects/Src/Shared.Struct.pas

@@ -60,7 +60,7 @@ type
     shAllowUNCPath, shUserInfoPage, shUsePreviousUserInfo,
     shUninstallRestartComputer, shRestartIfNeededByRun, shShowTasksTreeLines,
     shAllowCancelDuringInstall, shWizardImageStretch, shAppendDefaultDirName,
-    shAppendDefaultGroupName, shEncryptionUsed, shSetupLogging,
+    shAppendDefaultGroupName, shSetupLogging,
     shSignedUninstaller, shUsePreviousLanguage, shDisableWelcomePage,
     shCloseApplications, shRestartApplications, shAllowNetworkDrive,
     shForceCloseApplications, shAppNameHasConsts, shUsePreviousPrivileges,
@@ -85,6 +85,16 @@ const
   SetupProcessorArchitectureNames: array[TSetupProcessorArchitecture] of String =
     ('Unknown', 'x86', 'x64', 'Arm32', 'Arm64');
 
+type
+  { Should not contain strings }
+  TSetupEncryptionHeader = packed record
+    EncryptionUse: (euNone, euFiles, euFull);
+    KDFSalt: TSetupKDFSalt;
+    KDFIterations: Integer;
+    BaseNonce: TSetupEncryptionNonce;
+    PasswordTest: Integer;
+  end;
+
 const
   SetupHeaderStrings = 34;
   SetupHeaderAnsiStrings = 4;
@@ -109,10 +119,6 @@ type
     WizardStyle: TSetupWizardStyle;
     WizardSizePercentX, WizardSizePercentY: Integer;
     WizardImageAlphaFormat: (afIgnored, afDefined, afPremultiplied); // Must be same as Graphics.TAlphaFormat
-    PasswordTest: Integer;
-    EncryptionKDFSalt: TSetupKDFSalt;
-    EncryptionKDFIterations: Integer;
-    EncryptionBaseNonce: TSetupEncryptionNonce;
     ExtraDiskSpaceRequired: Integer64;
     SlicesPerDisk: Integer;
     UninstallLogMode: (lmAppend, lmNew, lmOverwrite);

+ 2 - 1
whatsnew.htm

@@ -213,8 +213,9 @@ issigtool --key-file="MyKey.ispublickey" verify "MyProg.dll"</code></pre>
       <li>Improved support for screen readers.</li>
     </ul>
   </li>
-  <li><tt>[Files]</tt> section parameter <tt>Excludes</tt> can now be combined with the <tt>external</tt> flag.</li>
+  <li><tt>[Setup]</tt> section directive <tt>Encryption</tt> can now be set to <tt>full</tt>, which will encrypt the entire installation and requires the password to be specified using the /PASSWORD= command line parameter.</li>
   <li>Increased the maximum value of <tt>[Setup]</tt> section directive <tt>LZMANumBlockThreads</tt> from 32 to 256.</li>
+  <li><tt>[Files]</tt> section parameter <tt>Excludes</tt> can now be combined with the <tt>external</tt> flag.</li>
   <li>Parameters <tt>ExternalSize</tt>, <tt>ExtraDiskSpaceRequired</tt>, and <tt>UninstallDisplaySize</tt> now support digit separators. Example: <tt>UninstallDisplaySize=1_073_741_824</tt>.</li>
   <li>ISPP changes:
     <ul>