From a99f35be51d78ea990a6c3237235ff83af8cc6b3 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 15 Sep 2026 13:11:12 +0000 Subject: [PATCH] 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);