From a99f35be51d78ea990a6c3237235ff83af8cc6b3 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 15 Sep 2026 13:11:12 +0000 Subject: [PATCH 1/5] AEAD lifecycle: tag after Done, EncodeAuthenticated, CCM via Done Follow-up to PR #102 (points 1, 3, 5, 6). Reading CalculatedAuthenticationTag before Done now raises EDECCipherException. Protected EncodeGCM/EncodeCCM are unified as EncodeAuthenticated (Decode counterpart too). CCM materializes the authentication tag in Done so GCM and CCM share one lifecycle. Modes without prescribed tag lengths document returning an empty array. Co-authored-by: Olaf Monien --- Source/DECAuthenticatedCipherModesBase.pas | 84 +++++++++++++++---- Source/DECCipherFormats.pas | 10 +-- Source/DECCipherInterface.pas | 20 +++-- Source/DECCipherModes.pas | 98 +++++++++++----------- Source/DECCipherModesCCM.pas | 90 +++++++++++++++++--- Source/DECCipherModesGCM.pas | 17 +--- Unit Tests/Tests/TestDECCipherModesCCM.pas | 82 ++++++++++++++++++ Unit Tests/Tests/TestDECCipherModesGCM.pas | 26 ++++++ 8 files changed, 323 insertions(+), 104 deletions(-) diff --git a/Source/DECAuthenticatedCipherModesBase.pas b/Source/DECAuthenticatedCipherModesBase.pas index 85b05b56..56d9783c 100644 --- a/Source/DECAuthenticatedCipherModesBase.pas +++ b/Source/DECAuthenticatedCipherModesBase.pas @@ -59,19 +59,21 @@ EDECAuthLengthException = class(EDECException); /// Base class for authenticated cipher modes (GCM, CCM, future AEAD modes). /// /// - /// Lifecycle for multi-call capable modes (e.g. GCM): + /// Lifecycle for authenticated modes (GCM, CCM): /// /// 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. + /// Done must be called before CalculatedAuthenticationTag may be read. + /// Reading the tag before Done raises EDECCipherException. 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. + /// GCM supports multi-call Encode/Decode. CCM currently remains one-shot + /// (single Encode/Decode with the full message length); the tag is still + /// materialized in Done so both modes share the same lifecycle. /// /// TAuthenticatedCipherModesBase = class(TObject) @@ -100,6 +102,13 @@ TAuthenticatedCipherModesBase = class(TObject) /// FEncryptionMethod : TEncodeDecodeMethod; + /// + /// True after Done has materialized the authentication tag. Reading + /// CalculatedAuthenticationTag before this is set raises. Encode/Decode + /// after finalization also raises until Init is called again. + /// + FFinalized : Boolean; + /// /// Defines the length of the resulting authentication value in bit. /// @@ -120,6 +129,24 @@ TAuthenticatedCipherModesBase = class(TObject) /// Length of the calculated authentication value in bit /// function GetAuthenticationTagBitLength: UInt32; virtual; + /// + /// Returns the calculated authentication tag. Raises if Done has not + /// been called yet. + /// + /// + /// Calculated authentication tag bytes + /// + /// + /// Raised when the tag is read before Done. + /// + function GetCalculatedAuthenticationTag: TBytes; virtual; + /// + /// Raises EDECCipherException when Encode/Decode is attempted after Done. + /// + /// + /// Raised when the mode has already been finalized. + /// + procedure CheckNotFinalized; public /// /// Should be called when starting encryption/decryption in order to @@ -169,8 +196,9 @@ TAuthenticatedCipherModesBase = class(TObject) /// /// 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). + /// Idempotent. Marks the tag as readable via CalculatedAuthenticationTag. + /// Concrete modes that defer tag computation (GCM, CCM) override this + /// to materialize the tag before calling inherited. /// procedure Done; virtual; @@ -179,7 +207,8 @@ TAuthenticatedCipherModesBase = class(TObject) /// the official specification of the standard. /// /// - /// List of bit lengths + /// List of bit lengths prescribed by the mode specification. If the + /// mode does not prescribe any tag lengths, an empty array is returned. /// function GetStandardAuthenticationTagBitLengths:TStandardBitLengths; virtual; @@ -200,11 +229,15 @@ TAuthenticatedCipherModesBase = class(TObject) read GetAuthenticationTagBitLength write SetAuthenticationTagLength; /// - /// Calculated authentication value. For multi-call modes this is only - /// complete after Done has been called. + /// Calculated authentication value. Valid only after Done has been + /// called. Reading this property before Done raises EDECCipherException + /// so callers follow the Init → Encode/Decode* → Done → tag lifecycle. /// + /// + /// Raised when the property is read before Done. + /// property CalculatedAuthenticationTag : TBytes - read FCalcAuthenticationTag + read GetCalculatedAuthenticationTag write FCalcAuthenticationTag; /// @@ -221,6 +254,12 @@ implementation uses DECUtil; +resourcestring + sAuthenticationTagNotFinalized = + 'Calculated authentication tag is not available before Done has been called'; + sAuthenticatedModeAlreadyFinalized = + 'Authenticated cipher mode already finalized; call Init before further Encode/Decode'; + { TAuthenticatedCipherModesBase } function TAuthenticatedCipherModesBase.GetAuthenticationTagBitLength: UInt32; @@ -228,8 +267,25 @@ function TAuthenticatedCipherModesBase.GetAuthenticationTagBitLength: UInt32; Result := FCalcAuthenticationTagLength shl 3; end; +function TAuthenticatedCipherModesBase.GetCalculatedAuthenticationTag: TBytes; +begin + if not FFinalized then + raise EDECCipherException.CreateRes(@sAuthenticationTagNotFinalized); + + Result := FCalcAuthenticationTag; +end; + +procedure TAuthenticatedCipherModesBase.CheckNotFinalized; +begin + if FFinalized then + raise EDECCipherException.CreateRes(@sAuthenticatedModeAlreadyFinalized); +end; + function TAuthenticatedCipherModesBase.GetStandardAuthenticationTagBitLengths: TStandardBitLengths; begin + // No prescribed lengths at this abstraction: return an empty array rather + // than a dummy 0-entry so callers can distinguish "none specified" from a + // specified length of 0 bits. SetLength(Result, 0); end; @@ -250,12 +306,12 @@ procedure TAuthenticatedCipherModesBase.Init(EncryptionMethod : TEncodeDecodeMet end; FEncryptionMethod := EncryptionMethod; + FFinalized := False; 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. + FFinalized := True; end; procedure TAuthenticatedCipherModesBase.SetAuthenticationTagLength(const Value: UInt32); diff --git a/Source/DECCipherFormats.pas b/Source/DECCipherFormats.pas index c4598933..e9a03e66 100644 --- a/Source/DECCipherFormats.pas +++ b/Source/DECCipherFormats.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 @@ -732,8 +732,8 @@ function TDECFormattedCipher.EncodeBytes(const Source: TBytes): TBytes; if Length(Result) > 0 then Encode(Source[0], Result[0], Length(Source)) else - if (FMode = cmGCM) then - EncodeGCM(nil, nil, 0); + if (FMode = cmGCM) or (FMode = cmCCM) then + EncodeAuthenticated(nil, nil, 0); end; begin @@ -755,8 +755,8 @@ function TDECFormattedCipher.DecodeBytes(const Source: TBytes): TBytes; Decode(Source[0], Result[0], Length(Source)); end else - if (FMode = cmGCM) then - DecodeGCM(nil, nil, 0); + if (FMode = cmGCM) or (FMode = cmCCM) then + DecodeAuthenticated(nil, nil, 0); if not (FPaddingClass = nil) then Result := FPaddingClass.RemovePadding(Result, Context.BlockSize); diff --git a/Source/DECCipherInterface.pas b/Source/DECCipherInterface.pas index a4e1c6ed..f9a280aa 100644 --- a/Source/DECCipherInterface.pas +++ b/Source/DECCipherInterface.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 @@ -665,11 +665,12 @@ interface /// /// /// Result of the authentication. Raises an EDECCipherException if this is - /// called for a cipher mode not supporting authentication. + /// called for a cipher mode not supporting authentication, or if Done + /// has not been called yet. /// /// /// Exception raised if called for a cipher mode not supporting - /// authentication. + /// authentication, or if the tag is read before Done. /// function GetCalcAuthenticatonResult: TBytes; /// @@ -705,8 +706,10 @@ interface /// the official specification of the standard. /// /// - /// List of bit lengths. If the cipher mode used is not an authenticated - /// one, the array will just contain a single value of 0. + /// List of bit lengths prescribed by the authenticated mode. If the + /// cipher mode used is not an authenticated one, the array will just + /// contain a single value of 0. If an authenticated mode does not + /// prescribe tag lengths, an empty array is returned. /// function GetStandardAuthenticationTagBitLengths:TStandardBitLengths; @@ -756,12 +759,13 @@ interface /// /// Some block chaining modes have the ability to authenticate the message /// in addition to encrypting it. This property contains the generated - /// authentication tag. Raises an EDECCipherException if this is - /// called for a cipher mode not supporting authentication. + /// authentication tag. Call Done before reading it; reading the tag + /// before Done raises EDECCipherException. Raises an EDECCipherException + /// if this is called for a cipher mode not supporting authentication. /// /// /// Exception raised if called for a cipher mode not supporting - /// authentication. + /// authentication, or if the tag is read before Done. /// property CalculatedAuthenticationResult : TBytes read GetCalcAuthenticatonResult; diff --git a/Source/DECCipherModes.pas b/Source/DECCipherModes.pas index db5889de..4a5e8176 100644 --- a/Source/DECCipherModes.pas +++ b/Source/DECCipherModes.pas @@ -81,11 +81,12 @@ TDECCipherModes = class(TDECCipher, IDECAuthenticatedCipher) /// /// /// Result of the authentication. Raises an EDECCipherException if this is - /// called for a cipher mode not supporting authentication. + /// called for a cipher mode not supporting authentication, or if Done + /// has not been called yet. /// /// /// Exception raised if called for a cipher mode not supporting - /// authentication. + /// authentication, or if the tag is read before Done. /// function GetCalcAuthenticatonResult: TBytes; /// @@ -235,16 +236,20 @@ TDECCipherModes = class(TDECCipher, IDECAuthenticatedCipher) /// procedure EncodeCTSx(Source, Dest: PUInt8Array; Size: Integer); virtual; /// - /// Authenticated encryption via FAuthObj (GCM, CCM). Kept as EncodeGCM for - /// protected-API compatibility; dispatches to the active auth mode object. + /// Authenticated encryption via FAuthObj (GCM, CCM, future AEAD modes). + /// Replaces the former protected EncodeGCM / EncodeCCM entry points. /// Callable even if source length is 0 (AAD-only / empty PT). /// - procedure EncodeGCM(Source, Dest: PUInt8Array; Size: Integer); virtual; - /// - /// Authenticated encryption via FAuthObj. Alias retained for protected-API - /// compatibility with code that overrode EncodeCCM. - /// - procedure EncodeCCM(Source, Dest: PUInt8Array; Size: Integer); virtual; + /// + /// Plain text to encrypt + /// + /// + /// Ciphertext after encryption + /// + /// + /// Number of bytes to encrypt + /// + procedure EncodeAuthenticated(Source, Dest: PUInt8Array; Size: Integer); virtual; {$IFDEF DEC3_CMCTS} /// /// double CBC, with @@ -329,15 +334,19 @@ TDECCipherModes = class(TDECCipher, IDECAuthenticatedCipher) /// procedure DecodeCTSx(Source, Dest: PUInt8Array; Size: Integer); virtual; /// - /// Authenticated decryption via FAuthObj (GCM, CCM). Kept as DecodeGCM for - /// protected-API compatibility. + /// Authenticated decryption via FAuthObj (GCM, CCM, future AEAD modes). + /// Replaces the former protected DecodeGCM / DecodeCCM entry points. /// - procedure DecodeGCM(Source, Dest: PUInt8Array; Size: Integer); virtual; - /// - /// Authenticated decryption via FAuthObj. Alias retained for protected-API - /// compatibility with code that overrode DecodeCCM. - /// - procedure DecodeCCM(Source, Dest: PUInt8Array; Size: Integer); virtual; + /// + /// Encrypted ciphertext to decrypt + /// + /// + /// Plaintext after decryption + /// + /// + /// Number of bytes to decrypt + /// + procedure DecodeAuthenticated(Source, Dest: PUInt8Array; Size: Integer); virtual; {$IFDEF DEC3_CMCTS} /// /// double CBC @@ -401,8 +410,10 @@ TDECCipherModes = class(TDECCipher, IDECAuthenticatedCipher) /// specified by the official specification of the standard. /// /// - /// List of bit lengths. If the cipher mode used is not an authenticated - /// one, the array will just contain a single value of 0. + /// List of bit lengths prescribed by the authenticated mode. If the + /// cipher mode used is not an authenticated one, the array will just + /// contain a single value of 0. If an authenticated mode does not + /// prescribe tag lengths, an empty array is returned. /// function GetStandardAuthenticationTagBitLengths:TStandardBitLengths; @@ -430,12 +441,13 @@ TDECCipherModes = class(TDECCipher, IDECAuthenticatedCipher) /// /// Some block chaining modes have the ability to authenticate the message /// in addition to encrypting it. This property contains the generated - /// authentication tag. Raises an EDECCipherException if this is - /// called for a cipher mode not supporting authentication. + /// authentication tag. Call Done before reading it; reading the tag + /// before Done raises EDECCipherException. Raises an EDECCipherException + /// if this is called for a cipher mode not supporting authentication. /// /// /// Exception raised if called for a cipher mode not supporting - /// authentication. + /// authentication, or if the tag is read before Done. /// property CalculatedAuthenticationResult : TBytes read GetCalcAuthenticatonResult; @@ -528,8 +540,8 @@ procedure TDECCipherModes.Encode(const Source; var Dest; DataSize: Integer); cmOFBx: EncodeOFBx(@Source, @Dest, DataSize); cmCFS8: EncodeCFS8(@Source, @Dest, DataSize); cmCFSx: EncodeCFSx(@Source, @Dest, DataSize); - cmGCM : EncodeGCM(@Source, @Dest, DataSize); - cmCCM : EncodeCCM(@Source, @Dest, DataSize); + cmGCM : EncodeAuthenticated(@Source, @Dest, DataSize); + cmCCM : EncodeAuthenticated(@Source, @Dest, DataSize); end; end; @@ -854,23 +866,14 @@ procedure TDECCipherModes.EncodeCTSx(Source, Dest: PUInt8Array; Size: Integer); FState := csEncode; end; -procedure TDECCipherModes.EncodeGCM(Source, Dest: PUInt8Array; Size: Integer); +procedure TDECCipherModes.EncodeAuthenticated(Source, Dest: PUInt8Array; Size: Integer); begin - if (Size < 0) then - Size := 0; - - // 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; + if not Assigned(FAuthObj) then + raise EDECCipherException.CreateResFmt(@sInvalidModeForMethod, ['cmGCM or cmCCM']); -procedure TDECCipherModes.EncodeCCM(Source, Dest: PUInt8Array; Size: Integer); -begin if (Size < 0) then Size := 0; - // 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; @@ -916,8 +919,8 @@ procedure TDECCipherModes.Decode(const Source; var Dest; DataSize: Integer); cmOFBx: DecodeOFBx(@Source, @Dest, DataSize); cmCFS8: DecodeCFS8(@Source, @Dest, DataSize); cmCFSx: DecodeCFSx(@Source, @Dest, DataSize); - cmGCM : DecodeGCM(@Source, @Dest, DataSize); - cmCCM : DecodeCCM(@Source, @Dest, DataSize); + cmGCM : DecodeAuthenticated(@Source, @Dest, DataSize); + cmCCM : DecodeAuthenticated(@Source, @Dest, DataSize); end; end; @@ -956,21 +959,14 @@ procedure TDECCipherModes.DecodeECBx(Source, Dest: PUInt8Array; Size: Integer); end; end; -procedure TDECCipherModes.DecodeGCM(Source, Dest: PUInt8Array; Size: Integer); +procedure TDECCipherModes.DecodeAuthenticated(Source, Dest: PUInt8Array; Size: Integer); begin - if (Size < 0) then - Size := 0; - - // Independent of DecodeCCM — see EncodeGCM/EncodeCCM. - FAuthObj.Decode(Source, Dest, Size); -end; + if not Assigned(FAuthObj) then + raise EDECCipherException.CreateResFmt(@sInvalidModeForMethod, ['cmGCM or cmCCM']); -procedure TDECCipherModes.DecodeCCM(Source, Dest: PUInt8Array; Size: Integer); -begin if (Size < 0) then Size := 0; - // Separate protected entry from DecodeGCM; same FAuthObj.Decode body. FAuthObj.Decode(Source, Dest, Size); end; @@ -1130,8 +1126,8 @@ procedure TDECCipherModes.Done; 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). + // Finalize authentication (GCM GHASH / CCM CBC-MAC tag) before optional + // ExpectedTag check. Both modes materialize the tag in FAuthObj.Done. FAuthObj.Done; if (Length(FAuthObj.ExpectedAuthenticationTag) > 0) and diff --git a/Source/DECCipherModesCCM.pas b/Source/DECCipherModesCCM.pas index 3f835f67..6adcf35f 100644 --- a/Source/DECCipherModesCCM.pas +++ b/Source/DECCipherModesCCM.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 @@ -51,6 +51,19 @@ TCCM = class(TAuthenticatedCipherModesBase) /// Init vector which is modified during processing /// FInitVector : TBlock16Byte; + /// + /// CBC-MAC state after Encode/Decode; the authentication tag is derived + /// from this in Done (S_0 XOR T), not inside Encode/Decode. + /// + FMacBlock : TBlock16Byte; + /// + /// L parameter (octets of the length field) used to restore CTR_0 in Done + /// + FLengthFieldOctets : UInt16; + /// + /// True after Encode/Decode has produced a CBC-MAC state for Done + /// + FMacReady : Boolean; /// /// Encodes or decodes a block of data using the supplied cipher @@ -69,6 +82,11 @@ TCCM = class(TAuthenticatedCipherModesBase) /// When true it is encrypting data, else it is descrypting data /// procedure EncodeDecode(Source, Dest: PUInt8Array; Size: Integer; Encode: Boolean); + /// + /// Derives CalculatedAuthenticationTag from FMacBlock (S_0 XOR T). + /// Called from Done after Encode/Decode have finished the CBC-MAC. + /// + procedure FinalizeAuthenticationTag; strict protected /// /// Defines the length of the resulting authentication value in bit. @@ -127,6 +145,14 @@ TCCM = class(TAuthenticatedCipherModesBase) Dest : PUInt8Array; Size : Integer); override; + /// + /// Materializes CalculatedAuthenticationTag from the CBC-MAC state. + /// 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. + /// + procedure Done; override; + /// /// Returns a list of authentication tag lengths explicitely specified by /// the official specification of the standard. @@ -165,6 +191,7 @@ destructor TCCM.Destroy; ProtectBytes(FOrigInitVector); ProtectBuffer(FInitVector, SizeOf(FInitVector)); + ProtectBuffer(FMacBlock, SizeOf(FMacBlock)); ProtectBytes(FCalcAuthenticationTag); ProtectBytes(FExpectedAuthenticationTag); @@ -181,9 +208,6 @@ procedure TCCM.EncodeDecode(Source, Dest: PUInt8Array; Encode: Boolean); var ecc : TBlock16Byte; // encrypted counter - FixedTagBuf : TBlock16Byte; // during calculation buffer of authentication tag - // might need to be bigger than then one specified - // by the user len : Int32; k, L : UInt16; b : UInt8; @@ -211,6 +235,8 @@ procedure TCCM.EncodeDecode(Source, Dest: PUInt8Array; end; begin + CheckNotFinalized; + if (Size > 0) and ((not Assigned(Source)) or (not Assigned(Dest))) then raise EDECCipherException.Create(sInvalidSourcePointer); @@ -234,6 +260,7 @@ procedure TCCM.EncodeDecode(Source, Dest: PUInt8Array; // Force Length(FInitVector) + L = 15. Since nLen <= 13, L is at least 2 L := 15 - InitVectLen; + FLengthFieldOctets := L; // compose B_0 = Flags | Nonce N | l(m) // octet 0: Flags = 64*HdrPresent | 8*((tLen-2) div 2 | (L-1) @@ -354,15 +381,10 @@ procedure TCCM.EncodeDecode(Source, Dest: PUInt8Array; FEncryptionMethod(@Buf[0], @Buf[0], Length(Buf)); end; - // setup counter for the tag (zero the count) - for k := 15 downto 16-L do - FInitVector[k] := 0; - - FEncryptionMethod(@FInitVector[0], @ecc[0], Length(ecc)); - - // store the TAG/Authentication result value - XORBuffers(Buf[0], ecc[0], 16, FixedTagBuf); - Move(FixedTagBuf[0], FCalcAuthenticationTag[0], length(FCalcAuthenticationTag)); + // Keep CBC-MAC state for Done; do not materialize the tag here so CCM + // shares the Init → Encode/Decode* → Done → tag lifecycle with GCM. + Move(Buf[0], FMacBlock[0], SizeOf(FMacBlock)); + FMacReady := True; ProtectBuffer(Buf, SizeOf(Buf)); end; @@ -394,6 +416,48 @@ procedure TCCM.Init(EncryptionMethod : TEncodeDecodeMethod; inherited; FOrigInitVector := InitVector; + FMacReady := False; + FLengthFieldOctets := 0; +end; + +procedure TCCM.FinalizeAuthenticationTag; +var + ecc : TBlock16Byte; + FixedTagBuf : TBlock16Byte; + k : UInt16; +begin + // Restore CTR_0 (zero the count) and encrypt to get S_0, then tag = T XOR S_0. + // See RFC 3610 §2.6 / NIST SP 800-38C: authentication tag is not part of Encode. + for k := 15 downto 16 - FLengthFieldOctets do + begin + FInitVector[k] := 0; + end; + + FEncryptionMethod(@FInitVector[0], @ecc[0], Length(ecc)); + + XORBuffers(FMacBlock[0], ecc[0], 16, FixedTagBuf); + if (Length(FCalcAuthenticationTag) > 0) then + begin + Move(FixedTagBuf[0], FCalcAuthenticationTag[0], Length(FCalcAuthenticationTag)); + end; + + ProtectBuffer(ecc, SizeOf(ecc)); + ProtectBuffer(FixedTagBuf, SizeOf(FixedTagBuf)); +end; + +procedure TCCM.Done; +begin + if FFinalized then + Exit; + + if not FMacReady then + begin + // Empty payload / AAD-only: format B_0 with l(m)=0 and process AAD. + EncodeDecode(nil, nil, 0, True); + end; + + FinalizeAuthenticationTag; + inherited; end; end. diff --git a/Source/DECCipherModesGCM.pas b/Source/DECCipherModesGCM.pas index e88476e5..00f28261 100644 --- a/Source/DECCipherModesGCM.pas +++ b/Source/DECCipherModesGCM.pas @@ -104,11 +104,6 @@ TGCM = class(TAuthenticatedCipherModesBase) /// Number of unused bytes remaining in FKeystream (0..15) /// FKeystreamRemainLen : Integer; - /// - /// True after Done has materialized the authentication tag. - /// Prevents double-finalization and post-Done GHASH/CTR updates. - /// - FFinalized : Boolean; /// /// XOR implementation for unsigned 128 bit numbers @@ -346,8 +341,6 @@ TGCM = class(TAuthenticatedCipherModesBase) 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 = @@ -541,7 +534,6 @@ procedure TGCM.Init(EncryptionMethod : TEncodeDecodeMethod; FKeystreamRemainLen := 0; FKeystreamLeftover[0] := 0; FKeystreamLeftover[1] := 0; - FFinalized := False; OldH := FH; EncryptionMethod(@Nullbytes[0], @FH[0], 16); @@ -658,8 +650,9 @@ procedure TGCM.Done; begin if FFinalized then Exit; + FinalizeAuthenticationTag; - FFinalized := True; + inherited; end; function TGCM.CalcGaloisHash(AuthenticatedData : PUInt8Array; AuthLen : integer; Ciphertext : PUInt8Array; @@ -757,8 +750,7 @@ procedure TGCM.ApplyCTR(Source, Dest: PUInt8Array; Size: Integer); procedure TGCM.Decode(Source, Dest: PUInt8Array; Size: Integer); begin - if FFinalized then - raise EDECCipherException.CreateRes(@sGCMAlreadyFinalized); + CheckNotFinalized; // AAD into GHASH once; tag finalized in Done (supports multi-call streams) EnsureAuthDataHashed; @@ -778,8 +770,7 @@ procedure TGCM.Decode(Source, Dest: PUInt8Array; Size: Integer); procedure TGCM.Encode(Source, Dest: PUInt8Array; Size: Integer); begin - if FFinalized then - raise EDECCipherException.CreateRes(@sGCMAlreadyFinalized); + CheckNotFinalized; // AAD into GHASH once; tag finalized in Done (supports multi-call streams) EnsureAuthDataHashed; diff --git a/Unit Tests/Tests/TestDECCipherModesCCM.pas b/Unit Tests/Tests/TestDECCipherModesCCM.pas index fb827fe3..c83604c5 100644 --- a/Unit Tests/Tests/TestDECCipherModesCCM.pas +++ b/Unit Tests/Tests/TestDECCipherModesCCM.pas @@ -56,6 +56,8 @@ TestTDECCCM = class(TTestCase) procedure DoTestInitFailureIVTooShort; procedure DoTestRFC3610(EncodeTest: Boolean); procedure DoTestAuthenticationBitLengthWrong; + procedure DoReadTagBeforeDone; + procedure DoEncodeAfterDone; public procedure SetUp; override; procedure TearDown; override; @@ -85,6 +87,18 @@ TestTDECCCM = class(TTestCase) procedure TestGetStandardAuthenticationTagBitLengths; procedure TestGetExpectedAuthenticationResult; procedure TestSetExpectedAuthenticationResult; + /// + /// Reading CalculatedAuthenticationResult before Done must raise. + /// + procedure TestCalculatedAuthenticationResultBeforeDoneRaises; + /// + /// Encode after Done must raise until Init is called again. + /// + procedure TestEncodeAfterDoneRejected; + /// + /// Done twice must leave CalculatedAuthenticationResult unchanged. + /// + procedure TestDoneIdempotent; end; @@ -760,6 +774,7 @@ procedure TestTDECCCM.DoTestRFC3610(EncodeTest: Boolean); CheckEquals(true, CompareMem(@buf,@DecodeBuf,plen), 'Plaintext wrong'); end; + CipherAES.Done; TagResult := CipherAES.CalculatedAuthenticationResult; // Test the generated tag @@ -772,6 +787,73 @@ procedure TestTDECCCM.DoTestRFC3610(EncodeTest: Boolean); end; end; +procedure TestTDECCCM.DoReadTagBeforeDone; +var + Tag: TBytes; +begin + Tag := FCipherAES.CalculatedAuthenticationResult; +end; + +procedure TestTDECCCM.DoEncodeAfterDone; +var + Data: TBytes; +begin + SetLength(Data, 4); + FillChar(Data[0], Length(Data), $A5); + FCipherAES.EncodeBytes(Data); +end; + +procedure TestTDECCCM.TestCalculatedAuthenticationResultBeforeDoneRaises; +var + TestData: TSingleAuthenticatedTestData; +begin + TestData := FTestDataList[0].TestData[0]; + FCipherAES.Init(BytesOf(TFormat_HexL.Decode(TestData.CryptKey)), + BytesOf(TFormat_HexL.Decode(TestData.InitVector)), + $FF); + FCipherAES.AuthenticationResultBitLength := FTestDataList[0].Taglen; + FCipherAES.DataToAuthenticate := TFormat_HexL.Decode(BytesOf(TestData.AAD)); + FCipherAES.EncodeBytes(TFormat_HexL.Decode(BytesOf(TestData.PT))); + CheckException(DoReadTagBeforeDone, EDECCipherException, + 'CalculatedAuthenticationResult before Done must raise EDECCipherException'); +end; + +procedure TestTDECCCM.TestEncodeAfterDoneRejected; +var + TestData: TSingleAuthenticatedTestData; +begin + TestData := FTestDataList[0].TestData[0]; + FCipherAES.Init(BytesOf(TFormat_HexL.Decode(TestData.CryptKey)), + BytesOf(TFormat_HexL.Decode(TestData.InitVector)), + $FF); + FCipherAES.AuthenticationResultBitLength := FTestDataList[0].Taglen; + FCipherAES.DataToAuthenticate := TFormat_HexL.Decode(BytesOf(TestData.AAD)); + FCipherAES.EncodeBytes(TFormat_HexL.Decode(BytesOf(TestData.PT))); + FCipherAES.Done; + CheckException(DoEncodeAfterDone, EDECCipherException, + 'Encode after Done must raise EDECCipherException'); +end; + +procedure TestTDECCCM.TestDoneIdempotent; +var + TestData: TSingleAuthenticatedTestData; + Tag1, Tag2: TBytes; +begin + TestData := FTestDataList[0].TestData[0]; + FCipherAES.Init(BytesOf(TFormat_HexL.Decode(TestData.CryptKey)), + BytesOf(TFormat_HexL.Decode(TestData.InitVector)), + $FF); + FCipherAES.AuthenticationResultBitLength := FTestDataList[0].Taglen; + FCipherAES.DataToAuthenticate := TFormat_HexL.Decode(BytesOf(TestData.AAD)); + FCipherAES.EncodeBytes(TFormat_HexL.Decode(BytesOf(TestData.PT))); + FCipherAES.Done; + Tag1 := Copy(FCipherAES.CalculatedAuthenticationResult); + FCipherAES.Done; + Tag2 := FCipherAES.CalculatedAuthenticationResult; + CheckTrue(IsEqual(Tag1, Tag2), + 'Second Done must not change CalculatedAuthenticationResult'); +end; + initialization // Register all test cases to be run {$IFDEF DUnitX} diff --git a/Unit Tests/Tests/TestDECCipherModesGCM.pas b/Unit Tests/Tests/TestDECCipherModesGCM.pas index e640c14b..3b295967 100644 --- a/Unit Tests/Tests/TestDECCipherModesGCM.pas +++ b/Unit Tests/Tests/TestDECCipherModesGCM.pas @@ -158,6 +158,7 @@ TestTDECGCM = class(TTestCase) procedure DoDecodeAfterDone; procedure DoChangeAADAfterEncode; procedure DoSetAuthTagBitLengthTooLong; + procedure DoReadTagBeforeDone; public procedure SetUp; override; procedure TearDown; override; @@ -198,6 +199,10 @@ TestTDECGCM = class(TTestCase) /// procedure TestAADChangeAfterEncodeRejected; /// + /// Reading CalculatedAuthenticationResult before Done must raise. + /// + procedure TestCalculatedAuthenticationResultBeforeDoneRaises; + /// /// AuthenticationResultBitLength > 128 must raise (tag buffer is 16 bytes). /// procedure TestAuthTagBitLengthTooLongRejected; @@ -921,6 +926,27 @@ procedure TestTDECGCM.TestDoneIdempotent; 'Second Done must not change CalculatedAuthenticationResult'); end; +procedure TestTDECGCM.DoReadTagBeforeDone; +var + Tag: TBytes; +begin + Tag := FCipherAES.CalculatedAuthenticationResult; +end; + +procedure TestTDECGCM.TestCalculatedAuthenticationResultBeforeDoneRaises; +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(cCAVS_MultiChunkAAD)); + FCipherAES.EncodeBytes(ptBytes); + CheckException(DoReadTagBeforeDone, EDECCipherException, + 'CalculatedAuthenticationResult before Done must raise EDECCipherException'); +end; + procedure TestTDECGCM.DoEncodeAfterDone; begin FCipherAES.EncodeBytes(FCallAfterDoneData); From 807a931ac1faa682b6e09c86593f6b4bac34f468 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 15 Sep 2026 13:21:20 +0000 Subject: [PATCH 2/5] CCM multi-chunk streaming and keep 128-bit InitMode check Follow-up to PR #102 (points 2 and 4). CCM can process several Encode/Decode chunks when the total payload length is known (AuthenticatedPayloadLength or a one-shot Size / EncodeStream DataSize). B_0 still encodes l(m) as required by RFC 3610 / NIST SP 800-38C. InitMode keeps the 128-bit block-size check for both GCM and CCM; comments cite the original CCM spec, RFC 3610, and SP 800-38C/38D. SupportsAuthenticatedMultiChunk reports the capability. Co-authored-by: Olaf Monien --- Source/DECAuthenticatedCipherModesBase.pas | 61 +++- Source/DECCipherFormats.pas | 5 + Source/DECCipherModes.pas | 72 +++- Source/DECCipherModesCCM.pas | 404 +++++++++++++++------ Source/DECCipherModesGCM.pas | 14 + Unit Tests/Tests/TestDECCipherModesCCM.pas | 114 +++++- Unit Tests/Tests/TestDECCipherModesGCM.pas | 10 + 7 files changed, 563 insertions(+), 117 deletions(-) diff --git a/Source/DECAuthenticatedCipherModesBase.pas b/Source/DECAuthenticatedCipherModesBase.pas index 56d9783c..891ebcdf 100644 --- a/Source/DECAuthenticatedCipherModesBase.pas +++ b/Source/DECAuthenticatedCipherModesBase.pas @@ -71,9 +71,11 @@ EDECAuthLengthException = class(EDECException); /// called again. /// /// - /// GCM supports multi-call Encode/Decode. CCM currently remains one-shot - /// (single Encode/Decode with the full message length); the tag is still - /// materialized in Done so both modes share the same lifecycle. + /// GCM supports multi-call Encode/Decode without a pre-declared length. + /// CCM can process several Encode/Decode chunks if the total payload + /// length is known first (DeclarePayloadLength / one-shot Size). + /// The authentication tag is materialized in Done so both modes share + /// the same lifecycle. /// /// TAuthenticatedCipherModesBase = class(TObject) @@ -163,7 +165,8 @@ TAuthenticatedCipherModesBase = class(TObject) /// /// Encodes a block of data using the supplied cipher. May be called - /// multiple times for modes that support streaming (e.g. GCM). + /// multiple times for modes that support streaming (e.g. GCM, CCM + /// with a declared payload length). /// /// /// Plain text to encrypt @@ -179,7 +182,8 @@ TAuthenticatedCipherModesBase = class(TObject) Size : Integer); virtual; abstract; /// /// Decodes a block of data using the supplied cipher. May be called - /// multiple times for modes that support streaming (e.g. GCM). + /// multiple times for modes that support streaming (e.g. GCM, CCM + /// with a declared payload length). /// /// /// Encrypted ciphertext to decrypt @@ -202,6 +206,38 @@ TAuthenticatedCipherModesBase = class(TObject) /// procedure Done; virtual; + /// + /// True when Encode/Decode may be called more than once before Done. + /// GCM always supports this. CCM supports it when the total payload + /// length is known in advance (CCM is not an online AEAD: B_0 encodes + /// l(m); see RFC 3610 §1 and NIST SP 800-38C). + /// + /// + /// True if the mode can process the payload in several Encode/Decode calls + /// + function SupportsMultiChunk: Boolean; virtual; + + /// + /// Declares the total payload length in bytes. Required by CCM before + /// the first Encode/Decode when the message will be supplied in several + /// chunks. Ignored by GCM. A later call is ignored once a length has + /// been set or processing has started. One-shot Encode/Decode still + /// works without this: the first call's Size is treated as the total. + /// + /// + /// Total plaintext/ciphertext length in bytes (not including the tag) + /// + procedure DeclarePayloadLength(const AByteLength: UInt64); virtual; + + /// + /// Returns the payload length last declared via DeclarePayloadLength or + /// taken from a one-shot Encode/Decode. 0 if none. + /// + /// + /// Declared payload length in bytes + /// + function GetDeclaredPayloadLength: UInt64; virtual; + /// /// Returns a list of authentication tag lengths explicitely specified by /// the official specification of the standard. @@ -314,6 +350,21 @@ procedure TAuthenticatedCipherModesBase.Done; FFinalized := True; end; +function TAuthenticatedCipherModesBase.SupportsMultiChunk: Boolean; +begin + Result := False; +end; + +procedure TAuthenticatedCipherModesBase.DeclarePayloadLength(const AByteLength: UInt64); +begin + // Default: GCM and other online AEADs ignore a pre-declared length. +end; + +function TAuthenticatedCipherModesBase.GetDeclaredPayloadLength: UInt64; +begin + Result := 0; +end; + procedure TAuthenticatedCipherModesBase.SetAuthenticationTagLength(const Value: UInt32); begin FCalcAuthenticationTagLength := Value shr 3; diff --git a/Source/DECCipherFormats.pas b/Source/DECCipherFormats.pas index e9a03e66..0e9b7552 100644 --- a/Source/DECCipherFormats.pas +++ b/Source/DECCipherFormats.pas @@ -780,6 +780,11 @@ procedure TDECFormattedCipher.DoEncodeDecodeStream(const Source, Dest: TStream; if DataSize < 0 then DataSize := Source.Size - Pos; + if Assigned(FAuthObj) then + begin + FAuthObj.DeclarePayloadLength(UInt64(DataSize)); + end; + Max := Pos + DataSize; StartPos := Pos; doPadding := false; diff --git a/Source/DECCipherModes.pas b/Source/DECCipherModes.pas index 4a5e8176..41892e0e 100644 --- a/Source/DECCipherModes.pas +++ b/Source/DECCipherModes.pas @@ -138,6 +138,20 @@ TDECCipherModes = class(TDECCipher, IDECAuthenticatedCipher) /// authentication. /// procedure SetExpectedAuthenticationResult(const Value: TBytes); + /// + /// Returns the declared payload length for authenticated modes + /// + /// + /// Declared payload length in bytes, or 0 if none / not applicable + /// + function GetAuthenticatedPayloadLength: UInt64; + /// + /// Declares the total payload length for authenticated modes that need it + /// + /// + /// Total plaintext/ciphertext length in bytes + /// + procedure SetAuthenticatedPayloadLength(const Value: UInt64); strict protected /// /// Authenticated mode implementation (GCM, CCM, future AEAD modes). @@ -417,6 +431,27 @@ TDECCipherModes = class(TDECCipher, IDECAuthenticatedCipher) /// function GetStandardAuthenticationTagBitLengths:TStandardBitLengths; + /// + /// True when the current authenticated mode can process Encode/Decode + /// in several calls before Done. GCM always can. CCM can when the total + /// payload length is known in advance (set AuthenticatedPayloadLength, or + /// a single Encode/Decode/EncodeStream that covers the whole message). + /// + /// + /// True for GCM and CCM; False for non-authenticated modes + /// + function SupportsAuthenticatedMultiChunk: Boolean; + + /// + /// Total payload length in bytes for authenticated modes that need it + /// before processing (CCM). Ignored by GCM. Set this before the first + /// Encode/Decode when feeding CCM in several chunks. One EncodeStream + /// of the full message sets it automatically from DataSize. + /// + property AuthenticatedPayloadLength: UInt64 + read GetAuthenticatedPayloadLength + write SetAuthenticatedPayloadLength; + /// /// Some block chaining modes have the ability to authenticate the message /// in addition to encrypting it. This property contains the data which @@ -754,6 +789,30 @@ function TDECCipherModes.GetCalcAuthenticatonResult: TBytes; raise EDECCipherException.CreateResFmt(@sInvalidModeForMethod, ['cmGCM or cmCCM']); end; +function TDECCipherModes.SupportsAuthenticatedMultiChunk: Boolean; +begin + if Assigned(FAuthObj) then + Result := FAuthObj.SupportsMultiChunk + else + Result := False; +end; + +function TDECCipherModes.GetAuthenticatedPayloadLength: UInt64; +begin + if Assigned(FAuthObj) then + Result := FAuthObj.GetDeclaredPayloadLength + else + Result := 0; +end; + +procedure TDECCipherModes.SetAuthenticatedPayloadLength(const Value: UInt64); +begin + if Assigned(FAuthObj) then + FAuthObj.DeclarePayloadLength(Value) + 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 @@ -769,7 +828,18 @@ procedure TDECCipherModes.InitMode; end; end else - // GCM and CCM require a cipher with 128 bit block size + // Keep the 128-bit block-size requirement for both GCM and CCM. + // + // GCM: NIST SP 800-38D requires a 128-bit block cipher. Do not weaken + // that AES-GCM safeguard. + // + // CCM: the original specification (Whiting, Housley, Ferguson, NIST + // submission "Counter with CBC-MAC (CCM)") is only defined for 128-bit + // block ciphers, such as AES. RFC 3610 §1 and NIST SP 800-38C repeat + // the same restriction. The original paper notes that the ideas can be + // extended to other block sizes, but "this will require further + // definitions" — so a 64-bit cipher (e.g. Blowfish) is not CCM as + // specified. We therefore do not special-case or loosen this check. raise EDECCipherException.CreateResFmt(@sInvalidBlockSize, [128, GetEnumName(TypeInfo(TCipherMode), Integer(FMode))]); diff --git a/Source/DECCipherModesCCM.pas b/Source/DECCipherModesCCM.pas index 6adcf35f..860c3759 100644 --- a/Source/DECCipherModesCCM.pas +++ b/Source/DECCipherModesCCM.pas @@ -52,19 +52,70 @@ TCCM = class(TAuthenticatedCipherModesBase) /// FInitVector : TBlock16Byte; /// - /// CBC-MAC state after Encode/Decode; the authentication tag is derived - /// from this in Done (S_0 XOR T), not inside Encode/Decode. + /// CBC-MAC state; the authentication tag is derived from this in Done /// FMacBlock : TBlock16Byte; /// + /// Number of payload bytes already XORed into FMacBlock since the last AES + /// + FMacFill : Integer; + /// + /// Leftover CTR keystream for unaligned multi-chunk Encode/Decode + /// + FKeystream : TBlock16Byte; + /// + /// Next unused index in FKeystream + /// + FKeystreamOffset : Integer; + /// + /// Unused leftover keystream bytes (0..15) + /// + FKeystreamRemain : Integer; + /// /// L parameter (octets of the length field) used to restore CTR_0 in Done /// FLengthFieldOctets : UInt16; /// - /// True after Encode/Decode has produced a CBC-MAC state for Done + /// Total payload length in bytes (known before B_0 is formatted) + /// + FExpectedPayloadLength : UInt64; + /// + /// Payload bytes processed since Start + /// + FPayloadProcessed : UInt64; + /// + /// True after DeclarePayloadLength or after the first Encode/Decode + /// has taken Size as the total (one-shot) + /// + FPayloadLengthDeclared : Boolean; + /// + /// True after B_0, AAD and CTR have been set up /// - FMacReady : Boolean; + FStarted : Boolean; + /// + /// Increments the CCM counter in the last L octets of CTR + /// + /// + /// Counter block to increment in place + /// + procedure IncCTR(var ACTR: TBlock16Byte); + /// + /// Formats B_0 and AAD, then sets up CTR. Total payload length must + /// already be known (declared or taken from the first call's Size). + /// + /// + /// Total payload length l(m) encoded in B_0 + /// + procedure Start(const ATotalLength: UInt64); + /// + /// Starts processing on the first Encode/Decode if not already started + /// + /// + /// Size of this Encode/Decode call; used as the total when no length + /// was declared + /// + procedure EnsureStarted(AChunkSize: Integer); /// /// Encodes or decodes a block of data using the supplied cipher /// @@ -96,6 +147,13 @@ TCCM = class(TAuthenticatedCipherModesBase) /// are: 32, 48, 64, 80, 96, 112, 128 /// procedure SetAuthenticationTagLength(const Value: UInt32); override; + /// + /// Rejects AAD assignment after Encode/Decode has started or after Done + /// + /// + /// Additional authenticated data + /// + procedure SetDataToAuthenticate(const Value: TBytes); override; public /// /// Savely clear any buffers @@ -161,6 +219,31 @@ TCCM = class(TAuthenticatedCipherModesBase) /// List of bit lengths /// function GetStandardAuthenticationTagBitLengths:TStandardBitLengths; override; + + /// + /// CCM can process several Encode/Decode chunks when the total payload + /// length is known (DeclarePayloadLength or one-shot Size). CCM is not + /// an online AEAD: B_0 encodes l(m). See RFC 3610 §1 / NIST SP 800-38C. + /// + /// + /// True + /// + function SupportsMultiChunk: Boolean; override; + /// + /// Declares the total payload length in bytes before the first + /// Encode/Decode. Ignored if a length is already set or processing started. + /// + /// + /// Total plaintext/ciphertext length in bytes + /// + procedure DeclarePayloadLength(const AByteLength: UInt64); override; + /// + /// Returns the payload length declared for this CCM instance + /// + /// + /// Declared payload length in bytes + /// + function GetDeclaredPayloadLength: UInt64; override; end; implementation @@ -179,6 +262,12 @@ implementation /// Exception raised when a size but no source data pointer was passed /// sInvalidSourcePointer = 'No source data pointer passed'; + sCCMPayloadTooLong = + 'CCM payload exceeds the declared length'; + sCCMIncompletePayload = + 'CCM payload is shorter than the declared length'; + sCCMAADLocked = + 'CCM DataToAuthenticate cannot be changed after Encode/Decode has started or after Done'; procedure TCCM.Decode(Source, Dest: PUInt8Array; Size: Integer); begin @@ -190,8 +279,9 @@ destructor TCCM.Destroy; if (Length(FOrigInitVector) > 0) then ProtectBytes(FOrigInitVector); - ProtectBuffer(FInitVector, SizeOf(FInitVector)); - ProtectBuffer(FMacBlock, SizeOf(FMacBlock)); + ProtectBuffer(FInitVector, SizeOf(FInitVector)); + ProtectBuffer(FMacBlock, SizeOf(FMacBlock)); + ProtectBuffer(FKeystream, SizeOf(FKeystream)); ProtectBytes(FCalcAuthenticationTag); ProtectBytes(FExpectedAuthenticationTag); @@ -203,190 +293,267 @@ procedure TCCM.Encode(Source, Dest: PUInt8Array; Size: Integer); EncodeDecode(Source, Dest, Size, true); end; -procedure TCCM.EncodeDecode(Source, Dest: PUInt8Array; - Size: Integer; - Encode: Boolean); +procedure TCCM.IncCTR(var ACTR: TBlock16Byte); var - ecc : TBlock16Byte; // encrypted counter - len : Int32; - k, L : UInt16; - b : UInt8; - pb : PByte; - AuthDataLen : Integer; // Length of data to authenticate in bytes - InitVectLen : Integer; // Length of the init vector in bytes - - Buf : TBlock16Byte; - - // Increment CTR[15]..CTR[16-L] - procedure IncCTR(var CTR: TBlock16Byte); - var - j: integer; + j: Integer; +begin + for j := 15 downto 16 - FLengthFieldOctets do begin - for j := 15 downto 16-L do + if (ACTR[j] = $FF) then + ACTR[j] := 0 + else begin - if (CTR[j] = $FF) then - CTR[j] := 0 - else - begin - inc(CTR[j]); - exit; - end; + Inc(ACTR[j]); + Exit; end; end; +end; +procedure TCCM.DeclarePayloadLength(const AByteLength: UInt64); begin CheckNotFinalized; - if (Size > 0) and - ((not Assigned(Source)) or (not Assigned(Dest))) then - raise EDECCipherException.Create(sInvalidSourcePointer); + if FStarted or FPayloadLengthDeclared then + Exit; + + FExpectedPayloadLength := AByteLength; + FPayloadLengthDeclared := True; +end; + +function TCCM.SupportsMultiChunk: Boolean; +begin + Result := True; +end; +function TCCM.GetDeclaredPayloadLength: UInt64; +begin + Result := FExpectedPayloadLength; +end; + +procedure TCCM.SetDataToAuthenticate(const Value: TBytes); +begin + if FStarted or FFinalized then + raise EDECCipherException.CreateRes(@sCCMAADLocked); + + inherited SetDataToAuthenticate(Value); +end; + +procedure TCCM.Start(const ATotalLength: UInt64); +var + len : UInt64; + AADPos : Integer; + k, L : UInt16; + b : UInt8; + pb : PByte; + AuthDataLen : Integer; + InitVectLen : Integer; + Buf : TBlock16Byte; +begin AuthDataLen := Length(FDataToAuthenticate); InitVectLen := Length(FOrigInitVector); - // calculate L value = max(number of bytes needed for sLen, 15-nLen) - len := Size; + // L = bytes needed for l(m), then force nLen + L = 15 (RFC 3610 §2.1) + len := ATotalLength; L := 0; while (len > 0) do begin - inc(L); + Inc(L); len := len shr 8; end; - // Length of nonce (= init vector) is InitVectLen if (InitVectLen + L > 15) then raise EDECNonceLengthException.CreateFmt(sWrongNonceLengthDetailed, - [InitVectLen + L]); + [InitVectLen + L]); - // Force Length(FInitVector) + L = 15. Since nLen <= 13, L is at least 2 L := 15 - InitVectLen; FLengthFieldOctets := L; - // compose B_0 = Flags | Nonce N | l(m) - // octet 0: Flags = 64*HdrPresent | 8*((tLen-2) div 2 | (L-1) - if (AuthDataLen > 0) then b := 64 else b := 0; - // Typecast for L-1 possible, since L is at least 2, see comment above - Buf[0] := b or ((FCalcAuthenticationTagLength-2) shl 2) or UInt16(L-1); - // octets 1..15-L is nonce - pb := @FOrigInitvector[0]; - for k := 1 to 15-L do + Buf[0] := b or ((FCalcAuthenticationTagLength - 2) shl 2) or UInt16(L - 1); + pb := @FOrigInitVector[0]; + for k := 1 to 15 - L do begin Buf[k] := pb^; - inc(pb); + Inc(pb); end; - // octets 16-L .. 15: l(m) - len := Size; + len := ATotalLength; for k := 1 to L do begin - Buf[16-k] := len and $FF; + Buf[16 - k] := len and $FF; len := len shr 8; end; FEncryptionMethod(@Buf[0], @Buf[0], Length(Buf)); - // process header if (AuthDataLen > 0) then begin - // octets 0..1: encoding of hLen. Note: since we allow max $FEFF bytes - // only these two octets are used. Generally up to 10 octets are needed. Buf[0] := Buf[0] xor (AuthDataLen shr 8); Buf[1] := Buf[1] xor (AuthDataLen and $FF); - // now append the hdr data - len := 2; - pb := @FDataToAuthenticate[0]; - for k:= 1 to AuthDataLen do + AADPos := 2; + pb := @FDataToAuthenticate[0]; + for k := 1 to AuthDataLen do begin - if (len = 16) then + if (AADPos = 16) then begin FEncryptionMethod(@Buf[0], @Buf[0], Length(Buf)); - len := 0; + AADPos := 0; end; - Buf[len] := Buf[len] xor pb^; - inc(len); - inc(pb); + Buf[AADPos] := Buf[AADPos] xor pb^; + Inc(AADPos); + Inc(pb); end; - if (len <> 0) then + if (AADPos <> 0) then FEncryptionMethod(@Buf[0], @Buf[0], Length(Buf)); end; - // setup the counter for source text processing pb := @FOrigInitVector[0]; - FInitVector[0] := (L-1) and $FF; + FInitVector[0] := (L - 1) and $FF; for k := 1 to 15 do begin - if (k < 16-L) then + if (k < 16 - L) then begin FInitVector[k] := pb^; - inc(pb); + Inc(pb); end else FInitVector[k] := 0; end; - // process full source text blocks - while (Size >= 16) do + Move(Buf[0], FMacBlock[0], SizeOf(FMacBlock)); + FMacFill := 0; + FKeystreamRemain := 0; + FKeystreamOffset := 0; + FPayloadProcessed := 0; + FStarted := True; + + ProtectBuffer(Buf, SizeOf(Buf)); +end; + +procedure TCCM.EnsureStarted(AChunkSize: Integer); +var + TotalLen: UInt64; +begin + if FStarted then + Exit; + + if FPayloadLengthDeclared then + TotalLen := FExpectedPayloadLength + else + begin + if AChunkSize < 0 then + TotalLen := 0 + else + TotalLen := UInt64(AChunkSize); + FExpectedPayloadLength := TotalLen; + FPayloadLengthDeclared := True; + end; + + Start(TotalLen); +end; + +procedure TCCM.EncodeDecode(Source, Dest: PUInt8Array; + Size: Integer; + Encode: Boolean); +var + ecc : TBlock16Byte; + b : UInt8; + pSrc : PByte; + pDst : PByte; + + procedure AbsorbMacByte(const APlain: UInt8); + begin + FMacBlock[FMacFill] := FMacBlock[FMacFill] xor APlain; + Inc(FMacFill); + if FMacFill = 16 then + begin + FEncryptionMethod(@FMacBlock[0], @FMacBlock[0], SizeOf(FMacBlock)); + FMacFill := 0; + end; + end; + + function NextKeystreamByte: UInt8; + begin + if FKeystreamRemain = 0 then + begin + IncCTR(FInitVector); + FEncryptionMethod(@FInitVector[0], @FKeystream[0], SizeOf(FKeystream)); + FKeystreamOffset := 0; + FKeystreamRemain := 16; + end; + Result := FKeystream[FKeystreamOffset]; + Inc(FKeystreamOffset); + Dec(FKeystreamRemain); + end; +begin + CheckNotFinalized; + + if Size < 0 then + begin + Size := 0; + end; + + if (Size > 0) and + ((not Assigned(Source)) or (not Assigned(Dest))) then + raise EDECCipherException.Create(sInvalidSourcePointer); + + EnsureStarted(Size); + + if (UInt64(Size) + FPayloadProcessed) > FExpectedPayloadLength then + raise EDECCipherException.CreateRes(@sCCMPayloadTooLong); + + // Fast path: 16-byte aligned blocks with no leftover MAC/keystream + while (Size >= 16) and (FMacFill = 0) and (FKeystreamRemain = 0) do begin IncCTR(FInitVector); - FEncryptionMethod(@FInitVector[0], @ecc[0], Length(FInitVector)); + FEncryptionMethod(@FInitVector[0], @ecc[0], SizeOf(ecc)); if Encode then begin - XORBuffers(Source[0], Buf[0], 16, Buf[0]); + XORBuffers(Source[0], FMacBlock[0], 16, FMacBlock[0]); XORBuffers(Source[0], ecc[0], 16, Dest[0]); end else begin XORBuffers(Source[0], ecc[0], 16, Dest[0]); - XORBuffers(Dest[0], Buf[0], 16, Buf[0]); + XORBuffers(Dest[0], FMacBlock[0], 16, FMacBlock[0]); end; - FEncryptionMethod(@Buf[0], @Buf[0], Length(Buf)); + FEncryptionMethod(@FMacBlock[0], @FMacBlock[0], SizeOf(FMacBlock)); - inc(PByte(Source), cBlockSize); - inc(PByte(Dest), cBlockSize); - dec(Size, cBlockSize); + Inc(PByte(Source), cBlockSize); + Inc(PByte(Dest), cBlockSize); + Dec(Size, cBlockSize); + Inc(FPayloadProcessed, 16); end; - if (Size > 0) then - begin - // handle remaining bytes of source text - IncCTR(FInitVector); - - FEncryptionMethod(@FInitVector[0], @ecc[0], Length(ecc)); + pSrc := PByte(Source); + pDst := PByte(Dest); - for k := 0 to UInt16(Size - 1) do + while Size > 0 do + begin + if Encode then begin - if Encode then - begin - b := PByte(Source)^; - PByte(Dest)^ := b xor ecc[k]; - end - else - begin - b := PByte(Source)^ xor ecc[k]; - PByte(Dest)^ := b; - end; - Buf[k] := Buf[k] xor b; - inc(PByte(Source)); - inc(PByte(Dest)); + b := pSrc^; + pDst^ := b xor NextKeystreamByte; + AbsorbMacByte(b); + end + else + begin + b := pSrc^ xor NextKeystreamByte; + pDst^ := b; + AbsorbMacByte(b); end; - - FEncryptionMethod(@Buf[0], @Buf[0], Length(Buf)); + Inc(pSrc); + Inc(pDst); + Dec(Size); + Inc(FPayloadProcessed); end; - - // Keep CBC-MAC state for Done; do not materialize the tag here so CCM - // shares the Init → Encode/Decode* → Done → tag lifecycle with GCM. - Move(Buf[0], FMacBlock[0], SizeOf(FMacBlock)); - FMacReady := True; - - ProtectBuffer(Buf, SizeOf(Buf)); end; function TCCM.GetStandardAuthenticationTagBitLengths: TStandardBitLengths; @@ -416,8 +583,14 @@ procedure TCCM.Init(EncryptionMethod : TEncodeDecodeMethod; inherited; FOrigInitVector := InitVector; - FMacReady := False; + FStarted := False; + FPayloadLengthDeclared := False; + FExpectedPayloadLength := 0; + FPayloadProcessed := 0; FLengthFieldOctets := 0; + FMacFill := 0; + FKeystreamRemain := 0; + FKeystreamOffset := 0; end; procedure TCCM.FinalizeAuthenticationTag; @@ -450,10 +623,21 @@ procedure TCCM.Done; if FFinalized then Exit; - if not FMacReady then + if not FStarted then begin // Empty payload / AAD-only: format B_0 with l(m)=0 and process AAD. - EncodeDecode(nil, nil, 0, True); + EnsureStarted(0); + end; + + if FPayloadProcessed < FExpectedPayloadLength then + raise EDECCipherException.CreateRes(@sCCMIncompletePayload); + + // Last partial CBC-MAC block is padded with implicit zeros (already in + // the un-xored tail of FMacBlock) and encrypted here, not at chunk boundaries. + if FMacFill > 0 then + begin + FEncryptionMethod(@FMacBlock[0], @FMacBlock[0], SizeOf(FMacBlock)); + FMacFill := 0; end; FinalizeAuthenticationTag; diff --git a/Source/DECCipherModesGCM.pas b/Source/DECCipherModesGCM.pas index 00f28261..281589eb 100644 --- a/Source/DECCipherModesGCM.pas +++ b/Source/DECCipherModesGCM.pas @@ -336,6 +336,15 @@ TGCM = class(TAuthenticatedCipherModesBase) /// List of bit lengths /// function GetStandardAuthenticationTagBitLengths:TStandardBitLengths; override; + + /// + /// GCM is an online AEAD: Encode/Decode may be called multiple times + /// without declaring the payload length in advance. + /// + /// + /// True + /// + function SupportsMultiChunk: Boolean; override; end; implementation @@ -799,6 +808,11 @@ function TGCM.GetStandardAuthenticationTagBitLengths: TStandardBitLengths; Result := [96, 104, 112, 120, 128]; end; +function TGCM.SupportsMultiChunk: Boolean; +begin + Result := True; +end; + // //function decrypt( const key, IV : TBytes; out plaintext : TBytes; const authenticated_data, //ciphertext : TBytes; len_auth_tag : integer; const authenticaton_tag : TBytes ) : boolean; diff --git a/Unit Tests/Tests/TestDECCipherModesCCM.pas b/Unit Tests/Tests/TestDECCipherModesCCM.pas index c83604c5..c1f324ab 100644 --- a/Unit Tests/Tests/TestDECCipherModesCCM.pas +++ b/Unit Tests/Tests/TestDECCipherModesCCM.pas @@ -79,7 +79,6 @@ TestTDECCCM = class(TTestCase) procedure TestInitFailureIVTooLong; procedure TestInitFailureIVTooShort; procedure TestEncodeStream; - // Deferred: multi-call CCM streams (AEAD roadmap) procedure TestGetDataToAuthenticate; procedure TestSetDataToAuthenticate; procedure TestSetAuthenticationBitLengths; @@ -99,6 +98,27 @@ TestTDECCCM = class(TTestCase) /// Done twice must leave CalculatedAuthenticationResult unchanged. /// procedure TestDoneIdempotent; + /// + /// CCM reports SupportsAuthenticatedMultiChunk = True. + /// + procedure TestSupportsAuthenticatedMultiChunk; + /// + /// RFC 3610 packet 1 encoded as 8+15 byte chunks with declared length. + /// + procedure TestEncodeMultiChunkUneven; + /// + /// RFC 3610 packet 1 decoded as 7+16 byte chunks with declared length. + /// + procedure TestDecodeMultiChunkUneven; + /// + /// EncodeStream of the full RFC vector with a small stream buffer so + /// Encode is called several times internally. + /// + procedure TestEncodeStreamInternalChunks; + /// + /// Several EncodeStream calls covering the message after declaring length. + /// + procedure TestEncodeStreamMultiChunk; end; @@ -495,6 +515,7 @@ procedure TestTDECCCM.DoTestEncodeStream_TestSingleSet(const aSetIndex, FCipherAES.AuthenticationResultBitLength := TestDataSet.Taglen; FCipherAES.DataToAuthenticate := TFormat_HexL.Decode( BytesOf(TestData.AAD)); + FCipherAES.AuthenticatedPayloadLength := UInt64(Length(ptBytes)); ptbStream := TBytesStream.Create(ptBytes); ctbStream := TBytesStream.Create; @@ -854,6 +875,97 @@ procedure TestTDECCCM.TestDoneIdempotent; 'Second Done must not change CalculatedAuthenticationResult'); end; +procedure TestTDECCCM.TestSupportsAuthenticatedMultiChunk; +begin + CheckTrue(FCipherAES.SupportsAuthenticatedMultiChunk, + 'CCM must report multi-chunk support (declared payload length)'); +end; + +procedure TestTDECCCM.TestEncodeMultiChunkUneven; +var + TestData: TSingleAuthenticatedTestData; + PT, CT, Chunk, AllCT: TBytes; +begin + // RFC 3610 Packet 1 (already in FTestDataList[0]): 23-byte PT, 64-bit tag + TestData := FTestDataList[0].TestData[0]; + PT := TFormat_HexL.Decode(BytesOf(TestData.PT)); + CheckEquals(23, Length(PT), 'RFC 3610 packet 1 PT is 23 bytes'); + + FCipherAES.Init(BytesOf(TFormat_HexL.Decode(TestData.CryptKey)), + BytesOf(TFormat_HexL.Decode(TestData.InitVector)), + $FF); + FCipherAES.AuthenticationResultBitLength := FTestDataList[0].Taglen; + FCipherAES.DataToAuthenticate := TFormat_HexL.Decode(BytesOf(TestData.AAD)); + FCipherAES.AuthenticatedPayloadLength := UInt64(Length(PT)); + + Chunk := FCipherAES.EncodeBytes(Copy(PT, 0, 8)); + SetLength(AllCT, Length(Chunk)); + if Length(Chunk) > 0 then + Move(Chunk[0], AllCT[0], Length(Chunk)); + Chunk := FCipherAES.EncodeBytes(Copy(PT, 8, 15)); + SetLength(AllCT, Length(AllCT) + Length(Chunk)); + if Length(Chunk) > 0 then + Move(Chunk[0], AllCT[Length(AllCT) - Length(Chunk)], Length(Chunk)); + FCipherAES.Done; + + CheckEquals(string(TestData.CT), StringOf(TFormat_HexL.Encode(AllCT)), + 'Ciphertext mismatch for CCM multi-chunk 8+15'); + CheckEquals(string(TestData.TagResult), + StringOf(TFormat_HexL.Encode(FCipherAES.CalculatedAuthenticationResult)), + 'Tag mismatch for CCM multi-chunk 8+15'); +end; + +procedure TestTDECCCM.TestDecodeMultiChunkUneven; +var + TestData: TSingleAuthenticatedTestData; + CT, PT, Chunk, AllPT: TBytes; +begin + TestData := FTestDataList[0].TestData[0]; + CT := TFormat_HexL.Decode(BytesOf(TestData.CT)); + + FCipherAES.Init(BytesOf(TFormat_HexL.Decode(TestData.CryptKey)), + BytesOf(TFormat_HexL.Decode(TestData.InitVector)), + $FF); + FCipherAES.AuthenticationResultBitLength := FTestDataList[0].Taglen; + FCipherAES.DataToAuthenticate := TFormat_HexL.Decode(BytesOf(TestData.AAD)); + FCipherAES.ExpectedAuthenticationResult := + TFormat_HexL.Decode(BytesOf(TestData.TagResult)); + FCipherAES.AuthenticatedPayloadLength := UInt64(Length(CT)); + + Chunk := FCipherAES.DecodeBytes(Copy(CT, 0, 7)); + SetLength(AllPT, Length(Chunk)); + if Length(Chunk) > 0 then + Move(Chunk[0], AllPT[0], Length(Chunk)); + Chunk := FCipherAES.DecodeBytes(Copy(CT, 7, 16)); + SetLength(AllPT, Length(AllPT) + Length(Chunk)); + if Length(Chunk) > 0 then + Move(Chunk[0], AllPT[Length(AllPT) - Length(Chunk)], Length(Chunk)); + FCipherAES.Done; + + CheckEquals(string(TestData.PT), StringOf(TFormat_HexL.Encode(AllPT)), + 'Plaintext mismatch for CCM multi-chunk decode 7+16'); +end; + +procedure TestTDECCCM.TestEncodeStreamInternalChunks; +var + SavedBufferSize: Integer; +begin + // StreamBufferSize is rounded up to the AES block size (16), so a 23-byte + // RFC vector is processed as 16+7 — still two Encode calls inside one stream. + SavedBufferSize := StreamBufferSize; + StreamBufferSize := 8; + try + DoTestEncodeStream_TestSingleSet(0, 0, -1); + finally + StreamBufferSize := SavedBufferSize; + end; +end; + +procedure TestTDECCCM.TestEncodeStreamMultiChunk; +begin + DoTestEncodeStream_TestSingleSet(0, 0, 8); +end; + initialization // Register all test cases to be run {$IFDEF DUnitX} diff --git a/Unit Tests/Tests/TestDECCipherModesGCM.pas b/Unit Tests/Tests/TestDECCipherModesGCM.pas index 3b295967..6b0de3fe 100644 --- a/Unit Tests/Tests/TestDECCipherModesGCM.pas +++ b/Unit Tests/Tests/TestDECCipherModesGCM.pas @@ -212,6 +212,10 @@ TestTDECGCM = class(TTestCase) procedure TestGetExpectedAuthenticationResult; procedure TestSetExpectedAuthenticationResult; + /// + /// GCM reports SupportsAuthenticatedMultiChunk = True. + /// + procedure TestSupportsAuthenticatedMultiChunk; /// /// Test for GitHub issue #86 /// @@ -1142,6 +1146,12 @@ procedure TestTDECGCM.TestGetStandardAuthenticationTagBitLengths; CheckEquals(128, BitLengths[4]); end; +procedure TestTDECGCM.TestSupportsAuthenticatedMultiChunk; +begin + CheckTrue(FCipherAES.SupportsAuthenticatedMultiChunk, + 'GCM must report multi-chunk support'); +end; + procedure TestTDECGCM.TestSetExpectedAuthenticationResult; var Exp, Act: TBytes; From d1cfd0a51cb68166b943dfde479a37ef43290367 Mon Sep 17 00:00:00 2001 From: Markus Humm Date: Wed, 16 Sep 2026 23:19:29 +0200 Subject: [PATCH 3/5] Messed up unremoved FFinalized removed. Unit Tests work now. --- Source/DECAuthenticatedCipherModesBase.pas | 7 ------- 1 file changed, 7 deletions(-) diff --git a/Source/DECAuthenticatedCipherModesBase.pas b/Source/DECAuthenticatedCipherModesBase.pas index 56d9783c..7fa3b6a6 100644 --- a/Source/DECAuthenticatedCipherModesBase.pas +++ b/Source/DECAuthenticatedCipherModesBase.pas @@ -102,13 +102,6 @@ TAuthenticatedCipherModesBase = class(TObject) /// FEncryptionMethod : TEncodeDecodeMethod; - /// - /// True after Done has materialized the authentication tag. Reading - /// CalculatedAuthenticationTag before this is set raises. Encode/Decode - /// after finalization also raises until Init is called again. - /// - FFinalized : Boolean; - /// /// Defines the length of the resulting authentication value in bit. /// From edb8351ff6360f07b825ec876edd72cd70754af1 Mon Sep 17 00:00:00 2001 From: Markus Humm Date: Wed, 16 Sep 2026 23:30:47 +0200 Subject: [PATCH 4/5] Accidentially removed flag readded --- Source/DECAuthenticatedCipherModesBase.pas | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Source/DECAuthenticatedCipherModesBase.pas b/Source/DECAuthenticatedCipherModesBase.pas index 7fa3b6a6..56d9783c 100644 --- a/Source/DECAuthenticatedCipherModesBase.pas +++ b/Source/DECAuthenticatedCipherModesBase.pas @@ -102,6 +102,13 @@ TAuthenticatedCipherModesBase = class(TObject) /// FEncryptionMethod : TEncodeDecodeMethod; + /// + /// True after Done has materialized the authentication tag. Reading + /// CalculatedAuthenticationTag before this is set raises. Encode/Decode + /// after finalization also raises until Init is called again. + /// + FFinalized : Boolean; + /// /// Defines the length of the resulting authentication value in bit. /// From 8a3d3c8085699a0ddacc32fb7354a2db25ac1e5f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 18 Sep 2026 13:19:24 +0000 Subject: [PATCH 5/5] CCM: harden DeclarePayloadLength against illegal states Make TCCM.DeclarePayloadLength fail hard after start or when a different length is declared, while still allowing an idempotent re-declare of the same length. EncodeStream only auto-declares when no length is set yet so multi-chunk streams keep working. Follow-up to #106. Co-authored-by: Olaf Monien --- Source/DECAuthenticatedCipherModesBase.pas | 8 +- Source/DECCipherFormats.pas | 5 +- Source/DECCipherModes.pas | 6 +- Source/DECCipherModesCCM.pas | 22 +++- Unit Tests/Tests/TestDECCipherModesCCM.pas | 131 +++++++++++++++++++++ 5 files changed, 164 insertions(+), 8 deletions(-) diff --git a/Source/DECAuthenticatedCipherModesBase.pas b/Source/DECAuthenticatedCipherModesBase.pas index 891ebcdf..be1ccfac 100644 --- a/Source/DECAuthenticatedCipherModesBase.pas +++ b/Source/DECAuthenticatedCipherModesBase.pas @@ -220,9 +220,11 @@ TAuthenticatedCipherModesBase = class(TObject) /// /// Declares the total payload length in bytes. Required by CCM before /// the first Encode/Decode when the message will be supplied in several - /// chunks. Ignored by GCM. A later call is ignored once a length has - /// been set or processing has started. One-shot Encode/Decode still - /// works without this: the first call's Size is treated as the total. + /// chunks. Ignored by GCM. For CCM, repeating the same length is + /// idempotent; a different length, a call after Encode/Decode has + /// started, or a call after Done raises EDECCipherException. One-shot + /// Encode/Decode still works without this: the first call's Size is + /// treated as the total. /// /// /// Total plaintext/ciphertext length in bytes (not including the tag) diff --git a/Source/DECCipherFormats.pas b/Source/DECCipherFormats.pas index 0e9b7552..a7068cd0 100644 --- a/Source/DECCipherFormats.pas +++ b/Source/DECCipherFormats.pas @@ -780,7 +780,10 @@ procedure TDECFormattedCipher.DoEncodeDecodeStream(const Source, Dest: TStream; if DataSize < 0 then DataSize := Source.Size - Pos; - if Assigned(FAuthObj) then + // One-shot authenticated streams declare DataSize as l(m) for CCM. Skip when + // a non-zero length is already set so multi-chunk EncodeStream can follow + // AuthenticatedPayloadLength without re-declaring the chunk size. + if Assigned(FAuthObj) and (FAuthObj.GetDeclaredPayloadLength = 0) then begin FAuthObj.DeclarePayloadLength(UInt64(DataSize)); end; diff --git a/Source/DECCipherModes.pas b/Source/DECCipherModes.pas index 41892e0e..badbde44 100644 --- a/Source/DECCipherModes.pas +++ b/Source/DECCipherModes.pas @@ -445,8 +445,10 @@ TDECCipherModes = class(TDECCipher, IDECAuthenticatedCipher) /// /// Total payload length in bytes for authenticated modes that need it /// before processing (CCM). Ignored by GCM. Set this before the first - /// Encode/Decode when feeding CCM in several chunks. One EncodeStream - /// of the full message sets it automatically from DataSize. + /// Encode/Decode when feeding CCM in several chunks. Repeating the same + /// length is allowed; a different value, a set after Encode/Decode has + /// started, or a set after Done raises EDECCipherException. One + /// EncodeStream of the full message sets it automatically from DataSize. /// property AuthenticatedPayloadLength: UInt64 read GetAuthenticatedPayloadLength diff --git a/Source/DECCipherModesCCM.pas b/Source/DECCipherModesCCM.pas index 860c3759..64e2cd34 100644 --- a/Source/DECCipherModesCCM.pas +++ b/Source/DECCipherModesCCM.pas @@ -231,11 +231,17 @@ TCCM = class(TAuthenticatedCipherModesBase) function SupportsMultiChunk: Boolean; override; /// /// Declares the total payload length in bytes before the first - /// Encode/Decode. Ignored if a length is already set or processing started. + /// Encode/Decode. Repeating the same length is idempotent. A different + /// length, a call after Encode/Decode has started, or a call after Done + /// raises EDECCipherException. /// /// /// Total plaintext/ciphertext length in bytes /// + /// + /// Raised after Done, after processing has started, or when a different + /// length is declared. + /// procedure DeclarePayloadLength(const AByteLength: UInt64); override; /// /// Returns the payload length declared for this CCM instance @@ -268,6 +274,10 @@ implementation 'CCM payload is shorter than the declared length'; sCCMAADLocked = 'CCM DataToAuthenticate cannot be changed after Encode/Decode has started or after Done'; + sCCMPayloadLengthAlreadyDeclared = + 'CCM payload length already declared as a different value'; + sCCMPayloadLengthLocked = + 'CCM payload length cannot be declared after Encode/Decode has started'; procedure TCCM.Decode(Source, Dest: PUInt8Array; Size: Integer); begin @@ -313,8 +323,16 @@ procedure TCCM.DeclarePayloadLength(const AByteLength: UInt64); begin CheckNotFinalized; - if FStarted or FPayloadLengthDeclared then + if FStarted then + raise EDECCipherException.CreateRes(@sCCMPayloadLengthLocked); + + if FPayloadLengthDeclared then + begin + if AByteLength <> FExpectedPayloadLength then + raise EDECCipherException.CreateRes(@sCCMPayloadLengthAlreadyDeclared); + Exit; + end; FExpectedPayloadLength := AByteLength; FPayloadLengthDeclared := True; diff --git a/Unit Tests/Tests/TestDECCipherModesCCM.pas b/Unit Tests/Tests/TestDECCipherModesCCM.pas index c1f324ab..4fa10669 100644 --- a/Unit Tests/Tests/TestDECCipherModesCCM.pas +++ b/Unit Tests/Tests/TestDECCipherModesCCM.pas @@ -47,6 +47,7 @@ TestTDECCCM = class(TTestCase) FTestDataList : TAuthenticatedTestDataList; FCipherAES : TCipher_AES; FTestBitLength : Integer; // AuthenticationBitLength for test for wring lengths + FTestPayloadLength : UInt64; // payload length used by CheckException helpers private function IsEqual(const a, b: TBytes): Boolean; procedure DoTestEncodeStream_LoadAndTestCAVSData(const aMaxChunkSize: Int64); @@ -58,6 +59,7 @@ TestTDECCCM = class(TTestCase) procedure DoTestAuthenticationBitLengthWrong; procedure DoReadTagBeforeDone; procedure DoEncodeAfterDone; + procedure DoSetAuthenticatedPayloadLength; public procedure SetUp; override; procedure TearDown; override; @@ -119,6 +121,26 @@ TestTDECCCM = class(TTestCase) /// Several EncodeStream calls covering the message after declaring length. /// procedure TestEncodeStreamMultiChunk; + /// + /// Re-declaring the same payload length is idempotent. + /// + procedure TestDeclarePayloadLengthIdempotent; + /// + /// Re-declaring a different payload length must raise. + /// + procedure TestDeclarePayloadLengthDifferentRaises; + /// + /// Declaring payload length after Encode has started must raise. + /// + procedure TestDeclarePayloadLengthAfterEncodeRaises; + /// + /// Declaring payload length after Decode has started must raise. + /// + procedure TestDeclarePayloadLengthAfterDecodeRaises; + /// + /// Declaring payload length after Done must raise via CheckNotFinalized. + /// + procedure TestDeclarePayloadLengthAfterDoneRaises; end; @@ -966,6 +988,115 @@ procedure TestTDECCCM.TestEncodeStreamMultiChunk; DoTestEncodeStream_TestSingleSet(0, 0, 8); end; +procedure TestTDECCCM.DoSetAuthenticatedPayloadLength; +begin + FCipherAES.AuthenticatedPayloadLength := FTestPayloadLength; +end; + +procedure TestTDECCCM.TestDeclarePayloadLengthIdempotent; +var + TestData: TSingleAuthenticatedTestData; + PT: TBytes; +begin + TestData := FTestDataList[0].TestData[0]; + PT := TFormat_HexL.Decode(BytesOf(TestData.PT)); + + FCipherAES.Init(BytesOf(TFormat_HexL.Decode(TestData.CryptKey)), + BytesOf(TFormat_HexL.Decode(TestData.InitVector)), + $FF); + FCipherAES.AuthenticationResultBitLength := FTestDataList[0].Taglen; + FCipherAES.DataToAuthenticate := TFormat_HexL.Decode(BytesOf(TestData.AAD)); + + FCipherAES.AuthenticatedPayloadLength := UInt64(Length(PT)); + FCipherAES.AuthenticatedPayloadLength := UInt64(Length(PT)); + + CheckEquals(Length(PT), Integer(FCipherAES.AuthenticatedPayloadLength), + 'Re-declaring the same payload length must keep the declared value'); +end; + +procedure TestTDECCCM.TestDeclarePayloadLengthDifferentRaises; +var + TestData: TSingleAuthenticatedTestData; + PT: TBytes; +begin + TestData := FTestDataList[0].TestData[0]; + PT := TFormat_HexL.Decode(BytesOf(TestData.PT)); + + FCipherAES.Init(BytesOf(TFormat_HexL.Decode(TestData.CryptKey)), + BytesOf(TFormat_HexL.Decode(TestData.InitVector)), + $FF); + FCipherAES.AuthenticationResultBitLength := FTestDataList[0].Taglen; + FCipherAES.DataToAuthenticate := TFormat_HexL.Decode(BytesOf(TestData.AAD)); + + FCipherAES.AuthenticatedPayloadLength := UInt64(Length(PT)); + FTestPayloadLength := UInt64(Length(PT) + 1); + CheckException(DoSetAuthenticatedPayloadLength, EDECCipherException, + 'Re-declaring a different payload length must raise EDECCipherException'); +end; + +procedure TestTDECCCM.TestDeclarePayloadLengthAfterEncodeRaises; +var + TestData: TSingleAuthenticatedTestData; + PT: TBytes; +begin + TestData := FTestDataList[0].TestData[0]; + PT := TFormat_HexL.Decode(BytesOf(TestData.PT)); + + FCipherAES.Init(BytesOf(TFormat_HexL.Decode(TestData.CryptKey)), + BytesOf(TFormat_HexL.Decode(TestData.InitVector)), + $FF); + FCipherAES.AuthenticationResultBitLength := FTestDataList[0].Taglen; + FCipherAES.DataToAuthenticate := TFormat_HexL.Decode(BytesOf(TestData.AAD)); + FCipherAES.EncodeBytes(PT); + + FTestPayloadLength := UInt64(Length(PT)); + CheckException(DoSetAuthenticatedPayloadLength, EDECCipherException, + 'Declaring payload length after Encode has started must raise EDECCipherException'); +end; + +procedure TestTDECCCM.TestDeclarePayloadLengthAfterDecodeRaises; +var + TestData: TSingleAuthenticatedTestData; + CT: TBytes; +begin + TestData := FTestDataList[0].TestData[0]; + CT := TFormat_HexL.Decode(BytesOf(TestData.CT)); + + FCipherAES.Init(BytesOf(TFormat_HexL.Decode(TestData.CryptKey)), + BytesOf(TFormat_HexL.Decode(TestData.InitVector)), + $FF); + FCipherAES.AuthenticationResultBitLength := FTestDataList[0].Taglen; + FCipherAES.DataToAuthenticate := TFormat_HexL.Decode(BytesOf(TestData.AAD)); + FCipherAES.ExpectedAuthenticationResult := + TFormat_HexL.Decode(BytesOf(TestData.TagResult)); + FCipherAES.DecodeBytes(CT); + + FTestPayloadLength := UInt64(Length(CT)); + CheckException(DoSetAuthenticatedPayloadLength, EDECCipherException, + 'Declaring payload length after Decode has started must raise EDECCipherException'); +end; + +procedure TestTDECCCM.TestDeclarePayloadLengthAfterDoneRaises; +var + TestData: TSingleAuthenticatedTestData; + PT: TBytes; +begin + TestData := FTestDataList[0].TestData[0]; + PT := TFormat_HexL.Decode(BytesOf(TestData.PT)); + + FCipherAES.Init(BytesOf(TFormat_HexL.Decode(TestData.CryptKey)), + BytesOf(TFormat_HexL.Decode(TestData.InitVector)), + $FF); + FCipherAES.AuthenticationResultBitLength := FTestDataList[0].Taglen; + FCipherAES.DataToAuthenticate := TFormat_HexL.Decode(BytesOf(TestData.AAD)); + FCipherAES.EncodeBytes(PT); + FCipherAES.Done; + + FTestPayloadLength := UInt64(Length(PT)); + CheckException(DoSetAuthenticatedPayloadLength, EDECCipherException, + 'Declaring payload length after Done must raise EDECCipherException'); +end; + initialization // Register all test cases to be run {$IFDEF DUnitX}