diff --git a/Docs/plans/2026-07-25-aead-architecture.md b/Docs/plans/2026-07-25-aead-architecture.md
new file mode 100644
index 00000000..6aa361a7
--- /dev/null
+++ b/Docs/plans/2026-07-25-aead-architecture.md
@@ -0,0 +1,64 @@
+# AEAD Architecture Package (from PR #90 Concern A)
+
+**Branch:** `package/aead-architecture`
+**Base:** `development`
+**Donor:** [PR #90](https://github.com/MHumm/DelphiEncryptionCompendium/pull/90) (architecture only)
+**GCM streaming reference:** PR #99 multi-call GCM semantics (absorbed)
+
+## Goal
+
+Separate AEAD architecture from ChaCha/Poly1305 (Cleanup-Roadmap Concern A vs B).
+
+## What landed
+
+| Area | Decision |
+|------|----------|
+| Mode object | Single `FAuthObj: TAuthenticatedCipherModesBase` instead of dual `FGCM`/`FCCM` |
+| Public API | `IDECAuthenticatedCipher` unchanged |
+| Protected API | `EncodeGCM`/`DecodeGCM`/`EncodeCCM`/`DecodeCCM` kept as dispatch wrappers (no rename break) |
+| GCM streaming | PR #99 engine: GHASH partial + CTR keystream remainder + `FFinalized` |
+| Tag timing | Tag is valid only after `Done` (multi-call GHASH). Callers must `Done` before reading the tag |
+| CCM | Still one-shot; base `Done` is a no-op for the CCM object; ExpectedTag still verified in `TDECCipherModes.Done` |
+| Poly1305 / ChaCha | **Out of scope** (package B) |
+| `cmPoly1305` enum | **Not** added in this package |
+
+## What was rejected from PR #90 (as-is)
+
+1. **`fIsLastBlock` CTR model** — treats any non-16-aligned *call* as end of message; breaks multi-chunk streams (e.g. 7+25).
+2. **Missing keystream remainder** — wrong ciphertext after partial blocks.
+3. **Abstract hooks with empty `inherited`** — EAbstractError risk on CCM stubs.
+4. **Burn-on-finalize of only H** without post-Done lock — unsafe continued Encode after Done.
+5. **Shipping Poly1305/ChaCha/CPU units** with the architecture package.
+
+## Lifecycle (binding)
+
+```
+Init(Key, IV)
+ → set DataToAuthenticate / AuthenticationResultBitLength / ExpectedAuthenticationResult
+ → Encode* or Decode* (multi-call OK for GCM; one-shot for CCM)
+ → Done (finalizes GCM tag; verifies ExpectedTag if set)
+ → read CalculatedAuthenticationResult
+```
+
+After `Done`, further GCM `Encode`/`Decode` raise until `Init` again. `Done` is idempotent for the tag.
+
+## Files
+
+| File | Role |
+|------|------|
+| `Source/DECAuthenticatedCipherModesBase.pas` | Shared AEAD base + virtual `Done` |
+| `Source/DECCipherModes.pas` | `FAuthObj` wiring, leak-safe `InitMode`, unified `Done` |
+| `Source/DECCipherModesGCM.pas` | Multi-call GCM (PR #99 semantics) |
+| `Unit Tests/Tests/TestDECCipherModesGCM.pas` | Multi-chunk + Done lifecycle tests |
+| `Unit Tests/Data/gcmEncryptExtIV256_large.rsp` | Corrected large-vector tag |
+
+## Test results (Delphi 13, Win32 Console DUnit)
+
+- GCM suite: **19/19** green (including multi-chunk 16+16, 7+25, Done lifecycle)
+- CCM suite: **16/16** green
+- Non-AEAD cipher modes: green
+- Residual reds: pre-existing Keccak vector issues only (separate PRs #98/#100)
+
+## Package B next
+
+ChaCha20 / XChaCha20 / Poly1305 AEAD on top of this base (`FAuthObj` + `Done` contract).
diff --git a/Source/DECAuthenticatedCipherModesBase.pas b/Source/DECAuthenticatedCipherModesBase.pas
index 6245f1af..85b05b56 100644
--- a/Source/DECAuthenticatedCipherModesBase.pas
+++ b/Source/DECAuthenticatedCipherModesBase.pas
@@ -1,4 +1,4 @@
-{*****************************************************************************
+{*****************************************************************************
The DEC team (see file NOTICE.txt) licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
@@ -40,9 +40,9 @@ EDECAuthLengthException = class(EDECException);
///
/// A method of this type needs to be supplied for encrypting or decrypting
- /// a block via this GCM algorithm. The method is implemented as a parameter,
- /// to avoid the need to bring TGCM in the inheritance chain. TGCM thus can
- /// be used for composition instead of inheritance.
+ /// a block via an authenticated cipher mode. The method is implemented as a
+ /// parameter to allow composition instead of inheritance (e.g. TGCM/TCCM
+ /// hold a reference to the underlying block cipher's encode method).
///
///
/// Data to be encrypted
@@ -56,8 +56,24 @@ EDECAuthLengthException = class(EDECException);
TEncodeDecodeMethod = procedure(Source, Dest: Pointer; Size: Integer) of Object;
///
- /// Base class for authenticated cipher modes
+ /// Base class for authenticated cipher modes (GCM, CCM, future AEAD modes).
///
+ ///
+ /// Lifecycle for multi-call capable modes (e.g. GCM):
+ ///
+ /// Init → set AAD / tag length / expected tag → Encode/Decode* → Done →
+ /// read CalculatedAuthenticationTag.
+ ///
+ ///
+ /// Done must be called before the calculated authentication tag is valid
+ /// for multi-call streams. Done is idempotent. After Done, further
+ /// Encode/Decode raises until Init is called again.
+ ///
+ ///
+ /// CCM remains one-shot (single Encode/Decode with full message length);
+ /// Done still verifies ExpectedAuthenticationTag when set.
+ ///
+ ///
TAuthenticatedCipherModesBase = class(TObject)
strict protected
///
@@ -93,7 +109,12 @@ TAuthenticatedCipherModesBase = class(TObject)
///
procedure SetAuthenticationTagLength(const Value: UInt32); virtual;
///
- /// Returns the length of the calculated authehtication value in bit
+ /// Assigns additional authenticated data (AAD). Modes may override to
+ /// reject changes after AAD has already been absorbed into the MAC state.
+ ///
+ procedure SetDataToAuthenticate(const Value: TBytes); virtual;
+ ///
+ /// Returns the length of the calculated authentication value in bit
///
///
/// Length of the calculated authentication value in bit
@@ -114,7 +135,8 @@ TAuthenticatedCipherModesBase = class(TObject)
InitVector : TBytes); virtual;
///
- /// Encodes a block of data using the supplied cipher
+ /// Encodes a block of data using the supplied cipher. May be called
+ /// multiple times for modes that support streaming (e.g. GCM).
///
///
/// Plain text to encrypt
@@ -129,7 +151,8 @@ TAuthenticatedCipherModesBase = class(TObject)
Dest : PUInt8Array;
Size : Integer); virtual; abstract;
///
- /// Decodes a block of data using the supplied cipher
+ /// Decodes a block of data using the supplied cipher. May be called
+ /// multiple times for modes that support streaming (e.g. GCM).
///
///
/// Encrypted ciphertext to decrypt
@@ -144,6 +167,13 @@ TAuthenticatedCipherModesBase = class(TObject)
Dest : PUInt8Array;
Size : Integer); virtual; abstract;
+ ///
+ /// Finalizes the authentication tag after all Encode/Decode calls.
+ /// Idempotent. Default implementation is a no-op (suitable for modes that
+ /// already compute the tag inside Encode/Decode, e.g. CCM).
+ ///
+ procedure Done; virtual;
+
///
/// Returns a list of authentication tag lengths explicitely specified by
/// the official specification of the standard.
@@ -158,7 +188,7 @@ TAuthenticatedCipherModesBase = class(TObject)
///
property DataToAuthenticate : TBytes
read FDataToAuthenticate
- write FDataToAuthenticate;
+ write SetDataToAuthenticate;
///
/// Sets the length of AuthenticatonTag in bit, values as per official
/// specification are: 128, 120, 112, 104, or 96 bit. For certain
@@ -170,7 +200,8 @@ TAuthenticatedCipherModesBase = class(TObject)
read GetAuthenticationTagBitLength
write SetAuthenticationTagLength;
///
- /// Calculated authentication value
+ /// Calculated authentication value. For multi-call modes this is only
+ /// complete after Done has been called.
///
property CalculatedAuthenticationTag : TBytes
read FCalcAuthenticationTag
@@ -221,10 +252,21 @@ procedure TAuthenticatedCipherModesBase.Init(EncryptionMethod : TEncodeDecodeMet
FEncryptionMethod := EncryptionMethod;
end;
+procedure TAuthenticatedCipherModesBase.Done;
+begin
+ // Default: no deferred finalization (CCM computes the tag in Encode/Decode).
+ // Streaming modes such as GCM override this to materialize the tag.
+end;
+
procedure TAuthenticatedCipherModesBase.SetAuthenticationTagLength(const Value: UInt32);
begin
FCalcAuthenticationTagLength := Value shr 3;
SetLength(FCalcAuthenticationTag, FCalcAuthenticationTagLength);
end;
+procedure TAuthenticatedCipherModesBase.SetDataToAuthenticate(const Value: TBytes);
+begin
+ FDataToAuthenticate := Value;
+end;
+
end.
diff --git a/Source/DECCipherModes.pas b/Source/DECCipherModes.pas
index 532cae85..db5889de 100644
--- a/Source/DECCipherModes.pas
+++ b/Source/DECCipherModes.pas
@@ -1,4 +1,4 @@
-{*****************************************************************************
+{*****************************************************************************
The DEC team (see file NOTICE.txt) licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
@@ -26,7 +26,7 @@ interface
{$ELSE}
System.SysUtils,
{$ENDIF}
- DECTypes, DECCipherBase, DECCipherModesGCM, DECCipherModesCCM,
+ DECTypes, DECCipherBase, DECAuthenticatedCipherModesBase,
DECCipherInterface;
type
@@ -139,15 +139,10 @@ TDECCipherModes = class(TDECCipher, IDECAuthenticatedCipher)
procedure SetExpectedAuthenticationResult(const Value: TBytes);
strict protected
///
- /// Implementation of the Galois counter mode. Only created when gmGCM is
- /// set as mode.
+ /// Authenticated mode implementation (GCM, CCM, future AEAD modes).
+ /// Created when an authenticated cipher mode is selected.
///
- FGCM : TGCM;
- ///
- /// Implementation of the Counter with CBC-MAC mode. Only created when
- /// gmCCM is set as mode.
- ///
- FCCM : TCCM;
+ FAuthObj : TAuthenticatedCipherModesBase;
///
/// Raises an EDECCipherException exception and provides the correct value
/// for block size in that message
@@ -240,15 +235,14 @@ TDECCipherModes = class(TDECCipher, IDECAuthenticatedCipher)
///
procedure EncodeCTSx(Source, Dest: PUInt8Array; Size: Integer); virtual;
///
- /// Galois Counter Mode: encryption with addtional optional authentication.
- /// Implemented in its own unit, but needed here to be callable even if
- /// source length is 0.
+ /// Authenticated encryption via FAuthObj (GCM, CCM). Kept as EncodeGCM for
+ /// protected-API compatibility; dispatches to the active auth mode object.
+ /// Callable even if source length is 0 (AAD-only / empty PT).
///
procedure EncodeGCM(Source, Dest: PUInt8Array; Size: Integer); virtual;
///
- /// Counter with CBC-MAC Mode: encryption with addtional optional authentication.
- /// Implemented in its own unit, but needed here to be callable even if
- /// source length is 0.
+ /// Authenticated encryption via FAuthObj. Alias retained for protected-API
+ /// compatibility with code that overrode EncodeCCM.
///
procedure EncodeCCM(Source, Dest: PUInt8Array; Size: Integer); virtual;
{$IFDEF DEC3_CMCTS}
@@ -335,11 +329,13 @@ TDECCipherModes = class(TDECCipher, IDECAuthenticatedCipher)
///
procedure DecodeCTSx(Source, Dest: PUInt8Array; Size: Integer); virtual;
///
- /// Galois Counter Mode, details are implemented in DECCipherModesGCM
+ /// Authenticated decryption via FAuthObj (GCM, CCM). Kept as DecodeGCM for
+ /// protected-API compatibility.
///
procedure DecodeGCM(Source, Dest: PUInt8Array; Size: Integer); virtual;
///
- /// Counter with CBC-MAC Mode, details are implemented in DECCipherModesCCM
+ /// Authenticated decryption via FAuthObj. Alias retained for protected-API
+ /// compatibility with code that overrode DecodeCCM.
///
procedure DecodeCCM(Source, Dest: PUInt8Array; Size: Integer); virtual;
{$IFDEF DEC3_CMCTS}
@@ -358,8 +354,7 @@ TDECCipherModes = class(TDECCipher, IDECAuthenticatedCipher)
procedure DecodeCTS3(Source, Dest: PUInt8Array; Size: Integer); virtual;
{$ENDIF}
///
- /// When setting mode to GCM the GCM implementing class instance needs to
- /// be created
+ /// When setting an authenticated mode, create the matching FAuthObj instance
///
procedure InitMode; override;
public
@@ -467,7 +462,9 @@ implementation
{$ELSE}
System.TypInfo,
{$ENDIF}
- DECUtil;
+ DECUtil,
+ DECCipherModesGCM,
+ DECCipherModesCCM;
resourcestring
sInvalidMessageLength = 'Message length for mode %0:s must be a multiple of %1:d bytes';
@@ -491,33 +488,27 @@ procedure TDECCipherModes.ReportInvalidMessageLength(Cipher: TDECCipher);
procedure TDECCipherModes.SetDataToAuthenticate(const Value: TBytes);
begin
- case FMode of
- cmGCM: FGCM.DataToAuthenticate := Value;
- cmCCM: FCCM.DataToAuthenticate := Value;
- else
- raise EDECCipherException.CreateResFmt(@sInvalidModeForMethod, ['cmGCM or cmCCM']);
- end;
+ if Assigned(FAuthObj) then
+ FAuthObj.DataToAuthenticate := Value
+ else
+ raise EDECCipherException.CreateResFmt(@sInvalidModeForMethod, ['cmGCM or cmCCM']);
end;
procedure TDECCipherModes.SetExpectedAuthenticationResult(const Value: TBytes);
begin
- case FMode of
- cmGCM: FGCM.ExpectedAuthenticationTag := Value;
- cmCCM: FCCM.ExpectedAuthenticationTag := Value;
- else
- raise EDECCipherException.CreateResFmt(@sInvalidModeForMethod, ['cmGCM or cmCCM']);
- end;
+ if Assigned(FAuthObj) then
+ FAuthObj.ExpectedAuthenticationTag := Value
+ else
+ raise EDECCipherException.CreateResFmt(@sInvalidModeForMethod, ['cmGCM or cmCCM']);
end;
procedure TDECCipherModes.SetAuthenticationResultBitLength(
const Value: Integer);
begin
- case FMode of
- cmGCM: FGCM.AuthenticationTagBitLength := Value;
- cmCCM: FCCM.AuthenticationTagBitLength := Value;
- else
- raise EDECCipherException.CreateResFmt(@sInvalidModeForMethod, ['cmGCM or cmCCM']);
- end;
+ if Assigned(FAuthObj) then
+ FAuthObj.AuthenticationTagBitLength := Value
+ else
+ raise EDECCipherException.CreateResFmt(@sInvalidModeForMethod, ['cmGCM or cmCCM']);
end;
procedure TDECCipherModes.Encode(const Source; var Dest; DataSize: Integer);
@@ -710,66 +701,59 @@ procedure TDECCipherModes.EncodeOFBx(Source, Dest: PUInt8Array; Size: Integer);
function TDECCipherModes.GetDataToAuthenticate: TBytes;
begin
- case FMode of
- cmGCM: Result := FGCM.DataToAuthenticate;
- cmCCM: Result := FCCM.DataToAuthenticate;
- else
- raise EDECCipherException.CreateResFmt(@sInvalidModeForMethod, ['cmGCM or cCCM']);
- end;
+ if Assigned(FAuthObj) then
+ Result := FAuthObj.DataToAuthenticate
+ else
+ raise EDECCipherException.CreateResFmt(@sInvalidModeForMethod, ['cmGCM or cmCCM']);
end;
function TDECCipherModes.GetExpectedAuthenticationResult: TBytes;
begin
- case FMode of
- cmGCM: Result := FGCM.ExpectedAuthenticationTag;
- cmCCM: Result := FCCM.ExpectedAuthenticationTag;
- else
- raise EDECCipherException.CreateResFmt(@sInvalidModeForMethod, ['cmGCM or cmCCM']);
- end;
+ if Assigned(FAuthObj) then
+ Result := FAuthObj.ExpectedAuthenticationTag
+ else
+ raise EDECCipherException.CreateResFmt(@sInvalidModeForMethod, ['cmGCM or cmCCM']);
end;
function TDECCipherModes.GetStandardAuthenticationTagBitLengths: TStandardBitLengths;
begin
- case FMode of
- cmGCM: Result := FGCM.GetStandardAuthenticationTagBitLengths;
- cmCCM: Result := FCCM.GetStandardAuthenticationTagBitLengths;
- else
- begin
- SetLength(Result, 1);
- Result[0] := 0;
- end;
+ if Assigned(FAuthObj) then
+ Result := FAuthObj.GetStandardAuthenticationTagBitLengths
+ else
+ begin
+ SetLength(Result, 1);
+ Result[0] := 0;
end;
end;
function TDECCipherModes.GetAuthenticationResultBitLength: Integer;
begin
- case FMode of
- cmGCM: Result := FGCM.AuthenticationTagBitLength;
- cmCCM: Result := FCCM.AuthenticationTagBitLength;
- else
- raise EDECCipherException.CreateResFmt(@sInvalidModeForMethod, ['cmGCM or cmCCM']);
- end;
+ if Assigned(FAuthObj) then
+ Result := FAuthObj.AuthenticationTagBitLength
+ else
+ raise EDECCipherException.CreateResFmt(@sInvalidModeForMethod, ['cmGCM or cmCCM']);
end;
function TDECCipherModes.GetCalcAuthenticatonResult: TBytes;
begin
- case FMode of
- cmGCM: Result := FGCM.CalculatedAuthenticationTag;
- cmCCM: Result := FCCM.CalculatedAuthenticationTag;
- else
- raise EDECCipherException.CreateResFmt(@sInvalidModeForMethod, ['cmGCM or cmCCM']);
- end;
+ if Assigned(FAuthObj) then
+ Result := FAuthObj.CalculatedAuthenticationTag
+ else
+ raise EDECCipherException.CreateResFmt(@sInvalidModeForMethod, ['cmGCM or cmCCM']);
end;
procedure TDECCipherModes.InitMode;
begin
+ // Always free previous auth object to avoid leaks on mode re-assignment
+ FreeAndNil(FAuthObj);
+
if FMode in [TCipherMode.cmGCM, TCipherMode.cmCCM] then
begin
if (Context.BlockSize = 16) then
begin
case FMode of
- cmGCM: FGCM := TGCM.Create;
- cmCCM: FCCM := TCCM.Create;
+ cmGCM: FAuthObj := TGCM.Create;
+ cmCCM: FAuthObj := TCCM.Create;
end;
end
else
@@ -777,14 +761,6 @@ procedure TDECCipherModes.InitMode;
raise EDECCipherException.CreateResFmt(@sInvalidBlockSize,
[128, GetEnumName(TypeInfo(TCipherMode),
Integer(FMode))]);
- end
- else
- begin
- if Assigned(FGCM) then
- FreeAndNil(FGCM);
-
- if Assigned(FCCM) then
- FreeAndNil(FCCM);
end;
end;
@@ -883,7 +859,9 @@ procedure TDECCipherModes.EncodeGCM(Source, Dest: PUInt8Array; Size: Integer);
if (Size < 0) then
Size := 0;
- FGCM.Encode(Source, Dest, Size);
+ // Dispatch through FAuthObj (TGCM when Mode=cmGCM). Independent of EncodeCCM
+ // so a subclass override of one entry point does not affect the other.
+ FAuthObj.Encode(Source, Dest, Size);
end;
procedure TDECCipherModes.EncodeCCM(Source, Dest: PUInt8Array; Size: Integer);
@@ -891,7 +869,9 @@ procedure TDECCipherModes.EncodeCCM(Source, Dest: PUInt8Array; Size: Integer);
if (Size < 0) then
Size := 0;
- FCCM.Encode(Source, Dest, Size);
+ // Same FAuthObj.Encode body as EncodeGCM, but a separate protected entry so
+ // overriding EncodeGCM does not change CCM behaviour (and vice versa).
+ FAuthObj.Encode(Source, Dest, Size);
end;
{$IFDEF DEC3_CMCTS}
@@ -981,7 +961,8 @@ procedure TDECCipherModes.DecodeGCM(Source, Dest: PUInt8Array; Size: Integer);
if (Size < 0) then
Size := 0;
- FGCM.Decode(Source, Dest, Size);
+ // Independent of DecodeCCM — see EncodeGCM/EncodeCCM.
+ FAuthObj.Decode(Source, Dest, Size);
end;
procedure TDECCipherModes.DecodeCCM(Source, Dest: PUInt8Array; Size: Integer);
@@ -989,7 +970,8 @@ procedure TDECCipherModes.DecodeCCM(Source, Dest: PUInt8Array; Size: Integer);
if (Size < 0) then
Size := 0;
- FCCM.Decode(Source, Dest, Size);
+ // Separate protected entry from DecodeGCM; same FAuthObj.Decode body.
+ FAuthObj.Decode(Source, Dest, Size);
end;
procedure TDECCipherModes.DecodeCFB8(Source, Dest: PUInt8Array; Size: Integer);
@@ -1137,8 +1119,7 @@ procedure TDECCipherModes.DecodeOFBx(Source, Dest: PUInt8Array; Size: Integer);
destructor TDECCipherModes.Destroy;
begin
- FGCM.Free;
- FCCM.Free;
+ FreeAndNil(FAuthObj);
inherited;
end;
@@ -1147,21 +1128,16 @@ procedure TDECCipherModes.Done;
begin
inherited;
- 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);
- end;
-
- cmCCM : begin
- if (length(FCCM.ExpectedAuthenticationTag) > 0) and
- (not IsEqual(FCCM.ExpectedAuthenticationTag, FCCM.CalculatedAuthenticationTag)) then
- raise EDECCipherAuthenticationException.CreateRes(@sInvalidAuthenticationValue);
- end;
+ if Assigned(FAuthObj) then
+ begin
+ // Finalize multi-call authentication (GCM) before optional ExpectedTag check.
+ // CCM Done is a no-op on the mode object (tag already computed in Encode/Decode).
+ FAuthObj.Done;
+
+ if (Length(FAuthObj.ExpectedAuthenticationTag) > 0) and
+ (not IsEqual(FAuthObj.ExpectedAuthenticationTag,
+ FAuthObj.CalculatedAuthenticationTag)) then
+ raise EDECCipherAuthenticationException.CreateRes(@sInvalidAuthenticationValue);
end;
end;
@@ -1169,10 +1145,8 @@ procedure TDECCipherModes.OnAfterInitVectorInitialization(const OriginalInitVect
begin
inherited;
- case FMode of
- cmGCM: FGCM.Init(self.DoEncode, OriginalInitVector);
- cmCCM: FCCM.Init(self.DoEncode, OriginalInitVector);
- end;
+ if Assigned(FAuthObj) then
+ FAuthObj.Init(Self.DoEncode, OriginalInitVector);
end;
procedure TDECCipherModes.DecodeCFSx(Source, Dest: PUInt8Array; Size: Integer);
diff --git a/Source/DECCipherModesGCM.pas b/Source/DECCipherModesGCM.pas
index 2f4a5127..8b3e9c43 100644
--- a/Source/DECCipherModesGCM.pas
+++ b/Source/DECCipherModesGCM.pas
@@ -273,8 +273,14 @@ TGCM = class(TAuthenticatedCipherModesBase)
/// are: 128, 120, 112, 104, or 96 bit. For certain applications, they
/// may be 64 or 32 as well, but the use of these two tag lengths
/// constrains the length of the input data and the lifetime of the key.
+ /// Must be 1..128; longer values would over-read the 16-byte GHASH tag.
///
procedure SetAuthenticationTagLength(const Value: UInt32); override;
+ ///
+ /// Rejects AAD assignment once GHASH has absorbed DataToAuthenticate
+ /// (after the first Encode/Decode) or after Done has finalized the tag.
+ ///
+ procedure SetDataToAuthenticate(const Value: TBytes); override;
public
///
/// Should be called when starting encryption/decryption in order to
@@ -325,7 +331,7 @@ TGCM = class(TAuthenticatedCipherModesBase)
/// Idempotent: a second call leaves the tag unchanged.
/// After finalization, Encode/Decode raise until Init is called again.
///
- procedure Done;
+ procedure Done; override;
///
/// Returns a list of authentication tag lengths explicitely specified by
@@ -342,6 +348,10 @@ implementation
resourcestring
sGCMAlreadyFinalized =
'GCM authentication already finalized; call Init before further Encode/Decode';
+ sGCMAADLocked =
+ 'GCM DataToAuthenticate cannot be changed after Encode/Decode has started or after Done';
+ sGCMAuthTagLength =
+ 'GCM AuthenticationTagBitLength must be between 1 and 128 bits';
function TGCM.XOR_T128(const x, y : T128): T128;
begin
@@ -468,8 +478,22 @@ procedure TGCM.ShiftRight(var rx : T128);
procedure TGCM.SetAuthenticationTagLength(const Value: UInt32);
begin
- FCalcAuthenticationTagLength := Value shr 3;
- SetLength(FCalcAuthenticationTag, FCalcAuthenticationTagLength);
+ // AuthTag is always a 16-byte (128-bit) GHASH result; longer bit lengths
+ // would over-read that buffer when materializing FCalcAuthenticationTag.
+ if (Value = 0) or (Value > 128) then
+ raise EDECAuthLengthException.CreateRes(@sGCMAuthTagLength);
+
+ inherited SetAuthenticationTagLength(Value);
+end;
+
+procedure TGCM.SetDataToAuthenticate(const Value: TBytes);
+begin
+ // Once AAD is in FX (or the tag is finalized), changing DataToAuthenticate
+ // would desync Length(AAD) in the GHASH length block from the absorbed AAD.
+ if FAuthDataHashed or FFinalized then
+ raise EDECCipherException.CreateRes(@sGCMAADLocked);
+
+ inherited SetDataToAuthenticate(Value);
end;
procedure TGCM.INCR(var Y : T128);
@@ -603,14 +627,6 @@ procedure TGCM.EnsureAuthDataHashed;
FAuthDataHashed := True;
end;
-procedure TGCM.Done;
-begin
- if FFinalized then
- Exit;
- FinalizeAuthenticationTag;
- FFinalized := True;
-end;
-
procedure TGCM.FinalizeAuthenticationTag;
var
AuthTag : T128;
@@ -622,15 +638,28 @@ procedure TGCM.FinalizeAuthenticationTag;
GHASHPadPartial;
AuthLen := Length(DataToAuthenticate);
- SetAuthenticationCipherLength(AuthCipherLength,
- UInt64(AuthLen) shl 3,
+ SetAuthenticationCipherLength(AuthCipherLength, UInt64(AuthLen) shl 3,
FTotalCiphertextBytes shl 3);
FX := poly_mult_H(XOR_T128(AuthCipherLength, FX));
AuthTag := XOR_T128(FX, FE_K_Y0);
+ // Defensive: never copy more than the 16-byte GHASH tag.
SetLength(FCalcAuthenticationTag, FCalcAuthenticationTagLength);
if (FCalcAuthenticationTagLength > 0) then
- Move(AuthTag[0], FCalcAuthenticationTag[0], FCalcAuthenticationTagLength);
+ begin
+ if FCalcAuthenticationTagLength > SizeOf(AuthTag) then
+ Move(AuthTag[0], FCalcAuthenticationTag[0], SizeOf(AuthTag))
+ else
+ Move(AuthTag[0], FCalcAuthenticationTag[0], FCalcAuthenticationTagLength);
+ end;
+end;
+
+procedure TGCM.Done;
+begin
+ if FFinalized then
+ Exit;
+ FinalizeAuthenticationTag;
+ FFinalized := True;
end;
function TGCM.CalcGaloisHash(AuthenticatedData : PUInt8Array; AuthLen : integer; Ciphertext : PUInt8Array;
diff --git a/Unit Tests/Tests/TestDECCipherModesGCM.pas b/Unit Tests/Tests/TestDECCipherModesGCM.pas
index f83ef87e..e640c14b 100644
--- a/Unit Tests/Tests/TestDECCipherModesGCM.pas
+++ b/Unit Tests/Tests/TestDECCipherModesGCM.pas
@@ -34,6 +34,7 @@ interface
System.Math,
DECBaseClass,
DECTypes,
+ DECAuthenticatedCipherModesBase,
DECCipherBase,
DECCipherModes,
DECCipherFormats,
@@ -155,6 +156,8 @@ TestTDECGCM = class(TTestCase)
const AChunkSizes: array of Integer);
procedure DoEncodeAfterDone;
procedure DoDecodeAfterDone;
+ procedure DoChangeAADAfterEncode;
+ procedure DoSetAuthTagBitLengthTooLong;
public
procedure SetUp; override;
procedure TearDown; override;
@@ -183,13 +186,21 @@ TestTDECGCM = class(TTestCase)
///
procedure TestDoneIdempotent;
///
- /// Encode after Done must raise an exception until Init is called again.
+ /// Encode after Done must raise until Init is called again.
///
procedure TestEncodeAfterDoneRejected;
///
- /// Decode after Done must raise an exception until Init is called again.
+ /// Decode after Done must raise until Init is called again.
///
procedure TestDecodeAfterDoneRejected;
+ ///
+ /// Changing DataToAuthenticate after Encode has started must raise.
+ ///
+ procedure TestAADChangeAfterEncodeRejected;
+ ///
+ /// AuthenticationResultBitLength > 128 must raise (tag buffer is 16 bytes).
+ ///
+ procedure TestAuthTagBitLengthTooLongRejected;
procedure TestSetGetDataToAuthenticate;
procedure TestSetGetAuthenticationBitLength;
procedure TestGetStandardAuthenticationTagBitLengths;
@@ -946,6 +957,39 @@ procedure TestTDECGCM.TestDecodeAfterDoneRejected;
'Decode after Done must raise EDECCipherException');
end;
+procedure TestTDECGCM.DoChangeAADAfterEncode;
+begin
+ FCipherAES.DataToAuthenticate := BytesOf(RawByteString('changed-aad'));
+end;
+
+procedure TestTDECGCM.TestAADChangeAfterEncodeRejected;
+var
+ ptBytes: TBytes;
+begin
+ ptBytes := TFormat_HexL.Decode(BytesOf(cCAVS_MultiChunkPT));
+ FCipherAES.Init(BytesOf(TFormat_HexL.Decode(cCAVS_MultiChunkKey)),
+ BytesOf(TFormat_HexL.Decode(cCAVS_MultiChunkIV)), $FF);
+ FCipherAES.AuthenticationResultBitLength := cCAVS_MultiChunkTagBits;
+ FCipherAES.DataToAuthenticate := TFormat_HexL.Decode(BytesOf('aabbccdd'));
+ // First Encode absorbs AAD into GHASH — subsequent AAD assignment must fail
+ FCipherAES.EncodeBytes(Copy(ptBytes, 0, 16));
+ CheckException(DoChangeAADAfterEncode, EDECCipherException,
+ 'Changing DataToAuthenticate after Encode must raise');
+end;
+
+procedure TestTDECGCM.DoSetAuthTagBitLengthTooLong;
+begin
+ FCipherAES.AuthenticationResultBitLength := 256;
+end;
+
+procedure TestTDECGCM.TestAuthTagBitLengthTooLongRejected;
+begin
+ FCipherAES.Init(BytesOf(TFormat_HexL.Decode(cCAVS_MultiChunkKey)),
+ BytesOf(TFormat_HexL.Decode(cCAVS_MultiChunkIV)), $FF);
+ CheckException(DoSetAuthTagBitLengthTooLong, EDECAuthLengthException,
+ 'AuthenticationResultBitLength > 128 must raise EDECAuthLengthException');
+end;
+
procedure TestTDECGCM.DoTestEncodeStream_LoadAndTestCAVSData(const
aMaxChunkSize: Int64);
var
@@ -1106,11 +1150,12 @@ procedure TestTDECGCM.TestSetExpectedAuthenticationResult;
procedure TestTDECGCM.TestSetGetAuthenticationBitLength;
begin
+ // NIST SP 800-38D: tag length is at most 128 bit (truncated GHASH result)
FCipherAES.AuthenticationResultBitLength := 128;
CheckEquals(128, FCipherAES.AuthenticationResultBitLength);
- FCipherAES.AuthenticationResultBitLength := 192;
- CheckEquals(192, FCipherAES.AuthenticationResultBitLength);
+ FCipherAES.AuthenticationResultBitLength := 96;
+ CheckEquals(96, FCipherAES.AuthenticationResultBitLength);
end;
procedure TestTDECGCM.TestSetGetDataToAuthenticate;