Skip to content

Commit 445db2a

Browse files
authored
Merge pull request #99 from omonien/pr-fix-gcm-multichunk
Fix GCM multi-call Encode/Decode streaming (GHASH + CTR) and multi-chunk tests. Will change a few small formatting and commenting things.
2 parents 0741757 + 7fb2908 commit 445db2a

4 files changed

Lines changed: 467 additions & 60 deletions

File tree

Source/DECCipherModes.pas

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1149,6 +1149,9 @@ procedure TDECCipherModes.Done;
11491149

11501150
case FMode of
11511151
cmGCM : begin
1152+
// Finalize multi-call GHASH + tag before optional ExpectedTag check
1153+
if Assigned(FGCM) then
1154+
FGCM.Done;
11521155
if (length(FGCM.ExpectedAuthenticationTag) > 0) and
11531156
(not IsEqual(FGCM.ExpectedAuthenticationTag, FGCM.CalculatedAuthenticationTag)) then
11541157
raise EDECCipherAuthenticationException.CreateRes(@sInvalidAuthenticationValue);

Source/DECCipherModesGCM.pas

Lines changed: 228 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,41 @@ TGCM = class(TAuthenticatedCipherModesBase)
7575
/// </summary>
7676
FE_K_Y0 : T128;
7777

78+
/// <summary>
79+
/// Running GHASH state (NIST "X"). Allows multi-call Encode/Decode.
80+
/// Tag is finalized only in Done — see Cleanup-Roadmap §3.1 (Option A).
81+
/// </summary>
82+
FX : T128;
83+
/// <summary>
84+
/// Incomplete 16-byte GHASH block carried across Encode/Decode calls
85+
/// </summary>
86+
FGHASHPartial : array[0..15] of Byte;
87+
/// <summary>
88+
/// Number of valid bytes in FGHASHPartial (0..15)
89+
/// </summary>
90+
FGHASHPartialLen : Integer;
91+
/// <summary>
92+
/// Total ciphertext bytes processed since Init (for length block)
93+
/// </summary>
94+
FTotalCiphertextBytes : UInt64;
95+
/// <summary>
96+
/// True after AAD has been absorbed into FX (and padded to 16 bytes)
97+
/// </summary>
98+
FAuthDataHashed : Boolean;
99+
/// <summary>
100+
/// Leftover keystream from an incomplete CTR block (multi-call Encode/Decode)
101+
/// </summary>
102+
FKeystream : T128;
103+
/// <summary>
104+
/// Number of unused bytes remaining in FKeystream (0..15)
105+
/// </summary>
106+
FKeystreamRemain : Integer;
107+
/// <summary>
108+
/// True after Done has materialized the authentication tag.
109+
/// Prevents double-finalization and post-Done GHASH/CTR updates.
110+
/// </summary>
111+
FFinalized : Boolean;
112+
78113
/// <summary>
79114
/// XOR implementation for unsigned 128 bit numbers
80115
/// </summary>
@@ -198,6 +233,27 @@ TGCM = class(TAuthenticatedCipherModesBase)
198233
Ciphertext : PUInt8Array;
199234
CiphertextSize : Integer): T128;
200235

236+
/// <summary>
237+
/// Feeds data into the running GHASH state FX (supports partial blocks).
238+
/// </summary>
239+
procedure GHASHUpdate(Data: PUInt8Array; DataSize: Integer);
240+
/// <summary>
241+
/// Pads any incomplete GHASH block with zeros and multiplies into FX.
242+
/// </summary>
243+
procedure GHASHPadPartial;
244+
/// <summary>
245+
/// Ensures AAD has been GHASH'd and padded once before ciphertext bytes.
246+
/// </summary>
247+
procedure EnsureAuthDataHashed;
248+
/// <summary>
249+
/// Completes GHASH (length block) and writes CalculatedAuthenticationTag.
250+
/// </summary>
251+
procedure FinalizeAuthenticationTag;
252+
/// <summary>
253+
/// GCM-CTR keystream XOR with multi-call partial-block carry.
254+
/// </summary>
255+
procedure ApplyCTR(Source, Dest: PUInt8Array; Size: Integer);
256+
201257
/// <summary>
202258
/// Encrypts a T128 value using the encryption method specified on init
203259
/// </summary>
@@ -263,6 +319,14 @@ TGCM = class(TAuthenticatedCipherModesBase)
263319
Dest : PUInt8Array;
264320
Size : Integer); override;
265321

322+
/// <summary>
323+
/// Finishes GHASH and materializes CalculatedAuthenticationTag.
324+
/// Must be called after the last Encode/Decode (cipher Done does this).
325+
/// Idempotent: a second call leaves the tag unchanged.
326+
/// After finalization, Encode/Decode raise until Init is called again.
327+
/// </summary>
328+
procedure Done;
329+
266330
/// <summary>
267331
/// Returns a list of authentication tag lengths explicitely specified by
268332
/// the official specification of the standard.
@@ -275,6 +339,10 @@ TGCM = class(TAuthenticatedCipherModesBase)
275339

276340
implementation
277341

342+
resourcestring
343+
sGCMAlreadyFinalized =
344+
'GCM authentication already finalized; call Init before further Encode/Decode';
345+
278346
function TGCM.XOR_T128(const x, y : T128): T128;
279347
begin
280348
Result[0] := x[0] xor y[0];
@@ -439,6 +507,18 @@ procedure TGCM.Init(EncryptionMethod : TEncodeDecodeMethod;
439507
Nullbytes[0] := 0;
440508
Nullbytes[1] := 0;
441509

510+
// Streaming GHASH + CTR state for multi-call Encode/Decode (Option A)
511+
FX[0] := 0;
512+
FX[1] := 0;
513+
FGHASHPartialLen := 0;
514+
FillChar(FGHASHPartial[0], SizeOf(FGHASHPartial), 0);
515+
FTotalCiphertextBytes := 0;
516+
FAuthDataHashed := False;
517+
FKeystreamRemain := 0;
518+
FKeystream[0] := 0;
519+
FKeystream[1] := 0;
520+
FFinalized := False;
521+
442522
OldH := FH;
443523
EncryptionMethod(@Nullbytes[0], @FH[0], 16);
444524

@@ -455,11 +535,103 @@ procedure TGCM.Init(EncryptionMethod : TEncodeDecodeMethod;
455535
b^ := 1;
456536
end
457537
else
538+
// One-shot GHASH over IV only (does not use streaming FX)
458539
FY := CalcGaloisHash(nil, 0, @InitVector[0], length(InitVector));
459540

460541
FEncryptionMethod(@FY[0], @FE_K_Y0[0], 16);
461542
end;
462543

544+
procedure TGCM.GHASHUpdate(Data: PUInt8Array; DataSize: Integer);
545+
var
546+
Offset, Take : Integer;
547+
begin
548+
if (DataSize <= 0) or (Data = nil) then
549+
Exit;
550+
551+
Offset := 0;
552+
553+
if FGHASHPartialLen > 0 then
554+
begin
555+
Take := 16 - FGHASHPartialLen;
556+
if Take > DataSize then
557+
Take := DataSize;
558+
Move(Data^[Offset], FGHASHPartial[FGHASHPartialLen], Take);
559+
Inc(FGHASHPartialLen, Take);
560+
Inc(Offset, Take);
561+
if FGHASHPartialLen = 16 then
562+
begin
563+
FX := poly_mult_H(XOR_PointerWithT128(@FGHASHPartial[0], FX));
564+
FGHASHPartialLen := 0;
565+
end;
566+
end;
567+
568+
while Offset + 16 <= DataSize do
569+
begin
570+
FX := poly_mult_H(XOR_PointerWithT128(@Data^[Offset], FX));
571+
Inc(Offset, 16);
572+
end;
573+
574+
if Offset < DataSize then
575+
begin
576+
FGHASHPartialLen := DataSize - Offset;
577+
Move(Data^[Offset], FGHASHPartial[0], FGHASHPartialLen);
578+
end;
579+
end;
580+
581+
procedure TGCM.GHASHPadPartial;
582+
var
583+
Block : T128;
584+
begin
585+
if FGHASHPartialLen > 0 then
586+
begin
587+
Block := nullbytes;
588+
Move(FGHASHPartial[0], Block[0], FGHASHPartialLen);
589+
FX := poly_mult_H(XOR_T128(Block, FX));
590+
FGHASHPartialLen := 0;
591+
end;
592+
end;
593+
594+
procedure TGCM.EnsureAuthDataHashed;
595+
begin
596+
if FAuthDataHashed then
597+
Exit;
598+
599+
if Length(DataToAuthenticate) > 0 then
600+
GHASHUpdate(@DataToAuthenticate[0], Length(DataToAuthenticate));
601+
// Pad AAD to 16-byte boundary before ciphertext (NIST GHASH layout)
602+
GHASHPadPartial;
603+
FAuthDataHashed := True;
604+
end;
605+
606+
procedure TGCM.FinalizeAuthenticationTag;
607+
var
608+
AuthTag : T128;
609+
AuthCipherLength : T128;
610+
AuthLen : Integer;
611+
begin
612+
EnsureAuthDataHashed;
613+
// Pad incomplete ciphertext block
614+
GHASHPadPartial;
615+
616+
AuthLen := Length(DataToAuthenticate);
617+
SetAuthenticationCipherLength(AuthCipherLength, UInt64(AuthLen) shl 3,
618+
FTotalCiphertextBytes shl 3);
619+
FX := poly_mult_H(XOR_T128(AuthCipherLength, FX));
620+
AuthTag := XOR_T128(FX, FE_K_Y0);
621+
622+
SetLength(FCalcAuthenticationTag, FCalcAuthenticationTagLength);
623+
if (FCalcAuthenticationTagLength > 0) then
624+
Move(AuthTag[0], FCalcAuthenticationTag[0], FCalcAuthenticationTagLength);
625+
end;
626+
627+
procedure TGCM.Done;
628+
begin
629+
if FFinalized then
630+
Exit;
631+
FinalizeAuthenticationTag;
632+
FFinalized := True;
633+
end;
634+
463635
function TGCM.CalcGaloisHash(AuthenticatedData : PUInt8Array; AuthLen : integer; Ciphertext : PUInt8Array;
464636
CiphertextSize: Integer): T128;
465637
var
@@ -507,85 +679,90 @@ function TGCM.CalcGaloisHash(AuthenticatedData : PUInt8Array; AuthLen : integer;
507679
Result := poly_mult_H(XOR_T128(AuthCipherLength, x));
508680
end;
509681

510-
procedure TGCM.Decode(Source, Dest: PUInt8Array; Size: Integer);
682+
procedure TGCM.ApplyCTR(Source, Dest: PUInt8Array; Size: Integer);
511683
var
512-
i, j, BlockCount : UInt64;
513-
a_tag : T128;
514-
pDataToAuth : PUInt8Array;
515-
pSrc : PUInt8Array;
684+
i, Take : Integer;
685+
KSBytes : P16ByteArray;
516686
begin
687+
if Size <= 0 then
688+
Exit;
689+
517690
i := 0;
518-
BlockCount := Size div 16;
691+
// Drain leftover keystream from a previous partial block
692+
if FKeystreamRemain > 0 then
693+
begin
694+
KSBytes := @FKeystream[0];
695+
Take := FKeystreamRemain;
696+
if Take > Size then
697+
Take := Size;
698+
XOR_ArrayWithT128(Source, i, Take, FKeystream, Dest);
699+
// Shift remaining keystream left so index 0 is next unused byte
700+
if Take < FKeystreamRemain then
701+
Move(KSBytes^[Take], KSBytes^[0], FKeystreamRemain - Take);
702+
Dec(FKeystreamRemain, Take);
703+
Inc(i, Take);
704+
end;
519705

520-
for j := 1 to BlockCount do
706+
while i + 16 <= Size do
521707
begin
522708
INCR(FY);
523709
P128(@Dest^[i])^ := XOR_PointerWithT128(@Source^[i], EncodeT128(FY));
524-
inc(i, 16);
710+
Inc(i, 16);
525711
end;
526712

527713
if i < Size then
528714
begin
529715
INCR(FY);
530-
XOR_ArrayWithT128(@Source^[0], i, UInt64(Size)-i, EncodeT128(FY), @Dest^[0]);
716+
FKeystream := EncodeT128(FY);
717+
Take := Size - i;
718+
XOR_ArrayWithT128(Source, i, Take, FKeystream, Dest);
719+
// Keep unused tail of this keystream block for the next call
720+
Move(P16ByteArray(@FKeystream[0])^[Take], P16ByteArray(@FKeystream[0])^[0], 16 - Take);
721+
// Clear used prefix is unnecessary; only FKeystreamRemain matters
722+
FKeystreamRemain := 16 - Take;
531723
end;
724+
end;
532725

533-
pDataToAuth := nil;
534-
if Length(DataToAuthenticate) > 0 then
535-
pDataToAuth := @DataToAuthenticate[0];
536-
pSrc := nil;
537-
if Size > 0 then
538-
pSrc := @source[0];
726+
procedure TGCM.Decode(Source, Dest: PUInt8Array; Size: Integer);
727+
begin
728+
if FFinalized then
729+
raise EDECCipherException.CreateRes(@sGCMAlreadyFinalized);
539730

540-
a_tag := XOR_T128(CalcGaloisHash(pDataToAuth, Length(DataToAuthenticate),
541-
pSrc, Size), FE_K_Y0);
731+
// AAD into GHASH once; tag finalized in Done (supports multi-call streams)
732+
EnsureAuthDataHashed;
542733

543-
Setlength(FCalcAuthenticationTag, FCalcAuthenticationTagLength);
544-
if (FCalcAuthenticationTagLength > 0) then
545-
Move(a_tag[0], FCalcAuthenticationTag[0], FCalcAuthenticationTagLength);
734+
if Size < 0 then
735+
Size := 0;
546736

547-
// Check for correct authentication result is in Done of DECCipherModes
548-
// if not IsEqual(FExpectedAuthenticationTag, FCalcAuthenticationTag) then
549-
// raise EDECCipherAuthenticationException.CreateRes(@sInvalidAuthenticationValue);
737+
// GHASH over ciphertext before CTR (Source is ciphertext)
738+
if Size > 0 then
739+
begin
740+
GHASHUpdate(Source, Size);
741+
Inc(FTotalCiphertextBytes, UInt64(Size));
742+
end;
550743

551-
// In difference to the NIST recommendation we do not discard plaintext if
552-
// authentication failed to make data recovery possible. But since we throw
553-
// an exception the user will get notified that there's something wrong
554-
// if not IsEqual(authenticaton_tag, ba_tag) then
555-
// SetLength(plaintext, 0); // NIST FAIL => pt=''
744+
ApplyCTR(Source, Dest, Size);
556745
end;
557746

558747
procedure TGCM.Encode(Source, Dest: PUInt8Array; Size: Integer);
559-
var
560-
i, j, div_len_plain : UInt64;
561-
AuthTag : T128;
562-
pDataToAuth : PUInt8Array;
563748
begin
564-
i := 0;
565-
div_len_plain := Size div 16;
749+
if FFinalized then
750+
raise EDECCipherException.CreateRes(@sGCMAlreadyFinalized);
566751

567-
for j := 1 to div_len_plain do
568-
begin
569-
INCR(FY);
752+
// AAD into GHASH once; tag finalized in Done (supports multi-call streams)
753+
EnsureAuthDataHashed;
570754

571-
P128(@Dest^[i])^ := XOR_PointerWithT128(@Source^[i], EncodeT128(FY));
755+
if Size < 0 then
756+
Size := 0;
572757

573-
inc(i,16);
574-
end;
758+
ApplyCTR(Source, Dest, Size);
575759

576-
if i < Size then
760+
// GHASH over ciphertext produced in Dest
761+
if Size > 0 then
577762
begin
578-
INCR(FY);
579-
XOR_ArrayWithT128(Source, i, UInt64(Size)-i, EncodeT128(FY), Dest);
763+
GHASHUpdate(Dest, Size);
764+
Inc(FTotalCiphertextBytes, UInt64(Size));
580765
end;
581-
582-
pDataToAuth := nil;
583-
if Length(DataToAuthenticate) > 0 then
584-
pDataToAuth := @DataToAuthenticate[0];
585-
AuthTag := XOR_T128(CalcGaloisHash(pDataToAuth, Length(DataToAuthenticate), @Dest[0], Size), FE_K_Y0);
586-
Setlength(FCalcAuthenticationTag, FCalcAuthenticationTagLength);
587-
if (FCalcAuthenticationTagLength > 0) then
588-
Move(AuthTag[0], FCalcAuthenticationTag[0], FCalcAuthenticationTagLength);
589766
end;
590767

591768
function TGCM.EncodeT128(Value: T128): T128;

0 commit comments

Comments
 (0)