diff --git a/Demos/HashBenchmark_FMX/MainFormHashBenchmark.pas b/Demos/HashBenchmark_FMX/MainFormHashBenchmark.pas index be9b529..2b9b942 100644 --- a/Demos/HashBenchmark_FMX/MainFormHashBenchmark.pas +++ b/Demos/HashBenchmark_FMX/MainFormHashBenchmark.pas @@ -217,7 +217,6 @@ procedure TFormMain.RunBenchmark(ClassName: string; RowIndex: Integer); Iterations : UInt32; BufferSize : UInt32; Salt : TBytes; - PwdHashBuf : TBytes; begin Hash := TDECHash.ClassByName(ClassName).Create; @@ -233,8 +232,6 @@ procedure TFormMain.RunBenchmark(ClassName: string; RowIndex: Integer); (TDECPasswordHash(Hash).MaxPasswordLength < BufferSize) then begin BufferSize := TDECPasswordHash(Hash).MaxPasswordLength; - SetLength(PwdHashBuf, BufferSize); - Move(FBenchmarkBuffer[0], PwdHashBuf[0], BufferSize); // Since password hashes take quite long time to calculate limit that time // by limiting the number of iterations calculated but in such a way that @@ -267,12 +264,11 @@ procedure TFormMain.RunBenchmark(ClassName: string; RowIndex: Integer); if Hash.IsPasswordHash then TDECPasswordHash(Hash).Salt := Salt; - if not Hash.IsPasswordHash then - HashResult := Hash.CalcBytes(FBenchmarkBuffer) - else - HashResult := Hash.CalcBytes(PwdHashBuf); -// Former implementation, but this leads to crashes: -// HashResult := Hash.CalcBuffer(@FBenchmarkBuffer[0], BufferSize); + // CalcBuffer(const Buffer; Size) already takes Buffer by reference. + // Do NOT write CalcBuffer(@FBenchmarkBuffer[0], …) — the extra @ makes + // the engine read from a stack temp (pointer-to-pointer), which caused + // intermittent OutOfRange/AVs in Absorb for large buffers (GitHub #94). + HashResult := Hash.CalcBuffer(FBenchmarkBuffer[0], BufferSize); end; FStopwatch.Stop; diff --git a/Source/DECHash.pas b/Source/DECHash.pas index f63a943..c74e529 100644 --- a/Source/DECHash.pas +++ b/Source/DECHash.pas @@ -341,9 +341,13 @@ THash_SHA3Base = class(TDECHashBit) end; /// - /// Buffer type + /// View type for sponge Absorb over an arbitrary-length message. + /// Must not be limited to 64 KiB: with {$R+} enabled for the SHA3 block, + /// indexing past 65535 raised ERangeError (GitHub issue #94 — + /// HashBenchmark / 1 MiB Keccak). The bound is only for typing; Absorb + /// never allocates a TBABytes instance. /// - TBABytes = array[0..65535] of UInt8; + TBABytes = array[0..High(Integer) div SizeOf(UInt8) - 1] of UInt8; /// /// Pointer to a buffer /// @@ -5131,24 +5135,28 @@ procedure THash_SHA3Base.AbsorbQueue; procedure THash_SHA3Base.Calc(const Data; DataSize: Integer); var - DataPtr : PBABytes; + DataPtr : PByte; RoundSize : UInt32; const - // Maximum number of bytes one can process in one round - MaxRoundSize = MaxInt div 8; + // Max bytes per Absorb round. Must keep RoundSize*8 within Int32 (Absorb bit + // length). Kept well below MaxInt/8 so multi-round walks are realistic for + // large messages (HashBenchmark 1 MiB). DataPtr must be PByte so Inc advances + // by bytes — PBABytes would scale by SizeOf(TBABytes) (GitHub #94 / CR). + cMaxBytesPerRound = 64 * 1024; begin // due to the way the inherited calc is constructed it must not be called here! if (DataSize > 0) then begin - DataPtr := PBABytes(@Data); + // Byte-addressed pointer: Inc(DataPtr, n) must advance n bytes. + DataPtr := @Data; while (UInt32(DataSize) > 0) do begin RoundSize := DataSize; - if (RoundSize > MaxRoundSize) then - RoundSize := MaxRoundSize; + if (RoundSize > cMaxBytesPerRound) then + RoundSize := cMaxBytesPerRound; - Absorb(DataPtr, RoundSize * 8); + Absorb(PBABytes(DataPtr), RoundSize * 8); Dec(DataSize, RoundSize); Inc(DataPtr, RoundSize); end; diff --git a/Unit Tests/Tests/TestDECHashSHA3.pas b/Unit Tests/Tests/TestDECHashSHA3.pas index 04729b2..e2a0a50 100644 --- a/Unit Tests/Tests/TestDECHashSHA3.pas +++ b/Unit Tests/Tests/TestDECHashSHA3.pas @@ -350,6 +350,11 @@ TestTHash_Keccak_224 = class(TestTHash_Keccak_Base) procedure TestIdentity; procedure TestFinalByteLength; procedure TestFinalByteLengthOverflow; + /// + /// Regression for GitHub #94: Absorb must accept messages larger than + /// 64 KiB under range checks (HashBenchmark uses 1 MiB). + /// + procedure TestCalcBufferLargeMessage; end; // Test methods for class THash_Keccak_256 @@ -2036,6 +2041,63 @@ procedure TestTHash_Keccak_224.TestIsPasswordHash; CheckNotEquals(true, FHash.IsPasswordHash); end; +procedure TestTHash_Keccak_224.TestCalcBufferLargeMessage; +const + // Larger than cMaxBytesPerRound (64 KiB) in THash_SHA3Base.Calc so the + // multi-round DataPtr walk is exercised (regression for PBABytes Inc scaling). + cSize = 1024 * 1024; // same as HashBenchmark_FMX cBufferSize + cIncrementalChunk = 4096; // each Init/Calc piece stays single-round + // PyCryptodome Crypto.Hash.keccak, digest_bits=224, over 0..255 repeating + cExpectedHex: RawByteString = + '7746d1b9b33662006e83d88443fc956ee1e01b8d058b8227a9d1e66b'; +var + Buf, DigestBuf, DigestBytes, DigestIncremental: TBytes; + i, n, Offset, Take: Integer; + Hex: RawByteString; +begin + SetLength(Buf, cSize); + n := 0; + for i := 0 to cSize - 1 do + begin + Buf[i] := n; + Inc(n); + if n > 255 then + n := 0; + end; + + // One-shot paths (internal multi-round Absorb walk for 1 MiB) + DigestBuf := FHash.CalcBuffer(Buf[0], Length(Buf)); + DigestBytes := FHash.CalcBytes(Buf); + + // Incremental reference: many small Calc calls, each single-round only + FHash.Init; + Offset := 0; + while Offset < cSize do + begin + Take := cIncrementalChunk; + if Offset + Take > cSize then + Take := cSize - Offset; + FHash.Calc(Buf[Offset], Take); + Inc(Offset, Take); + end; + FHash.Done; + DigestIncremental := FHash.DigestAsBytes; + + CheckEquals(Length(DigestBuf), Length(DigestBytes), + 'CalcBuffer and CalcBytes digest lengths must match'); + CheckTrue(CompareMem(@DigestBuf[0], @DigestBytes[0], Length(DigestBuf)), + 'CalcBuffer and CalcBytes digests must match for 1 MiB input'); + CheckEquals(Length(DigestBuf), Length(DigestIncremental), + 'One-shot and incremental digest lengths must match'); + CheckTrue(CompareMem(@DigestBuf[0], @DigestIncremental[0], Length(DigestBuf)), + 'Multi-round one-shot Calc must match incremental small-chunk Calc '+ + '(catches wrong DataPtr byte advance)'); + + Hex := BytesToRawString(TFormat_HEXL.Encode(DigestBuf)); + CheckEquals(string(cExpectedHex), string(Hex), + 'Keccak-224 of 1 MiB (0..255 pattern) must match known digest'); +end; + { TestTHash_Keccak_256 } procedure TestTHash_Keccak_256.ConfigHashClass(HashClass: TDECHash;