2
0
Эх сурвалжийг харах

Cleanup password testing: use the encryption itself (by encrypting 0 and comparing results) to test the password instead of having something separate as before.

Encryption itself is unchanged by this commit except that the key is now derived from the password only once. So it still uses SHA-256 for this.

The next step is changing this to a proper KDF, by changing the implementation of GenerateEncryptionKey and the TSetupEncryptionKey type.
Martijn Laan 11 сар өмнө
parent
commit
9a91001f91

+ 1 - 1
ISHelp/isetup.xml

@@ -5030,7 +5030,7 @@ DiskSliceSize=1457664
 <body>
 <p>Specifies a password you want to prompt the user for at the beginning of the installation.</p>
 <p>When using a password, you might consider setting <link topic="setup_encryption">Encryption</link> to <tt>yes</tt> as well, otherwise files will be stored as plain text and it would not be exceedingly difficult for someone to gain access to them through reverse engineering.</p>
-<p>The password itself is not stored as clear text; it's stored as a SHA-256 hash, salted with a 64-bit random number. (Note: When encryption is enabled, this stored hash is <i>not</i> used for the encryption key.)</p>
+<p>The password itself is not stored as clear text.</p>
 </body>
 </setuptopic>
 

+ 5 - 8
Projects/Src/Compiler.CompressionHandler.pas

@@ -12,7 +12,7 @@ unit Compiler.CompressionHandler;
 interface
 
 uses
-  SHA256, ChaCha20, Shared.Int64Em, Shared.FileClass, Compression.Base,
+  SHA256, ChaCha20, Shared.Struct, Shared.Int64Em, Shared.FileClass, Compression.Base,
   Compiler.StringLists, Compiler.SetupCompiler;
 
 type
@@ -45,7 +45,7 @@ type
     procedure Finish;
     procedure NewChunk(const ACompressorClass: TCustomCompressorClass;
       const ACompressLevel: Integer; const ACompressorProps: TCompressorProps;
-      const AUseEncryption: Boolean; const ACryptKey: String);
+      const AUseEncryption: Boolean; const ACryptKey: TSetupEncryptionKey);
     procedure ProgressProc(BytesProcessed: Cardinal);
     function ReserveBytesOnSlice(const Bytes: Cardinal): Boolean;
     procedure WriteProc(const Buf; BufSize: Longint);
@@ -61,7 +61,7 @@ type
 implementation
 
 uses
-  SysUtils, Shared.Struct, Compiler.Messages, Compiler.HelperFunc;
+  SysUtils, Compiler.Messages, Compiler.HelperFunc;
 
 constructor TCompressionHandler.Create(ACompiler: TSetupCompiler;
   const InitialSliceFilename: String);
@@ -161,7 +161,7 @@ end;
 
 procedure TCompressionHandler.NewChunk(const ACompressorClass: TCustomCompressorClass;
   const ACompressLevel: Integer; const ACompressorProps: TCompressorProps;
-  const AUseEncryption: Boolean; const ACryptKey: String);
+  const AUseEncryption: Boolean; const ACryptKey: TSetupEncryptionKey);
 
   procedure SelectCompressor;
   var
@@ -190,15 +190,12 @@ procedure TCompressionHandler.NewChunk(const ACompressorClass: TCustomCompressor
 
   procedure InitEncryption;
   begin
-    { Create an SHA-256 hash of ACryptKey, and use that as the key }
-    var Key := SHA256Buf(Pointer(ACryptKey)^, Length(ACryptKey)*SizeOf(ACryptKey[1]));
-
     { 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, Key[0], Length(Key), Nonce, SizeOf(Nonce), 0);
+    XChaCha20Init(FCryptContext, ACryptKey[0], Length(ACryptKey), Nonce, SizeOf(Nonce), 0);
   end;
 
 var

+ 30 - 31
Projects/Src/Compiler.SetupCompiler.pas

@@ -19,7 +19,7 @@ interface
 
 uses
   Windows, SysUtils, Classes, Generics.Collections,
-  SimpleExpression, SHA256,
+  SimpleExpression, SHA256, ChaCha20,
   Shared.Struct, Shared.CompilerInt, Shared.PreprocInt, Shared.SetupMessageIDs,
   Shared.SetupSectionDirectives, Shared.VerInfoFunc, Shared.Int64Em, Shared.DebugStruct,
   Compiler.ScriptCompiler, Compiler.StringLists, Compression.LZMACompressor;
@@ -111,7 +111,7 @@ type
     InternalCompressProps, CompressProps: TLZMACompressorProps;
     UseSolidCompression: Boolean;
     DontMergeDuplicateFiles: Boolean;
-    CryptKey: String;
+    CryptKey: TSHA256Digest;
     TimeStampsInUTC: Boolean;
     TimeStampRounding: Integer;
     TouchDateOption: (tdCurrent, tdNone, tdExplicit);
@@ -271,7 +271,7 @@ type
     function GetDebugInfo: TMemoryStream;
     function GetDiskSliceSize:Longint;
     function GetDiskSpanning: Boolean;
-    function GetEncryptionBaseNonce: TSetupNonce;
+    function GetEncryptionBaseNonce: TSetupEncryptionNonce;
     function GetExeFilename: String;
     function GetLineFilename: String;
     function GetLineNumber: Integer;
@@ -597,7 +597,7 @@ begin
   Result := DiskSpanning;
 end;
 
-function TSetupCompiler.GetEncryptionBaseNonce: TSetupNonce;
+function TSetupCompiler.GetEncryptionBaseNonce: TSetupEncryptionNonce;
 begin
   Result := SetupHeader.EncryptionBaseNonce;
 end;
@@ -2350,29 +2350,31 @@ var
     end;
   end;
 
-  procedure GeneratePasswordHashAndSalt(const Password: String;
-    var Hash: TSHA256Digest; var Salt: TSetupSalt);
-  var
-    Context: TSHA256Context;
-  begin
-    { Random salt is mixed into the password hash to make it more difficult
-      for someone to tell that two installations use the same password. A
-      fixed string is also mixed in "just in case" the system's RNG is
-      broken -- this hash must never be the same as the hash used for
-      encryption. }
-    GenerateRandomBytes(Salt, SizeOf(Salt));
-    SHA256Init(Context);
-    SHA256Update(Context, PAnsiChar('PasswordCheckHash')^, Length('PasswordCheckHash'));
-    SHA256Update(Context, Salt, SizeOf(Salt));
-    SHA256Update(Context, Pointer(Password)^, Length(Password)*SizeOf(Password[1]));
-    Hash := SHA256Final(Context);
-  end;
-
-  procedure GenerateEncryptionBaseNonce(var Nonce: TSetupNonce);
+  procedure GenerateEncryptionBaseNonce(var Nonce: TSetupEncryptionNonce);
   begin
     GenerateRandomBytes(Nonce, SizeOf(Nonce));
   end;
 
+  procedure GenerateEncryptionKey(const Password: String; var Key: TSetupEncryptionKey);
+  begin
+    Key := SHA256Buf(Pointer(Password)^, Length(Password)*SizeOf(Password[1]))
+  end;
+
+  { This function assumes EncryptionKey is based on the password }
+  procedure GeneratePasswordTest(const EncryptionKey: TSetupEncryptionKey;
+    const EncryptionBaseNonce: TSetupEncryptionNonce; var 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;
+
   procedure StrToTouchDate(const S: String);
   var
     P: PChar;
@@ -2813,11 +2815,8 @@ begin
     ssEnableDirDoesntExistWarning: begin
         SetSetupHeaderOption(shEnableDirDoesntExistWarning);
       end;
-    ssEncryption:
-      begin
+    ssEncryption: begin
         SetSetupHeaderOption(shEncryptionUsed);
-        if shEncryptionUsed in SetupHeader.Options then
-          GenerateEncryptionBaseNonce(SetupHeader.EncryptionBaseNonce);
       end;
     ssExtraDiskSpaceRequired: begin
         if not StrToInteger64(Value, SetupHeader.ExtraDiskSpaceRequired) then
@@ -2929,9 +2928,9 @@ begin
       end;
     ssPassword: begin
         if Value <> '' then begin
-          CryptKey := Value;
-          GeneratePasswordHashAndSalt(Value, SetupHeader.PasswordHash,
-            SetupHeader.PasswordSalt);
+          GenerateEncryptionBaseNonce(SetupHeader.EncryptionBaseNonce);
+          GenerateEncryptionKey(Value, CryptKey);
+          GeneratePasswordTest(CryptKey, SetupHeader.EncryptionBaseNonce, SetupHeader.PasswordTest);
           Include(SetupHeader.Options, shPassword);
         end;
       end;
@@ -7559,7 +7558,7 @@ begin
       else
         VersionInfoProductTextVersion := VersionInfoProductVersionOriginalValue;
     end;
-    if (shEncryptionUsed in SetupHeader.Options) and (CryptKey = '') then begin
+    if (shEncryptionUsed in SetupHeader.Options) and not (shPassword in SetupHeader.Options) then begin
       LineNumber := SetupDirectiveLines[ssEncryption];
       AbortCompileFmt(SCompilerEntryMissing2, ['Setup', 'Password']);
     end;

+ 12 - 7
Projects/Src/Setup.FileExtractor.pas

@@ -28,12 +28,14 @@ type
     FNeedReset: Boolean;
     FChunkCompressed, FChunkEncrypted: Boolean;
     FCryptContext: TChaCha20Context;
-    FCryptKey: String;
+    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;
@@ -41,7 +43,7 @@ type
       const ProgressProc: TExtractorProgressProc; const VerifyChecksum: Boolean);
     procedure SeekTo(const FL: TSetupFileLocationEntry;
       const ProgressProc: TExtractorProgressProc);
-    property CryptKey: String write FCryptKey;
+    property CryptKey: TSetupEncryptionKey write SetCryptKey;
   end;
 
 function FileExtractor: TFileExtractor;
@@ -100,6 +102,12 @@ begin
   inherited;
 end;
 
+procedure TFileExtractor.SetCryptKey(const Value: TSetupEncryptionKey);
+begin
+  FCryptKey := Value;
+  FCryptKeySet := True;
+end;
+
 var
   LastSourceDir: String;
 
@@ -190,15 +198,12 @@ procedure TFileExtractor.SeekTo(const FL: TSetupFileLocationEntry;
 
   procedure InitDecryption;
   begin
-    { Initialize the key, which is the SHA-256 hash of FCryptKey }
-    var Key := SHA256Buf(Pointer(FCryptKey)^, Length(FCryptKey)*SizeOf(FCryptKey[1]));
-
     { 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, Key[0], Length(Key), Nonce, SizeOf(Nonce), 0);
+    XChaCha20Init(FCryptContext, FCryptKey[0], Length(FCryptKey), Nonce, SizeOf(Nonce), 0);
   end;
 
   procedure Discard(Count: Integer64);
@@ -232,7 +237,7 @@ begin
     InternalError('Cannot call file extractor recursively');
   Inc(FEntered);
   try
-    if (foChunkEncrypted in FL.Flags) and (FCryptKey = '') then
+    if (foChunkEncrypted 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?

+ 24 - 14
Projects/Src/Setup.MainFunc.pas

@@ -227,7 +227,8 @@ 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 Password: String): Boolean;
+procedure GenerateEncryptionKey(const Password: String; var Key: TSetupEncryptionKey);
+function TestPassword(const EncryptionKey: TSetupEncryptionKey): Boolean;
 procedure UnloadSHFolderDLL;
 function WindowsVersionAtLeast(const AMajor, AMinor: Byte; const ABuild: Word = 0): Boolean;
 function IsWindows8: Boolean;
@@ -237,7 +238,7 @@ function IsWindows11: Boolean;
 implementation
 
 uses
-  ShellAPI, ShlObj, StrUtils, ActiveX, RegStr, SHA256,
+  ShellAPI, ShlObj, StrUtils, ActiveX, RegStr, SHA256, ChaCha20,
   SetupLdrAndSetup.Messages, Shared.SetupMessageIDs, Setup.Install, SetupLdrAndSetup.InstFunc,
   Setup.InstFunc, SetupLdrAndSetup.RedirFunc, PathFunc,
   Compression.Base, Compression.Zlib, Compression.bzlib, Compression.LZMADecompressor,
@@ -371,17 +372,24 @@ begin
   Result := -1;
 end;
 
-function TestPassword(const Password: String): Boolean;
-var
-  Context: TSHA256Context;
-  Hash: TSHA256Digest;
+procedure GenerateEncryptionKey(const Password: String; var Key: TSetupEncryptionKey);
+begin
+  Key := SHA256Buf(Pointer(Password)^, Length(Password)*SizeOf(Password[1]))
+end;
+
+{ This function assumes EncryptionKey is based on the password }
+function TestPassword(const EncryptionKey: TSetupEncryptionKey): Boolean;
 begin
-  SHA256Init(Context);
-  SHA256Update(Context, PAnsiChar('PasswordCheckHash')^, Length('PasswordCheckHash'));
-  SHA256Update(Context, SetupHeader.PasswordSalt, SizeOf(SetupHeader.PasswordSalt));
-  SHA256Update(Context, Pointer(Password)^, Length(Password)*SizeOf(Password[1]));
-  Hash := SHA256Final(Context);
-  Result := SHA256DigestsEqual(Hash, SetupHeader.PasswordHash);
+  { 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;
@@ -2691,15 +2699,17 @@ var
     if NeedPassword and (InitPassword <> '') then begin
       PasswordOk := False;
       S := InitPassword;
+      var CryptKey: TSetupEncryptionKey;
+      GenerateEncryptionKey(S, CryptKey);
       if shPassword in SetupHeader.Options then
-        PasswordOk := TestPassword(S);
+        PasswordOk := TestPassword(CryptKey);
       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
-          FileExtractor.CryptKey := S;
+          FileExtractor.CryptKey := CryptKey;
       end;
     end;
   end;

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

@@ -2286,15 +2286,17 @@ procedure TWizardForm.NextButtonClick(Sender: TObject);
   begin
     Result := False;
     S := PasswordEdit.Text;
+    var CryptKey: TSetupEncryptionKey;
+    GenerateEncryptionKey(S, CryptKey);
     if shPassword in SetupHeader.Options then
-      Result := TestPassword(S);
+      Result := TestPassword(CryptKey);
     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
-        FileExtractor.CryptKey := S;
+        FileExtractor.CryptKey := CryptKey;
       PasswordEdit.Text := '';
     end else begin
       { Delay for 750 ms when an incorrect password is entered to

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

@@ -68,8 +68,8 @@ type
     shWizardResizable, shUninstallLogging);
   TSetupLanguageDetectionMethod = (ldUILanguage, ldLocale, ldNone);
   TSetupCompressMethod = (cmStored, cmZip, cmBzip, cmLZMA, cmLZMA2);
-  TSetupSalt = array[0..7] of Byte;
-  TSetupNonce = record
+  TSetupEncryptionKey = TSHA256Digest;
+  TSetupEncryptionNonce = record
     RandomXorStartOffset: Int64;
     RandomXorFirstSlice: Int32;
     RemainingRandom: array[0..2] of Int32;
@@ -109,9 +109,8 @@ type
     WizardStyle: TSetupWizardStyle;
     WizardSizePercentX, WizardSizePercentY: Integer;
     WizardImageAlphaFormat: (afIgnored, afDefined, afPremultiplied); // Must be same as Graphics.TAlphaFormat
-    PasswordHash: TSHA256Digest;
-    PasswordSalt: TSetupSalt;
-    EncryptionBaseNonce: TSetupNonce;
+    PasswordTest: Integer;
+    EncryptionBaseNonce: TSetupEncryptionNonce;
     ExtraDiskSpaceRequired: Integer64;
     SlicesPerDisk: Integer;
     UninstallLogMode: (lmAppend, lmNew, lmOverwrite);