diff --git a/src/libraries/System.Security.Cryptography.Primitives/ref/System.Security.Cryptography.Primitives.cs b/src/libraries/System.Security.Cryptography.Primitives/ref/System.Security.Cryptography.Primitives.cs index 0c7b73074e25f6..dbe106511bcdbf 100644 --- a/src/libraries/System.Security.Cryptography.Primitives/ref/System.Security.Cryptography.Primitives.cs +++ b/src/libraries/System.Security.Cryptography.Primitives/ref/System.Security.Cryptography.Primitives.cs @@ -248,6 +248,9 @@ public void Dispose() { } protected virtual void Dispose(bool disposing) { } public abstract void GenerateIV(); public abstract void GenerateKey(); + public int GetCiphertextLengthCbc(int plaintextLength, System.Security.Cryptography.PaddingMode paddingMode = System.Security.Cryptography.PaddingMode.PKCS7) { throw null; } + public int GetCiphertextLengthCfb(int plaintextLength, System.Security.Cryptography.PaddingMode paddingMode = System.Security.Cryptography.PaddingMode.None, int feedbackSizeInBits = 8) { throw null; } + public int GetCiphertextLengthEcb(int plaintextLength, System.Security.Cryptography.PaddingMode paddingMode) { throw null; } public bool ValidKeySize(int bitLength) { throw null; } } } diff --git a/src/libraries/System.Security.Cryptography.Primitives/src/Resources/Strings.resx b/src/libraries/System.Security.Cryptography.Primitives/src/Resources/Strings.resx index a1b8604ebcd3a0..75639b420001c3 100644 --- a/src/libraries/System.Security.Cryptography.Primitives/src/Resources/Strings.resx +++ b/src/libraries/System.Security.Cryptography.Primitives/src/Resources/Strings.resx @@ -72,6 +72,9 @@ Stream was not writable. + + The value specified in bits must be a whole number of bytes. + Non-negative number required. @@ -108,6 +111,15 @@ The specified OID ({0}) does not represent a known hash algorithm. + + The specified plaintext size is not valid for the the padding and block size. + + + The specified plaintext size is not valid for the the padding and feedback size. + + + The specified plaintext size is too large. + Method not supported. Derived class must override. @@ -129,4 +141,7 @@ The algorithm's implementation is incorrect. + + The algorithm's block size is not supported. + diff --git a/src/libraries/System.Security.Cryptography.Primitives/src/System/Security/Cryptography/SymmetricAlgorithm.cs b/src/libraries/System.Security.Cryptography.Primitives/src/System/Security/Cryptography/SymmetricAlgorithm.cs index 9ad7b01d7b6cf5..0ee176f21ec0ac 100644 --- a/src/libraries/System.Security.Cryptography.Primitives/src/System/Security/Cryptography/SymmetricAlgorithm.cs +++ b/src/libraries/System.Security.Cryptography.Primitives/src/System/Security/Cryptography/SymmetricAlgorithm.cs @@ -222,6 +222,212 @@ public bool ValidKeySize(int bitLength) return bitLength.IsLegalSize(validSizes); } + /// + /// Gets the length of a ciphertext with a given padding mode and plaintext length in ECB mode. + /// + /// The padding mode used to pad the plaintext to the algorithm's block size. + /// The plaintext length, in bytes. + /// The length, in bytes, of the ciphertext with padding. + /// + /// + /// is a negative number. + /// + /// + /// - or - + /// + /// + /// when padded is too large to represent as + /// a signed 32-bit integer. + /// + /// + /// - or - + /// + /// + /// is not a valid padding mode. + /// + /// + /// + /// + /// is not a positive integer. + /// + /// + /// - or - + /// + /// + /// is not a whole number of bytes. It must be divisible by 8. + /// + /// + /// + /// + /// The padding mode was used, but + /// is not a whole number of blocks. + /// + /// + public int GetCiphertextLengthEcb(int plaintextLength, PaddingMode paddingMode) => + GetCiphertextLengthBlockAligned(plaintextLength, paddingMode); + + /// + /// Gets the length of a ciphertext with a given padding mode and plaintext length in CBC mode. + /// + /// The padding mode used to pad the plaintext to the algorithm's block size. + /// The plaintext length, in bytes. + /// The length, in bytes, of the ciphertext with padding. + /// + /// + /// is a negative number. + /// + /// + /// - or - + /// + /// + /// when padded is too large to represent as + /// a signed 32-bit integer. + /// + /// + /// - or - + /// + /// + /// is not a valid padding mode. + /// + /// + /// + /// + /// is not a positive integer. + /// + /// + /// - or - + /// + /// + /// is not a whole number of bytes. It must be divisible by 8. + /// + /// + /// + /// + /// The padding mode was used, but + /// is not a whole number of blocks. + /// + /// + public int GetCiphertextLengthCbc(int plaintextLength, PaddingMode paddingMode = PaddingMode.PKCS7) => + GetCiphertextLengthBlockAligned(plaintextLength, paddingMode); + + private int GetCiphertextLengthBlockAligned(int plaintextLength, PaddingMode paddingMode) + { + if (plaintextLength < 0) + throw new ArgumentOutOfRangeException(nameof(plaintextLength), SR.ArgumentOutOfRange_NeedNonNegNum); + + int blockSizeBits = BlockSize; // The BlockSize property is in bits. + + if (blockSizeBits <= 0 || (blockSizeBits & 0b111) != 0) + throw new InvalidOperationException(SR.InvalidOperation_UnsupportedBlockSize); + + int blockSizeBytes = blockSizeBits >> 3; + int wholeBlocks = Math.DivRem(plaintextLength, blockSizeBytes, out int remainder) * blockSizeBytes; + + switch (paddingMode) + { + case PaddingMode.None when remainder != 0: + throw new ArgumentException(SR.Cryptography_MatchBlockSize, nameof(plaintextLength)); + case PaddingMode.None: + case PaddingMode.Zeros when remainder == 0: + return plaintextLength; + case PaddingMode.Zeros: + case PaddingMode.PKCS7: + case PaddingMode.ANSIX923: + case PaddingMode.ISO10126: + if (int.MaxValue - wholeBlocks < blockSizeBytes) + { + throw new ArgumentOutOfRangeException(nameof(plaintextLength), SR.Cryptography_PlaintextTooLarge); + } + + return wholeBlocks + blockSizeBytes; + default: + throw new ArgumentOutOfRangeException(nameof(paddingMode), SR.Cryptography_InvalidPaddingMode); + } + } + + /// + /// Gets the length of a ciphertext with a given padding mode and plaintext length in CFB mode. + /// + /// The padding mode used to pad the plaintext to the feedback size. + /// The plaintext length, in bytes. + /// The feedback size, in bits. + /// The length, in bytes, of the ciphertext with padding. + /// + /// + /// is not a positive number. + /// + /// + /// - or - + /// + /// + /// is a negative number. + /// + /// + /// - or - + /// + /// + /// when padded is too large to represent as + /// a signed 32-bit integer. + /// + /// + /// - or - + /// + /// + /// is not a valid padding mode. + /// + /// + /// + /// + /// The padding mode was used, but + /// is not a whole number of blocks. + /// + /// + /// - or - + /// + /// + /// is not a whole number of bytes. It must be divisible by 8. + /// + /// + /// + /// accepts any value that is a valid feedback size, regardless if the algorithm + /// supports the specified feedback size. + /// + public int GetCiphertextLengthCfb(int plaintextLength, PaddingMode paddingMode = PaddingMode.None, int feedbackSizeInBits = 8) + { + if (plaintextLength < 0) + throw new ArgumentOutOfRangeException(nameof(plaintextLength), SR.ArgumentOutOfRange_NeedNonNegNum); + + if (feedbackSizeInBits <= 0) + throw new ArgumentOutOfRangeException(nameof(feedbackSizeInBits), SR.ArgumentOutOfRange_NeedPosNum); + + if ((feedbackSizeInBits & 0b111) != 0) + throw new ArgumentException(SR.Argument_BitsMustBeWholeBytes, nameof(feedbackSizeInBits)); + + int feedbackSizeInBytes = feedbackSizeInBits >> 3; + int feedbackAligned = Math.DivRem(plaintextLength, feedbackSizeInBytes, out int remainder) * feedbackSizeInBytes; + + switch (paddingMode) + { + case PaddingMode.None when remainder != 0: + throw new ArgumentException(SR.Cryptography_MatchFeedbackSize, nameof(plaintextLength)); + case PaddingMode.None: + case PaddingMode.Zeros when remainder == 0: + return plaintextLength; + case PaddingMode.Zeros: + case PaddingMode.PKCS7: + case PaddingMode.ANSIX923: + case PaddingMode.ISO10126: + if (int.MaxValue - feedbackAligned < feedbackSizeInBytes) + { + throw new ArgumentOutOfRangeException(nameof(plaintextLength), SR.Cryptography_PlaintextTooLarge); + } + + return feedbackAligned + feedbackSizeInBytes; + default: + throw new ArgumentOutOfRangeException(nameof(paddingMode), SR.Cryptography_InvalidPaddingMode); + } + } + protected CipherMode ModeValue; protected PaddingMode PaddingValue; protected byte[]? KeyValue; diff --git a/src/libraries/System.Security.Cryptography.Primitives/tests/SymmetricAlgorithmTests.cs b/src/libraries/System.Security.Cryptography.Primitives/tests/SymmetricAlgorithmTests.cs new file mode 100644 index 00000000000000..8edfa2b0650aa5 --- /dev/null +++ b/src/libraries/System.Security.Cryptography.Primitives/tests/SymmetricAlgorithmTests.cs @@ -0,0 +1,235 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using Xunit; + +namespace System.Security.Cryptography.Primitives.Tests +{ + public static class SymmetricAlgorithmTests + { + [Theory] + [MemberData(nameof(CiphertextLengthTheories))] + public static void GetCiphertextLengthBlock_ValidInputs( + PaddingMode mode, + int plaintextSize, + int expectedCiphertextSize, + int alignmentSizeInBits) + { + AnySizeAlgorithm alg = new AnySizeAlgorithm { BlockSize = alignmentSizeInBits }; + int ciphertextSizeCbc = alg.GetCiphertextLengthCbc(plaintextSize, mode); + int ciphertextSizeEcb = alg.GetCiphertextLengthEcb(plaintextSize, mode); + Assert.Equal(expectedCiphertextSize, ciphertextSizeCbc); + Assert.Equal(expectedCiphertextSize, ciphertextSizeEcb); + } + + [Theory] + [MemberData(nameof(CiphertextLengthTheories))] + public static void GetCiphertextLengthCfb_ValidInputs( + PaddingMode mode, + int plaintextSize, + int expectedCiphertextSize, + int alignmentSizeInBits) + { + AnySizeAlgorithm alg = new AnySizeAlgorithm(); + int ciphertextSizeCfb = alg.GetCiphertextLengthCfb(plaintextSize, mode, alignmentSizeInBits); + Assert.Equal(expectedCiphertextSize, ciphertextSizeCfb); + } + + [Theory] + [MemberData(nameof(AllPaddingModes))] + public static void GetCiphertextLength_ThrowsForNegativeInput(PaddingMode mode) + { + AnySizeAlgorithm alg = new AnySizeAlgorithm { BlockSize = 128 }; + AssertExtensions.Throws("plaintextLength", () => alg.GetCiphertextLengthCbc(-1, mode)); + AssertExtensions.Throws("plaintextLength", () => alg.GetCiphertextLengthEcb(-1, mode)); + AssertExtensions.Throws("plaintextLength", () => alg.GetCiphertextLengthCfb(-1, mode)); + } + + [Theory] + [InlineData(PaddingMode.ANSIX923)] + [InlineData(PaddingMode.ISO10126)] + [InlineData(PaddingMode.PKCS7)] + [InlineData(PaddingMode.Zeros)] + public static void GetCiphertextLengthBlock_ThrowsForOverflow(PaddingMode mode) + { + AnySizeAlgorithm alg = new AnySizeAlgorithm { BlockSize = 128 }; + AssertExtensions.Throws("plaintextLength", () => alg.GetCiphertextLengthCbc(0x7FFFFFF1, mode)); + AssertExtensions.Throws("plaintextLength", () => alg.GetCiphertextLengthEcb(0x7FFFFFF1, mode)); + } + + [Theory] + [InlineData(PaddingMode.ANSIX923)] + [InlineData(PaddingMode.ISO10126)] + [InlineData(PaddingMode.PKCS7)] + [InlineData(PaddingMode.Zeros)] + public static void GetCiphertextLengthCfb_ThrowsForOverflow(PaddingMode mode) + { + AnySizeAlgorithm alg = new AnySizeAlgorithm(); + AssertExtensions.Throws("plaintextLength", () => + alg.GetCiphertextLengthCfb(0x7FFFFFFF, mode, feedbackSizeInBits: 128)); + } + + [Theory] + [MemberData(nameof(AllPaddingModes))] + public static void GetCiphertextLengthBlock_ThrowsForNonByteBlockSize(PaddingMode mode) + { + AnySizeAlgorithm alg = new AnySizeAlgorithm { BlockSize = 5 }; + Assert.Throws(() => alg.GetCiphertextLengthCbc(16, mode)); + Assert.Throws(() => alg.GetCiphertextLengthEcb(16, mode)); + } + + [Theory] + [MemberData(nameof(AllPaddingModes))] + public static void GetCiphertextLengthCfb_ThrowsForNonByteFeedbackSize(PaddingMode mode) + { + AnySizeAlgorithm alg = new AnySizeAlgorithm(); + AssertExtensions.Throws("feedbackSizeInBits", () => + alg.GetCiphertextLengthCfb(16, mode, 7)); + } + + [Theory] + [MemberData(nameof(AllPaddingModes))] + public static void GetCiphertextLengthBlock_ThrowsForZeroBlockSize(PaddingMode mode) + { + AnySizeAlgorithm alg = new AnySizeAlgorithm { BlockSize = 0 }; + Assert.Throws(() => alg.GetCiphertextLengthCbc(16, mode)); + Assert.Throws(() => alg.GetCiphertextLengthEcb(16, mode)); + } + + [Theory] + [MemberData(nameof(AllPaddingModes))] + public static void GetCiphertextLengthCfb_ThrowsForZeroFeedbackSize(PaddingMode mode) + { + AnySizeAlgorithm alg = new AnySizeAlgorithm(); + AssertExtensions.Throws("feedbackSizeInBits", () => + alg.GetCiphertextLengthCfb(16, mode, 0)); + } + + [Fact] + public static void GetCiphertextLength_ThrowsForInvalidPaddingMode() + { + AnySizeAlgorithm alg = new AnySizeAlgorithm { BlockSize = 128 }; + PaddingMode mode = (PaddingMode)(-1); + Assert.Throws("paddingMode", () => alg.GetCiphertextLengthCbc(16, mode)); + Assert.Throws("paddingMode", () => alg.GetCiphertextLengthEcb(16, mode)); + Assert.Throws("paddingMode", () => alg.GetCiphertextLengthCfb(16, mode)); + } + + [Fact] + public static void GetCiphertextLengthBlock_NoPaddingAndPlaintextSizeNotBlockAligned() + { + AnySizeAlgorithm alg = new AnySizeAlgorithm { BlockSize = 128 }; + Assert.Throws("plaintextLength", () => alg.GetCiphertextLengthCbc(17, PaddingMode.None)); + Assert.Throws("plaintextLength", () => alg.GetCiphertextLengthEcb(17, PaddingMode.None)); + } + + [Fact] + public static void GetCiphertextLengthCfb_NoPaddingAndPlaintextSizeNotFeedbackAligned() + { + AnySizeAlgorithm alg = new AnySizeAlgorithm(); + Assert.Throws("plaintextLength", () => + alg.GetCiphertextLengthCfb(17, PaddingMode.None, feedbackSizeInBits: 128)); + } + + public static IEnumerable CiphertextLengthTheories + { + get + { + // new object[] { PaddingMode mode, int plaintextSize, int expectedCiphertextSize, int alignmentSizeInBits } + + PaddingMode[] fullPaddings = new[] { + PaddingMode.ANSIX923, + PaddingMode.ISO10126, + PaddingMode.PKCS7, + }; + + foreach (PaddingMode mode in fullPaddings) + { + // 128-bit aligned value + yield return new object[] { mode, 0, 16, 128 }; + yield return new object[] { mode, 15, 16, 128 }; + yield return new object[] { mode, 16, 32, 128 }; + yield return new object[] { mode, 17, 32, 128 }; + yield return new object[] { mode, 1023, 1024, 128 }; + yield return new object[] { mode, 0x7FFFFFEF, 0x7FFFFFF0, 128 }; + + // 64-bit aligned value + yield return new object[] { mode, 0, 8, 64 }; + yield return new object[] { mode, 15, 16, 64 }; + yield return new object[] { mode, 16, 24, 64 }; + yield return new object[] { mode, 17, 24, 64 }; + yield return new object[] { mode, 1023, 1024, 64 }; + yield return new object[] { mode, 0x7FFFFFF7, 0x7FFFFFF8, 64 }; + + // 8-bit aligned value + yield return new object[] { mode, 0, 1, 8 }; + yield return new object[] { mode, 7, 8, 8 }; + yield return new object[] { mode, 16, 17, 8 }; + yield return new object[] { mode, 17, 18, 8 }; + yield return new object[] { mode, 1023, 1024, 8 }; + yield return new object[] { mode, 0x7FFFFFFE, 0x7FFFFFFF, 8 }; + + // 176-bit (22 byte) aligned value + yield return new object[] { mode, 0, 22, 176 }; + yield return new object[] { mode, 21, 22, 176 }; + yield return new object[] { mode, 22, 44, 176 }; + yield return new object[] { mode, 43, 44, 176 }; + yield return new object[] { mode, 1011, 1012, 176 }; + yield return new object[] { mode, 0x7FFFFFFD, 0x7FFFFFFE, 176 }; + } + + PaddingMode[] noPadOnAlignSize = new[] { + PaddingMode.Zeros, + PaddingMode.None, + }; + + foreach(PaddingMode mode in noPadOnAlignSize) + { + // 128-bit aligned + yield return new object[] { mode, 16, 16, 128 }; + yield return new object[] { mode, 00, 00, 128 }; + yield return new object[] { mode, 1024, 1024, 128 }; + yield return new object[] { mode, 0x7FFFFFF0, 0x7FFFFFF0, 128 }; + + // 8-bit aligned + yield return new object[] { mode, 0x7FFFFFFF, 0x7FFFFFFF, 8 }; + } + + // Pad only when length is not aligned + yield return new object[] { PaddingMode.Zeros, 15, 16, 128 }; + yield return new object[] { PaddingMode.Zeros, 17, 32, 128 }; + yield return new object[] { PaddingMode.Zeros, 0x7FFFFFEF, 0x7FFFFFF0, 128 }; + } + } + + public static IEnumerable AllPaddingModes + { + get + { + yield return new object[] { PaddingMode.ANSIX923 }; + yield return new object[] { PaddingMode.ISO10126 }; + yield return new object[] { PaddingMode.PKCS7 }; + yield return new object[] { PaddingMode.Zeros }; + yield return new object[] { PaddingMode.None }; + } + } + + private class AnySizeAlgorithm : SymmetricAlgorithm + { + public override int BlockSize + { + get => BlockSizeValue; + set => BlockSizeValue = value; + } + + public override ICryptoTransform CreateDecryptor(byte[] rgbKey, byte[] rgbIV) => + throw new NotImplementedException(); + public override ICryptoTransform CreateEncryptor(byte[] rgbKey, byte[] rgbIV) => + throw new NotImplementedException(); + public override void GenerateIV() => throw new NotImplementedException(); + public override void GenerateKey() => throw new NotImplementedException(); + } + } +} diff --git a/src/libraries/System.Security.Cryptography.Primitives/tests/System.Security.Cryptography.Primitives.Tests.csproj b/src/libraries/System.Security.Cryptography.Primitives/tests/System.Security.Cryptography.Primitives.Tests.csproj index caa018377958b3..9fde6f5f339cb0 100644 --- a/src/libraries/System.Security.Cryptography.Primitives/tests/System.Security.Cryptography.Primitives.Tests.csproj +++ b/src/libraries/System.Security.Cryptography.Primitives/tests/System.Security.Cryptography.Primitives.Tests.csproj @@ -22,6 +22,7 @@ +