diff --git a/Source/DECAuthenticatedCipherModesBase.pas b/Source/DECAuthenticatedCipherModesBase.pas index 85b05b5..be1ccfa 100644 --- a/Source/DECAuthenticatedCipherModesBase.pas +++ b/Source/DECAuthenticatedCipherModesBase.pas @@ -59,19 +59,23 @@ 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 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) @@ -100,6 +104,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 +131,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 @@ -136,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 @@ -152,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 @@ -169,17 +200,53 @@ 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; + /// + /// 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. 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) + /// + 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. /// /// - /// 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 +267,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 +292,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 +305,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 +344,27 @@ 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; + +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); diff --git a/Source/DECCipherFormats.pas b/Source/DECCipherFormats.pas index c459893..a7068cd 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); @@ -780,6 +780,14 @@ procedure TDECFormattedCipher.DoEncodeDecodeStream(const Source, Dest: TStream; if DataSize < 0 then DataSize := Source.Size - Pos; + // 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; + Max := Pos + DataSize; StartPos := Pos; doPadding := false; diff --git a/Source/DECCipherInterface.pas b/Source/DECCipherInterface.pas index a4e1c6e..f9a280a 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 db5889d..badbde4 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; /// @@ -137,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). @@ -235,16 +250,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 +348,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,11 +424,36 @@ 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; + /// + /// 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. 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 + write SetAuthenticatedPayloadLength; + /// /// Some block chaining modes have the ability to authenticate the message /// in addition to encrypting it. This property contains the data which @@ -430,12 +478,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 +577,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; @@ -742,6 +791,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 @@ -757,7 +830,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))]); @@ -854,23 +938,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 +991,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 +1031,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 +1198,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 3f835f6..64e2cd3 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,7 +51,71 @@ TCCM = class(TAuthenticatedCipherModesBase) /// Init vector which is modified during processing /// FInitVector : TBlock16Byte; + /// + /// 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; + /// + /// 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 + /// + 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 /// @@ -69,6 +133,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. @@ -78,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 @@ -127,6 +203,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. @@ -135,6 +219,37 @@ 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. 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 + /// + /// + /// Declared payload length in bytes + /// + function GetDeclaredPayloadLength: UInt64; override; end; implementation @@ -153,6 +268,16 @@ 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'; + 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 @@ -164,7 +289,9 @@ destructor TCCM.Destroy; if (Length(FOrigInitVector) > 0) then ProtectBytes(FOrigInitVector); - ProtectBuffer(FInitVector, SizeOf(FInitVector)); + ProtectBuffer(FInitVector, SizeOf(FInitVector)); + ProtectBuffer(FMacBlock, SizeOf(FMacBlock)); + ProtectBuffer(FKeystream, SizeOf(FKeystream)); ProtectBytes(FCalcAuthenticationTag); ProtectBytes(FExpectedAuthenticationTag); @@ -176,195 +303,275 @@ 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 - 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; - 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 - if (Size > 0) and - ((not Assigned(Source)) or (not Assigned(Dest))) then - raise EDECCipherException.Create(sInvalidSourcePointer); + CheckNotFinalized; + + 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; +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; - - // compose B_0 = Flags | Nonce N | l(m) - // octet 0: Flags = 64*HdrPresent | 8*((tLen-2) div 2 | (L-1) + FLengthFieldOctets := L; 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; - - // 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)); - - ProtectBuffer(Buf, SizeOf(Buf)); end; function TCCM.GetStandardAuthenticationTagBitLengths: TStandardBitLengths; @@ -394,6 +601,65 @@ procedure TCCM.Init(EncryptionMethod : TEncodeDecodeMethod; inherited; FOrigInitVector := InitVector; + FStarted := False; + FPayloadLengthDeclared := False; + FExpectedPayloadLength := 0; + FPayloadProcessed := 0; + FLengthFieldOctets := 0; + FMacFill := 0; + FKeystreamRemain := 0; + FKeystreamOffset := 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 FStarted then + begin + // Empty payload / AAD-only: format B_0 with l(m)=0 and process AAD. + 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; + inherited; end; end. diff --git a/Source/DECCipherModesGCM.pas b/Source/DECCipherModesGCM.pas index e88476e..281589e 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 @@ -341,13 +336,20 @@ 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 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 +543,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 +659,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 +759,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 +779,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; @@ -808,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 fb827fe..4fa1066 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); @@ -56,6 +57,9 @@ TestTDECCCM = class(TTestCase) procedure DoTestInitFailureIVTooShort; procedure DoTestRFC3610(EncodeTest: Boolean); procedure DoTestAuthenticationBitLengthWrong; + procedure DoReadTagBeforeDone; + procedure DoEncodeAfterDone; + procedure DoSetAuthenticatedPayloadLength; public procedure SetUp; override; procedure TearDown; override; @@ -77,7 +81,6 @@ TestTDECCCM = class(TTestCase) procedure TestInitFailureIVTooLong; procedure TestInitFailureIVTooShort; procedure TestEncodeStream; - // Deferred: multi-call CCM streams (AEAD roadmap) procedure TestGetDataToAuthenticate; procedure TestSetDataToAuthenticate; procedure TestSetAuthenticationBitLengths; @@ -85,6 +88,59 @@ 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; + /// + /// 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; + /// + /// 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; @@ -481,6 +537,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; @@ -760,6 +817,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 +830,273 @@ 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; + +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; + +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} diff --git a/Unit Tests/Tests/TestDECCipherModesGCM.pas b/Unit Tests/Tests/TestDECCipherModesGCM.pas index e640c14..6b0de3f 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; @@ -207,6 +212,10 @@ TestTDECGCM = class(TTestCase) procedure TestGetExpectedAuthenticationResult; procedure TestSetExpectedAuthenticationResult; + /// + /// GCM reports SupportsAuthenticatedMultiChunk = True. + /// + procedure TestSupportsAuthenticatedMultiChunk; /// /// Test for GitHub issue #86 /// @@ -921,6 +930,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); @@ -1116,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;