Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Source/DECCipherModes.pas
Original file line number Diff line number Diff line change
Expand Up @@ -1149,6 +1149,9 @@ procedure TDECCipherModes.Done;

case FMode of
cmGCM : begin
// Finalize multi-call GHASH + tag before optional ExpectedTag check
if Assigned(FGCM) then
FGCM.Done;
if (length(FGCM.ExpectedAuthenticationTag) > 0) and
(not IsEqual(FGCM.ExpectedAuthenticationTag, FGCM.CalculatedAuthenticationTag)) then
raise EDECCipherAuthenticationException.CreateRes(@sInvalidAuthenticationValue);
Expand Down
279 changes: 228 additions & 51 deletions Source/DECCipherModesGCM.pas
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,41 @@ TGCM = class(TAuthenticatedCipherModesBase)
/// </summary>
FE_K_Y0 : T128;

/// <summary>
/// Running GHASH state (NIST "X"). Allows multi-call Encode/Decode.
/// Tag is finalized only in Done — see Cleanup-Roadmap §3.1 (Option A).
/// </summary>
FX : T128;
/// <summary>
/// Incomplete 16-byte GHASH block carried across Encode/Decode calls
/// </summary>
FGHASHPartial : array[0..15] of Byte;
/// <summary>
/// Number of valid bytes in FGHASHPartial (0..15)
/// </summary>
FGHASHPartialLen : Integer;
/// <summary>
/// Total ciphertext bytes processed since Init (for length block)
/// </summary>
FTotalCiphertextBytes : UInt64;
/// <summary>
/// True after AAD has been absorbed into FX (and padded to 16 bytes)
/// </summary>
FAuthDataHashed : Boolean;
/// <summary>
/// Leftover keystream from an incomplete CTR block (multi-call Encode/Decode)
/// </summary>
FKeystream : T128;
/// <summary>
/// Number of unused bytes remaining in FKeystream (0..15)
/// </summary>
FKeystreamRemain : Integer;
/// <summary>
/// True after Done has materialized the authentication tag.
/// Prevents double-finalization and post-Done GHASH/CTR updates.
/// </summary>
FFinalized : Boolean;

/// <summary>
/// XOR implementation for unsigned 128 bit numbers
/// </summary>
Expand Down Expand Up @@ -198,6 +233,27 @@ TGCM = class(TAuthenticatedCipherModesBase)
Ciphertext : PUInt8Array;
CiphertextSize : Integer): T128;

/// <summary>
/// Feeds data into the running GHASH state FX (supports partial blocks).
/// </summary>
procedure GHASHUpdate(Data: PUInt8Array; DataSize: Integer);
/// <summary>
/// Pads any incomplete GHASH block with zeros and multiplies into FX.
/// </summary>
procedure GHASHPadPartial;
/// <summary>
/// Ensures AAD has been GHASH'd and padded once before ciphertext bytes.
/// </summary>
procedure EnsureAuthDataHashed;
/// <summary>
/// Completes GHASH (length block) and writes CalculatedAuthenticationTag.
/// </summary>
procedure FinalizeAuthenticationTag;
/// <summary>
/// GCM-CTR keystream XOR with multi-call partial-block carry.
/// </summary>
procedure ApplyCTR(Source, Dest: PUInt8Array; Size: Integer);

/// <summary>
/// Encrypts a T128 value using the encryption method specified on init
/// </summary>
Expand Down Expand Up @@ -263,6 +319,14 @@ TGCM = class(TAuthenticatedCipherModesBase)
Dest : PUInt8Array;
Size : Integer); override;

/// <summary>
/// Finishes GHASH and materializes CalculatedAuthenticationTag.
/// Must be called after the last Encode/Decode (cipher Done does this).
/// Idempotent: a second call leaves the tag unchanged.
/// After finalization, Encode/Decode raise until Init is called again.
/// </summary>
procedure Done;

/// <summary>
/// Returns a list of authentication tag lengths explicitely specified by
/// the official specification of the standard.
Expand All @@ -275,6 +339,10 @@ TGCM = class(TAuthenticatedCipherModesBase)

implementation

resourcestring
sGCMAlreadyFinalized =
'GCM authentication already finalized; call Init before further Encode/Decode';

function TGCM.XOR_T128(const x, y : T128): T128;
begin
Result[0] := x[0] xor y[0];
Expand Down Expand Up @@ -439,6 +507,18 @@ procedure TGCM.Init(EncryptionMethod : TEncodeDecodeMethod;
Nullbytes[0] := 0;
Nullbytes[1] := 0;

// Streaming GHASH + CTR state for multi-call Encode/Decode (Option A)
FX[0] := 0;
FX[1] := 0;
FGHASHPartialLen := 0;
FillChar(FGHASHPartial[0], SizeOf(FGHASHPartial), 0);
FTotalCiphertextBytes := 0;
FAuthDataHashed := False;
FKeystreamRemain := 0;
FKeystream[0] := 0;
FKeystream[1] := 0;
FFinalized := False;

OldH := FH;
EncryptionMethod(@Nullbytes[0], @FH[0], 16);

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

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

procedure TGCM.GHASHUpdate(Data: PUInt8Array; DataSize: Integer);
var
Offset, Take : Integer;
begin
if (DataSize <= 0) or (Data = nil) then
Exit;

Offset := 0;

if FGHASHPartialLen > 0 then
begin
Take := 16 - FGHASHPartialLen;
if Take > DataSize then
Take := DataSize;
Move(Data^[Offset], FGHASHPartial[FGHASHPartialLen], Take);
Inc(FGHASHPartialLen, Take);
Inc(Offset, Take);
if FGHASHPartialLen = 16 then
begin
FX := poly_mult_H(XOR_PointerWithT128(@FGHASHPartial[0], FX));
FGHASHPartialLen := 0;
end;
end;

while Offset + 16 <= DataSize do
begin
FX := poly_mult_H(XOR_PointerWithT128(@Data^[Offset], FX));
Inc(Offset, 16);
end;

if Offset < DataSize then
begin
FGHASHPartialLen := DataSize - Offset;
Move(Data^[Offset], FGHASHPartial[0], FGHASHPartialLen);
end;
end;

procedure TGCM.GHASHPadPartial;
var
Block : T128;
begin
if FGHASHPartialLen > 0 then
begin
Block := nullbytes;
Move(FGHASHPartial[0], Block[0], FGHASHPartialLen);
FX := poly_mult_H(XOR_T128(Block, FX));
FGHASHPartialLen := 0;
end;
end;

procedure TGCM.EnsureAuthDataHashed;
begin
if FAuthDataHashed then
Exit;

if Length(DataToAuthenticate) > 0 then
GHASHUpdate(@DataToAuthenticate[0], Length(DataToAuthenticate));
// Pad AAD to 16-byte boundary before ciphertext (NIST GHASH layout)
GHASHPadPartial;
FAuthDataHashed := True;
end;

procedure TGCM.FinalizeAuthenticationTag;
var
AuthTag : T128;
AuthCipherLength : T128;
AuthLen : Integer;
begin
EnsureAuthDataHashed;
// Pad incomplete ciphertext block
GHASHPadPartial;

AuthLen := Length(DataToAuthenticate);
SetAuthenticationCipherLength(AuthCipherLength, UInt64(AuthLen) shl 3,
FTotalCiphertextBytes shl 3);
FX := poly_mult_H(XOR_T128(AuthCipherLength, FX));
AuthTag := XOR_T128(FX, FE_K_Y0);

SetLength(FCalcAuthenticationTag, FCalcAuthenticationTagLength);
if (FCalcAuthenticationTagLength > 0) then
Move(AuthTag[0], FCalcAuthenticationTag[0], FCalcAuthenticationTagLength);
end;

procedure TGCM.Done;
begin
if FFinalized then
Exit;
FinalizeAuthenticationTag;
FFinalized := True;
end;

function TGCM.CalcGaloisHash(AuthenticatedData : PUInt8Array; AuthLen : integer; Ciphertext : PUInt8Array;
CiphertextSize: Integer): T128;
var
Expand Down Expand Up @@ -507,85 +679,90 @@ function TGCM.CalcGaloisHash(AuthenticatedData : PUInt8Array; AuthLen : integer;
Result := poly_mult_H(XOR_T128(AuthCipherLength, x));
end;

procedure TGCM.Decode(Source, Dest: PUInt8Array; Size: Integer);
procedure TGCM.ApplyCTR(Source, Dest: PUInt8Array; Size: Integer);
var
i, j, BlockCount : UInt64;
a_tag : T128;
pDataToAuth : PUInt8Array;
pSrc : PUInt8Array;
i, Take : Integer;
KSBytes : P16ByteArray;
begin
if Size <= 0 then
Exit;

i := 0;
BlockCount := Size div 16;
// Drain leftover keystream from a previous partial block
if FKeystreamRemain > 0 then
begin
KSBytes := @FKeystream[0];
Take := FKeystreamRemain;
if Take > Size then
Take := Size;
XOR_ArrayWithT128(Source, i, Take, FKeystream, Dest);
// Shift remaining keystream left so index 0 is next unused byte
if Take < FKeystreamRemain then
Move(KSBytes^[Take], KSBytes^[0], FKeystreamRemain - Take);
Dec(FKeystreamRemain, Take);
Inc(i, Take);
end;

for j := 1 to BlockCount do
while i + 16 <= Size do
begin
INCR(FY);
P128(@Dest^[i])^ := XOR_PointerWithT128(@Source^[i], EncodeT128(FY));
inc(i, 16);
Inc(i, 16);
end;

if i < Size then
begin
INCR(FY);
XOR_ArrayWithT128(@Source^[0], i, UInt64(Size)-i, EncodeT128(FY), @Dest^[0]);
FKeystream := EncodeT128(FY);
Take := Size - i;
XOR_ArrayWithT128(Source, i, Take, FKeystream, Dest);
// Keep unused tail of this keystream block for the next call
Move(P16ByteArray(@FKeystream[0])^[Take], P16ByteArray(@FKeystream[0])^[0], 16 - Take);
// Clear used prefix is unnecessary; only FKeystreamRemain matters
FKeystreamRemain := 16 - Take;
end;
end;

pDataToAuth := nil;
if Length(DataToAuthenticate) > 0 then
pDataToAuth := @DataToAuthenticate[0];
pSrc := nil;
if Size > 0 then
pSrc := @source[0];
procedure TGCM.Decode(Source, Dest: PUInt8Array; Size: Integer);
begin
if FFinalized then
raise EDECCipherException.CreateRes(@sGCMAlreadyFinalized);

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

Setlength(FCalcAuthenticationTag, FCalcAuthenticationTagLength);
if (FCalcAuthenticationTagLength > 0) then
Move(a_tag[0], FCalcAuthenticationTag[0], FCalcAuthenticationTagLength);
if Size < 0 then
Size := 0;

// Check for correct authentication result is in Done of DECCipherModes
// if not IsEqual(FExpectedAuthenticationTag, FCalcAuthenticationTag) then
// raise EDECCipherAuthenticationException.CreateRes(@sInvalidAuthenticationValue);
// GHASH over ciphertext before CTR (Source is ciphertext)
if Size > 0 then
begin
GHASHUpdate(Source, Size);
Inc(FTotalCiphertextBytes, UInt64(Size));
end;

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

procedure TGCM.Encode(Source, Dest: PUInt8Array; Size: Integer);
var
i, j, div_len_plain : UInt64;
AuthTag : T128;
pDataToAuth : PUInt8Array;
begin
i := 0;
div_len_plain := Size div 16;
if FFinalized then
raise EDECCipherException.CreateRes(@sGCMAlreadyFinalized);

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

P128(@Dest^[i])^ := XOR_PointerWithT128(@Source^[i], EncodeT128(FY));
if Size < 0 then
Size := 0;

inc(i,16);
end;
ApplyCTR(Source, Dest, Size);

if i < Size then
// GHASH over ciphertext produced in Dest
if Size > 0 then
begin
INCR(FY);
XOR_ArrayWithT128(Source, i, UInt64(Size)-i, EncodeT128(FY), Dest);
GHASHUpdate(Dest, Size);
Inc(FTotalCiphertextBytes, UInt64(Size));
end;

pDataToAuth := nil;
if Length(DataToAuthenticate) > 0 then
pDataToAuth := @DataToAuthenticate[0];
AuthTag := XOR_T128(CalcGaloisHash(pDataToAuth, Length(DataToAuthenticate), @Dest[0], Size), FE_K_Y0);
Setlength(FCalcAuthenticationTag, FCalcAuthenticationTagLength);
if (FCalcAuthenticationTagLength > 0) then
Move(AuthTag[0], FCalcAuthenticationTag[0], FCalcAuthenticationTagLength);
end;

function TGCM.EncodeT128(Value: T128): T128;
Expand Down
Loading