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;