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;