diff --git a/Source/DECAuthenticatedCipherModesBase.pas b/Source/DECAuthenticatedCipherModesBase.pas index 891ebcd..be1ccfa 100644 --- a/Source/DECAuthenticatedCipherModesBase.pas +++ b/Source/DECAuthenticatedCipherModesBase.pas @@ -220,9 +220,11 @@ TAuthenticatedCipherModesBase = class(TObject) /// /// Declares the total payload length in bytes. Required by CCM before /// the first Encode/Decode when the message will be supplied in several - /// chunks. Ignored by GCM. A later call is ignored once a length has - /// been set or processing has started. One-shot Encode/Decode still - /// works without this: the first call's Size is treated as the total. + /// chunks. Ignored by GCM. For CCM, repeating the same length is + /// idempotent; a different length, a call after Encode/Decode has + /// started, or a call after Done raises EDECCipherException. One-shot + /// Encode/Decode still works without this: the first call's Size is + /// treated as the total. /// /// /// Total plaintext/ciphertext length in bytes (not including the tag) diff --git a/Source/DECCipherFormats.pas b/Source/DECCipherFormats.pas index 0e9b755..a7068cd 100644 --- a/Source/DECCipherFormats.pas +++ b/Source/DECCipherFormats.pas @@ -780,7 +780,10 @@ procedure TDECFormattedCipher.DoEncodeDecodeStream(const Source, Dest: TStream; if DataSize < 0 then DataSize := Source.Size - Pos; - if Assigned(FAuthObj) then + // One-shot authenticated streams declare DataSize as l(m) for CCM. Skip when + // a non-zero length is already set so multi-chunk EncodeStream can follow + // AuthenticatedPayloadLength without re-declaring the chunk size. + if Assigned(FAuthObj) and (FAuthObj.GetDeclaredPayloadLength = 0) then begin FAuthObj.DeclarePayloadLength(UInt64(DataSize)); end; diff --git a/Source/DECCipherModes.pas b/Source/DECCipherModes.pas index 41892e0..badbde4 100644 --- a/Source/DECCipherModes.pas +++ b/Source/DECCipherModes.pas @@ -445,8 +445,10 @@ TDECCipherModes = class(TDECCipher, IDECAuthenticatedCipher) /// /// Total payload length in bytes for authenticated modes that need it /// before processing (CCM). Ignored by GCM. Set this before the first - /// Encode/Decode when feeding CCM in several chunks. One EncodeStream - /// of the full message sets it automatically from DataSize. + /// Encode/Decode when feeding CCM in several chunks. Repeating the same + /// length is allowed; a different value, a set after Encode/Decode has + /// started, or a set after Done raises EDECCipherException. One + /// EncodeStream of the full message sets it automatically from DataSize. /// property AuthenticatedPayloadLength: UInt64 read GetAuthenticatedPayloadLength diff --git a/Source/DECCipherModesCCM.pas b/Source/DECCipherModesCCM.pas index 860c375..64e2cd3 100644 --- a/Source/DECCipherModesCCM.pas +++ b/Source/DECCipherModesCCM.pas @@ -231,11 +231,17 @@ TCCM = class(TAuthenticatedCipherModesBase) function SupportsMultiChunk: Boolean; override; /// /// Declares the total payload length in bytes before the first - /// Encode/Decode. Ignored if a length is already set or processing started. + /// Encode/Decode. Repeating the same length is idempotent. A different + /// length, a call after Encode/Decode has started, or a call after Done + /// raises EDECCipherException. /// /// /// Total plaintext/ciphertext length in bytes /// + /// + /// Raised after Done, after processing has started, or when a different + /// length is declared. + /// procedure DeclarePayloadLength(const AByteLength: UInt64); override; /// /// Returns the payload length declared for this CCM instance @@ -268,6 +274,10 @@ implementation 'CCM payload is shorter than the declared length'; sCCMAADLocked = 'CCM DataToAuthenticate cannot be changed after Encode/Decode has started or after Done'; + sCCMPayloadLengthAlreadyDeclared = + 'CCM payload length already declared as a different value'; + sCCMPayloadLengthLocked = + 'CCM payload length cannot be declared after Encode/Decode has started'; procedure TCCM.Decode(Source, Dest: PUInt8Array; Size: Integer); begin @@ -313,8 +323,16 @@ procedure TCCM.DeclarePayloadLength(const AByteLength: UInt64); begin CheckNotFinalized; - if FStarted or FPayloadLengthDeclared then + if FStarted then + raise EDECCipherException.CreateRes(@sCCMPayloadLengthLocked); + + if FPayloadLengthDeclared then + begin + if AByteLength <> FExpectedPayloadLength then + raise EDECCipherException.CreateRes(@sCCMPayloadLengthAlreadyDeclared); + Exit; + end; FExpectedPayloadLength := AByteLength; FPayloadLengthDeclared := True; diff --git a/Unit Tests/Tests/TestDECCipherModesCCM.pas b/Unit Tests/Tests/TestDECCipherModesCCM.pas index c1f324a..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); @@ -58,6 +59,7 @@ TestTDECCCM = class(TTestCase) procedure DoTestAuthenticationBitLengthWrong; procedure DoReadTagBeforeDone; procedure DoEncodeAfterDone; + procedure DoSetAuthenticatedPayloadLength; public procedure SetUp; override; procedure TearDown; override; @@ -119,6 +121,26 @@ TestTDECCCM = class(TTestCase) /// Several EncodeStream calls covering the message after declaring length. /// procedure TestEncodeStreamMultiChunk; + /// + /// Re-declaring the same payload length is idempotent. + /// + procedure TestDeclarePayloadLengthIdempotent; + /// + /// Re-declaring a different payload length must raise. + /// + procedure TestDeclarePayloadLengthDifferentRaises; + /// + /// Declaring payload length after Encode has started must raise. + /// + procedure TestDeclarePayloadLengthAfterEncodeRaises; + /// + /// Declaring payload length after Decode has started must raise. + /// + procedure TestDeclarePayloadLengthAfterDecodeRaises; + /// + /// Declaring payload length after Done must raise via CheckNotFinalized. + /// + procedure TestDeclarePayloadLengthAfterDoneRaises; end; @@ -966,6 +988,115 @@ procedure TestTDECCCM.TestEncodeStreamMultiChunk; DoTestEncodeStream_TestSingleSet(0, 0, 8); end; +procedure TestTDECCCM.DoSetAuthenticatedPayloadLength; +begin + FCipherAES.AuthenticatedPayloadLength := FTestPayloadLength; +end; + +procedure TestTDECCCM.TestDeclarePayloadLengthIdempotent; +var + TestData: TSingleAuthenticatedTestData; + PT: TBytes; +begin + TestData := FTestDataList[0].TestData[0]; + PT := TFormat_HexL.Decode(BytesOf(TestData.PT)); + + FCipherAES.Init(BytesOf(TFormat_HexL.Decode(TestData.CryptKey)), + BytesOf(TFormat_HexL.Decode(TestData.InitVector)), + $FF); + FCipherAES.AuthenticationResultBitLength := FTestDataList[0].Taglen; + FCipherAES.DataToAuthenticate := TFormat_HexL.Decode(BytesOf(TestData.AAD)); + + FCipherAES.AuthenticatedPayloadLength := UInt64(Length(PT)); + FCipherAES.AuthenticatedPayloadLength := UInt64(Length(PT)); + + CheckEquals(Length(PT), Integer(FCipherAES.AuthenticatedPayloadLength), + 'Re-declaring the same payload length must keep the declared value'); +end; + +procedure TestTDECCCM.TestDeclarePayloadLengthDifferentRaises; +var + TestData: TSingleAuthenticatedTestData; + PT: TBytes; +begin + TestData := FTestDataList[0].TestData[0]; + PT := TFormat_HexL.Decode(BytesOf(TestData.PT)); + + FCipherAES.Init(BytesOf(TFormat_HexL.Decode(TestData.CryptKey)), + BytesOf(TFormat_HexL.Decode(TestData.InitVector)), + $FF); + FCipherAES.AuthenticationResultBitLength := FTestDataList[0].Taglen; + FCipherAES.DataToAuthenticate := TFormat_HexL.Decode(BytesOf(TestData.AAD)); + + FCipherAES.AuthenticatedPayloadLength := UInt64(Length(PT)); + FTestPayloadLength := UInt64(Length(PT) + 1); + CheckException(DoSetAuthenticatedPayloadLength, EDECCipherException, + 'Re-declaring a different payload length must raise EDECCipherException'); +end; + +procedure TestTDECCCM.TestDeclarePayloadLengthAfterEncodeRaises; +var + TestData: TSingleAuthenticatedTestData; + PT: TBytes; +begin + TestData := FTestDataList[0].TestData[0]; + PT := TFormat_HexL.Decode(BytesOf(TestData.PT)); + + FCipherAES.Init(BytesOf(TFormat_HexL.Decode(TestData.CryptKey)), + BytesOf(TFormat_HexL.Decode(TestData.InitVector)), + $FF); + FCipherAES.AuthenticationResultBitLength := FTestDataList[0].Taglen; + FCipherAES.DataToAuthenticate := TFormat_HexL.Decode(BytesOf(TestData.AAD)); + FCipherAES.EncodeBytes(PT); + + FTestPayloadLength := UInt64(Length(PT)); + CheckException(DoSetAuthenticatedPayloadLength, EDECCipherException, + 'Declaring payload length after Encode has started must raise EDECCipherException'); +end; + +procedure TestTDECCCM.TestDeclarePayloadLengthAfterDecodeRaises; +var + TestData: TSingleAuthenticatedTestData; + CT: TBytes; +begin + TestData := FTestDataList[0].TestData[0]; + CT := TFormat_HexL.Decode(BytesOf(TestData.CT)); + + FCipherAES.Init(BytesOf(TFormat_HexL.Decode(TestData.CryptKey)), + BytesOf(TFormat_HexL.Decode(TestData.InitVector)), + $FF); + FCipherAES.AuthenticationResultBitLength := FTestDataList[0].Taglen; + FCipherAES.DataToAuthenticate := TFormat_HexL.Decode(BytesOf(TestData.AAD)); + FCipherAES.ExpectedAuthenticationResult := + TFormat_HexL.Decode(BytesOf(TestData.TagResult)); + FCipherAES.DecodeBytes(CT); + + FTestPayloadLength := UInt64(Length(CT)); + CheckException(DoSetAuthenticatedPayloadLength, EDECCipherException, + 'Declaring payload length after Decode has started must raise EDECCipherException'); +end; + +procedure TestTDECCCM.TestDeclarePayloadLengthAfterDoneRaises; +var + TestData: TSingleAuthenticatedTestData; + PT: TBytes; +begin + TestData := FTestDataList[0].TestData[0]; + PT := TFormat_HexL.Decode(BytesOf(TestData.PT)); + + FCipherAES.Init(BytesOf(TFormat_HexL.Decode(TestData.CryptKey)), + BytesOf(TFormat_HexL.Decode(TestData.InitVector)), + $FF); + FCipherAES.AuthenticationResultBitLength := FTestDataList[0].Taglen; + FCipherAES.DataToAuthenticate := TFormat_HexL.Decode(BytesOf(TestData.AAD)); + FCipherAES.EncodeBytes(PT); + FCipherAES.Done; + + FTestPayloadLength := UInt64(Length(PT)); + CheckException(DoSetAuthenticatedPayloadLength, EDECCipherException, + 'Declaring payload length after Done must raise EDECCipherException'); +end; + initialization // Register all test cases to be run {$IFDEF DUnitX}