Add one-shot ECB methods
authorKevin Jones <kevin@vcsjones.com>
Mon, 28 Jun 2021 15:55:27 +0000 (11:55 -0400)
committerGitHub <noreply@github.com>
Mon, 28 Jun 2021 15:55:27 +0000 (08:55 -0700)
This change adds SymmetricAlgorithm.EncryptEcb, SymmetricAlgorithm.DecryptEcb, their
respective Try- and -Core methods, derived type implementations thereof, and tests.

There's an open question of should these members on on the base class throw or
"succeed if the Mode property is in agreement with the algorithm".  While the latter is "nicer",
just throwing  is easier to reason about, and that's the current behavior.

Co-authored-by: Jeremy Barton <jbarton@microsoft.com>
48 files changed:
src/libraries/Common/src/Internal/Cryptography/BasicSymmetricCipherBCrypt.cs
src/libraries/Common/src/Internal/Cryptography/Helpers.cs
src/libraries/Common/src/Internal/Cryptography/UniversalCryptoDecryptor.cs
src/libraries/Common/src/Internal/Cryptography/UniversalCryptoEncryptor.cs
src/libraries/Common/src/Internal/Cryptography/UniversalCryptoTransform.cs
src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/AES/AesCipherTests.OneShot.cs [new file with mode: 0644]
src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/AES/AesCipherTests.cs
src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/DES/DESCipherTests.OneShot.cs [new file with mode: 0644]
src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/DES/DESCipherTests.cs
src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/RC2/RC2CipherTests.OneShot.cs [new file with mode: 0644]
src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/RC2/RC2CipherTests.cs
src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/TripleDES/TripleDESCipherTests.OneShot.cs [new file with mode: 0644]
src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/TripleDES/TripleDESCipherTests.cs
src/libraries/System.Security.Cryptography.Algorithms/src/Internal/Cryptography/AesImplementation.OSX.cs
src/libraries/System.Security.Cryptography.Algorithms/src/Internal/Cryptography/AesImplementation.Unix.cs
src/libraries/System.Security.Cryptography.Algorithms/src/Internal/Cryptography/AesImplementation.Windows.cs
src/libraries/System.Security.Cryptography.Algorithms/src/Internal/Cryptography/AesImplementation.cs
src/libraries/System.Security.Cryptography.Algorithms/src/Internal/Cryptography/AppleCCCryptor.cs
src/libraries/System.Security.Cryptography.Algorithms/src/Internal/Cryptography/DesImplementation.Android.cs
src/libraries/System.Security.Cryptography.Algorithms/src/Internal/Cryptography/DesImplementation.OSX.cs
src/libraries/System.Security.Cryptography.Algorithms/src/Internal/Cryptography/DesImplementation.Unix.cs
src/libraries/System.Security.Cryptography.Algorithms/src/Internal/Cryptography/DesImplementation.Windows.cs
src/libraries/System.Security.Cryptography.Algorithms/src/Internal/Cryptography/DesImplementation.cs
src/libraries/System.Security.Cryptography.Algorithms/src/Internal/Cryptography/RC2Implementation.Android.cs
src/libraries/System.Security.Cryptography.Algorithms/src/Internal/Cryptography/RC2Implementation.OSX.cs
src/libraries/System.Security.Cryptography.Algorithms/src/Internal/Cryptography/RC2Implementation.Unix.cs
src/libraries/System.Security.Cryptography.Algorithms/src/Internal/Cryptography/RC2Implementation.Windows.cs
src/libraries/System.Security.Cryptography.Algorithms/src/Internal/Cryptography/RC2Implementation.cs
src/libraries/System.Security.Cryptography.Algorithms/src/Internal/Cryptography/TripleDesImplementation.OSX.cs
src/libraries/System.Security.Cryptography.Algorithms/src/Internal/Cryptography/TripleDesImplementation.Unix.cs
src/libraries/System.Security.Cryptography.Algorithms/src/Internal/Cryptography/TripleDesImplementation.Windows.cs
src/libraries/System.Security.Cryptography.Algorithms/src/Internal/Cryptography/TripleDesImplementation.cs
src/libraries/System.Security.Cryptography.Algorithms/tests/System.Security.Cryptography.Algorithms.Tests.csproj
src/libraries/System.Security.Cryptography.Cng/src/Internal/Cryptography/BasicSymmetricCipherNCrypt.cs
src/libraries/System.Security.Cryptography.Cng/src/Internal/Cryptography/CngSymmetricAlgorithmCore.cs
src/libraries/System.Security.Cryptography.Cng/src/Internal/Cryptography/ICngSymmetricAlgorithm.cs
src/libraries/System.Security.Cryptography.Cng/src/System/Security/Cryptography/AesCng.cs
src/libraries/System.Security.Cryptography.Cng/src/System/Security/Cryptography/TripleDESCng.cs
src/libraries/System.Security.Cryptography.Cng/tests/AesCngTests.cs
src/libraries/System.Security.Cryptography.Cng/tests/SymmetricCngTestHelpers.cs
src/libraries/System.Security.Cryptography.Cng/tests/System.Security.Cryptography.Cng.Tests.csproj
src/libraries/System.Security.Cryptography.Cng/tests/TripleDESCngTests.cs
src/libraries/System.Security.Cryptography.Csp/src/System/Security/Cryptography/DESCryptoServiceProvider.Windows.cs
src/libraries/System.Security.Cryptography.Csp/tests/ShimHelpers.cs
src/libraries/System.Security.Cryptography.Primitives/ref/System.Security.Cryptography.Primitives.cs
src/libraries/System.Security.Cryptography.Primitives/src/Resources/Strings.resx
src/libraries/System.Security.Cryptography.Primitives/src/System/Security/Cryptography/SymmetricAlgorithm.cs
src/libraries/System.Security.Cryptography.Primitives/tests/SymmetricAlgorithmTests.cs

index 59d984f..348f1a3 100644 (file)
@@ -64,15 +64,27 @@ namespace Internal.Cryptography
             Debug.Assert(input.Length > 0);
             Debug.Assert((input.Length % PaddingSizeInBytes) == 0);
 
-            int numBytesWritten;
+            int numBytesWritten = 0;
 
-            if (_encrypting)
+            // BCryptEncrypt and BCryptDecrypt can do in place encryption, but if the buffers overlap
+            // the offset must be zero. In that case, we need to copy to a temporary location.
+            if (input.Overlaps(output, out int offset) && offset != 0)
             {
-                numBytesWritten = Interop.BCrypt.BCryptEncrypt(_hKey, input, _currentIv, output);
+                byte[] rented = CryptoPool.Rent(output.Length);
+
+                try
+                {
+                    numBytesWritten = BCryptTransform(input, rented);
+                    rented.AsSpan(0, numBytesWritten).CopyTo(output);
+                }
+                finally
+                {
+                    CryptoPool.Return(rented, clearSize: numBytesWritten);
+                }
             }
             else
             {
-                numBytesWritten = Interop.BCrypt.BCryptDecrypt(_hKey, input, _currentIv, output);
+                numBytesWritten = BCryptTransform(input, output);
             }
 
             if (numBytesWritten != input.Length)
@@ -84,6 +96,13 @@ namespace Internal.Cryptography
             }
 
             return numBytesWritten;
+
+            int BCryptTransform(ReadOnlySpan<byte> input, Span<byte> output)
+            {
+                return _encrypting ?
+                    Interop.BCrypt.BCryptEncrypt(_hKey, input, _currentIv, output) :
+                    Interop.BCrypt.BCryptDecrypt(_hKey, input, _currentIv, output);
+            }
         }
 
         public override int TransformFinal(ReadOnlySpan<byte> input, Span<byte> output)
index c299844..2ee5ffc 100644 (file)
@@ -30,11 +30,11 @@ namespace Internal.Cryptography
             return (byte[])(src.Clone());
         }
 
-        public static int GetPaddingSize(this SymmetricAlgorithm algorithm)
+        public static int GetPaddingSize(this SymmetricAlgorithm algorithm, CipherMode mode, int feedbackSizeBits)
         {
             // CFB8 does not require any padding at all
             // otherwise, it is always required to pad for block size
-            if (algorithm.Mode == CipherMode.CFB && algorithm.FeedbackSize == 8)
+            if (mode == CipherMode.CFB && feedbackSizeBits == 8)
                 return 1;
 
             return algorithm.BlockSize / 8;
index 3f050ff..0578bb3 100644 (file)
@@ -177,6 +177,62 @@ namespace Internal.Cryptography
             base.Dispose(disposing);
         }
 
+        public override unsafe bool TransformOneShot(ReadOnlySpan<byte> input, Span<byte> output, out int bytesWritten)
+        {
+            if (input.Length % PaddingSizeBytes != 0)
+                throw new CryptographicException(SR.Cryptography_PartialBlock);
+
+            // If there is no padding that needs to be removed, and the output buffer is large enough to hold
+            // the resulting plaintext, we can decrypt directly in to the output buffer.
+            // We do not do this for modes that require padding removal.
+            //
+            // This is not done for padded ciphertexts because we don't know if the padding is valid
+            // until it's been decrypted. We don't want to decrypt in to a user-supplied buffer and then throw
+            // a padding exception after we've already filled the user buffer with plaintext. We should only
+            // release the plaintext to the caller once we know the padding is valid.
+            if (!DepaddingRequired)
+            {
+                if (output.Length >= input.Length)
+                {
+                    bytesWritten = BasicSymmetricCipher.TransformFinal(input, output);
+                    return true;
+                }
+
+                // If no padding is going to be removed, we know the buffer is too small and we can bail out.
+                bytesWritten = 0;
+                return false;
+            }
+
+            byte[] rentedBuffer = CryptoPool.Rent(input.Length);
+            Span<byte> buffer = rentedBuffer.AsSpan(0, input.Length);
+            Span<byte> decryptedBuffer = default;
+
+            fixed (byte* pBuffer = buffer)
+            {
+                try
+                {
+                    int transformWritten = BasicSymmetricCipher.TransformFinal(input, buffer);
+                    decryptedBuffer = buffer.Slice(0, transformWritten);
+                    int unpaddedLength = GetPaddingLength(decryptedBuffer); // validates padding
+
+                    if (unpaddedLength > output.Length)
+                    {
+                        bytesWritten = 0;
+                        return false;
+                    }
+
+                    decryptedBuffer.Slice(0, unpaddedLength).CopyTo(output);
+                    bytesWritten = unpaddedLength;
+                    return true;
+                }
+                finally
+                {
+                    CryptographicOperations.ZeroMemory(decryptedBuffer);
+                    CryptoPool.Return(rentedBuffer, clearSize: 0); // ZeroMemory clears the part of the buffer that was written to.
+                }
+            }
+        }
+
         private void Reset()
         {
             if (_heldoverCipher != null)
index af88c7e..b913978 100644 (file)
@@ -56,6 +56,33 @@ namespace Internal.Cryptography
             return buffer;
         }
 
+        public override bool TransformOneShot(ReadOnlySpan<byte> input, Span<byte> output, out int bytesWritten)
+        {
+            int ciphertextLength = GetCiphertextLength(input.Length);
+
+            if (output.Length < ciphertextLength)
+            {
+                bytesWritten = 0;
+                return false;
+            }
+
+            // Copy the input to the output, and apply padding if required. This will not throw since the
+            // output length has already been checked, and PadBlock will not copy from input to output
+            // until it has checked that it will be able to apply padding correctly.
+            int padWritten = PadBlock(input, output);
+
+            // Do an in-place encrypt. All of our implementations support this, either natively
+            // or making a temporary buffer themselves if in-place is not supported by the native
+            // implementation.
+            Span<byte> paddedOutput = output.Slice(0, padWritten);
+            bytesWritten = BasicSymmetricCipher.TransformFinal(paddedOutput, paddedOutput);
+
+            // After padding, we should have an even number of blocks, and the same applies
+            // to the transform.
+            Debug.Assert(padWritten == bytesWritten);
+            return true;
+        }
+
         private int GetCiphertextLength(int plaintextLength)
         {
             Debug.Assert(plaintextLength >= 0);
index 23966df..a3a2216 100644 (file)
@@ -17,7 +17,7 @@ namespace Internal.Cryptography
     //
     internal abstract class UniversalCryptoTransform : ICryptoTransform
     {
-        public static ICryptoTransform Create(
+        public static UniversalCryptoTransform Create(
             PaddingMode paddingMode,
             BasicSymmetricCipher cipher,
             bool encrypting)
@@ -108,6 +108,8 @@ namespace Internal.Cryptography
             return output;
         }
 
+        public abstract bool TransformOneShot(ReadOnlySpan<byte> input, Span<byte> output, out int bytesWritten);
+
         protected virtual void Dispose(bool disposing)
         {
             if (disposing)
diff --git a/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/AES/AesCipherTests.OneShot.cs b/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/AES/AesCipherTests.OneShot.cs
new file mode 100644 (file)
index 0000000..fd82160
--- /dev/null
@@ -0,0 +1,638 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using System.Security.Cryptography;
+using Test.Cryptography;
+using Xunit;
+
+namespace System.Security.Cryptography.Encryption.Aes.Tests
+{
+    using Aes = System.Security.Cryptography.Aes;
+
+    public partial class AesCipherTests
+    {
+        private static byte[] s_aes128OneShotKey =
+            new byte[] { 0x00, 0x01, 0x02, 0x03, 0x05, 0x06, 0x07, 0x08, 0x0A, 0x0B, 0x0C, 0x0D, 0x0F, 0x10, 0x11, 0x12 };
+
+        [Theory]
+        [MemberData(nameof(EcbTestCases))]
+        public static void EcbRoundtrip(byte[] plaintext, byte[] ciphertext, PaddingMode padding)
+        {
+            using (Aes aes = AesFactory.Create())
+            {
+                aes.Key = s_aes128OneShotKey;
+
+                // Even though we have set the instance to use CFB, the Ecb one shots should
+                // always be done in ECB.
+                aes.FeedbackSize = 8;
+                aes.Mode = CipherMode.CFB;
+                aes.Padding = padding == PaddingMode.None ? PaddingMode.PKCS7 : PaddingMode.None;
+
+                byte[] encrypted = aes.EncryptEcb(plaintext, padding);
+                byte[] decrypted = aes.DecryptEcb(encrypted, padding);
+
+                if (padding == PaddingMode.Zeros)
+                {
+                    Assert.Equal(plaintext, decrypted[..plaintext.Length]);
+                    AssertFilledWith(0, plaintext.AsSpan(plaintext.Length));
+                }
+                else
+                {
+                    Assert.Equal(plaintext, decrypted);
+                }
+
+                decrypted = aes.DecryptEcb(ciphertext, padding);
+                encrypted = aes.EncryptEcb(decrypted, padding);
+
+                if (padding == PaddingMode.ISO10126)
+                {
+                    int blockSizeBytes = aes.BlockSize / 8;
+                    Assert.Equal(ciphertext[..^blockSizeBytes], encrypted[..^blockSizeBytes]);
+                }
+                else
+                {
+                    Assert.Equal(ciphertext, encrypted);
+                }
+            }
+        }
+
+        [Theory]
+        [MemberData(nameof(EcbTestCases))]
+        public static void TryDecryptEcb_DestinationTooSmall(byte[] plaintext, byte[] ciphertext, PaddingMode padding)
+        {
+            if (plaintext.Length == 0)
+            {
+                // Can't have a ciphertext length shorter than zero.
+                return;
+            }
+
+            using (Aes aes = AesFactory.Create())
+            {
+                aes.Key = s_aes128OneShotKey;
+
+                Span<byte> destinationBuffer = new byte[plaintext.Length - 1];
+
+                bool result = aes.TryDecryptEcb(ciphertext, destinationBuffer, padding, out int bytesWritten);
+                Assert.False(result, "TryDecryptEcb");
+                Assert.Equal(0, bytesWritten);
+            }
+        }
+
+        [Theory]
+        [MemberData(nameof(EcbTestCases))]
+        public static void TryEncryptEcb_DestinationTooSmall(byte[] plaintext, byte[] ciphertext, PaddingMode padding)
+        {
+            if (ciphertext.Length == 0)
+            {
+                // Can't have a too small buffer for zero.
+                return;
+            }
+
+            using (Aes aes = AesFactory.Create())
+            {
+                aes.Key = s_aes128OneShotKey;
+
+                Span<byte> destinationBuffer = new byte[ciphertext.Length - 1];
+
+                bool result = aes.TryEncryptEcb(plaintext, destinationBuffer, padding, out int bytesWritten);
+                Assert.False(result, "TryDecryptEcb");
+                Assert.Equal(0, bytesWritten);
+            }
+        }
+
+        [Theory]
+        [MemberData(nameof(EcbTestCases))]
+        public static void TryDecryptEcb_DestinationJustRight(byte[] plaintext, byte[] ciphertext, PaddingMode padding)
+        {
+            using (Aes aes = AesFactory.Create())
+            {
+                aes.Key = s_aes128OneShotKey;
+
+                int expectedPlaintextSize = padding == PaddingMode.Zeros ? ciphertext.Length : plaintext.Length;
+                Span<byte> destinationBuffer = new byte[expectedPlaintextSize];
+
+                bool result = aes.TryDecryptEcb(ciphertext, destinationBuffer, padding, out int bytesWritten);
+                Assert.True(result, "TryDecryptEcb");
+                Assert.Equal(destinationBuffer.Length, bytesWritten);
+
+                if (padding == PaddingMode.Zeros)
+                {
+                    Assert.Equal(plaintext, destinationBuffer.Slice(0, plaintext.Length).ToArray());
+                    AssertFilledWith(0, destinationBuffer.Slice(plaintext.Length));
+                }
+                else
+                {
+                    Assert.Equal(plaintext, destinationBuffer.ToArray());
+                }
+            }
+        }
+
+        [Theory]
+        [MemberData(nameof(EcbTestCases))]
+        public static void TryEncryptEcb_DestinationJustRight(byte[] plaintext, byte[] ciphertext, PaddingMode padding)
+        {
+            using (Aes aes = AesFactory.Create())
+            {
+                aes.Key = s_aes128OneShotKey;
+
+                int expectedCiphertextSize = aes.GetCiphertextLengthEcb(plaintext.Length, padding);
+                Span<byte> destinationBuffer = new byte[expectedCiphertextSize];
+
+                bool result = aes.TryEncryptEcb(plaintext, destinationBuffer, padding, out int bytesWritten);
+                Assert.True(result, "TryEncryptEcb");
+                Assert.Equal(expectedCiphertextSize, bytesWritten);
+
+                if (padding == PaddingMode.ISO10126)
+                {
+                    int blockSizeBytes = aes.BlockSize / 8;
+                    // Padding is random so we can't validate the last block.
+                    Assert.Equal(ciphertext[..^blockSizeBytes], destinationBuffer[..^blockSizeBytes].ToArray());
+                }
+                else
+                {
+                    Assert.Equal(ciphertext, destinationBuffer.ToArray());
+                }
+            }
+        }
+
+        [Theory]
+        [MemberData(nameof(EcbTestCases))]
+        public static void TryDecryptEcb_DestinationLarger(byte[] plaintext, byte[] ciphertext, PaddingMode padding)
+        {
+            using (Aes aes = AesFactory.Create())
+            {
+                aes.Key = s_aes128OneShotKey;
+                int expectedPlaintextSize = padding == PaddingMode.Zeros ? ciphertext.Length : plaintext.Length;
+
+                Span<byte> largeBuffer = new byte[expectedPlaintextSize + 10];
+                Span<byte> destinationBuffer = largeBuffer.Slice(0, expectedPlaintextSize);
+                largeBuffer.Fill(0xCC);
+
+                bool result = aes.TryDecryptEcb(
+                    ciphertext,
+                    destinationBuffer,
+                    padding,
+                    out int bytesWritten);
+
+                Assert.True(result, "TryDecryptEcb");
+                Assert.Equal(destinationBuffer.Length, bytesWritten);
+
+                if (padding == PaddingMode.Zeros)
+                {
+                    Assert.Equal(plaintext, destinationBuffer.Slice(0, plaintext.Length).ToArray());
+                    AssertFilledWith(0, destinationBuffer.Slice(plaintext.Length));
+                }
+                else
+                {
+                    Assert.Equal(plaintext, destinationBuffer.ToArray());
+                }
+
+                Span<byte> excess = largeBuffer.Slice(destinationBuffer.Length);
+                AssertFilledWith(0xCC, excess);
+            }
+        }
+
+        [Theory]
+        [MemberData(nameof(EcbTestCases))]
+        public static void TryEncryptEcb_DestinationLarger(byte[] plaintext, byte[] ciphertext, PaddingMode padding)
+        {
+            using (Aes aes = AesFactory.Create())
+            {
+                aes.Key = s_aes128OneShotKey;
+
+                Span<byte> largeBuffer = new byte[ciphertext.Length + 10];
+                Span<byte> destinationBuffer = largeBuffer.Slice(0, ciphertext.Length);
+                largeBuffer.Fill(0xCC);
+
+                bool result = aes.TryEncryptEcb(
+                    plaintext,
+                    destinationBuffer,
+                    padding,
+                    out int bytesWritten);
+
+                Assert.True(result, "TryEncryptEcb");
+                Assert.Equal(destinationBuffer.Length, bytesWritten);
+
+                if (padding == PaddingMode.ISO10126)
+                {
+                    int blockSizeBytes = aes.BlockSize / 8;
+                    Assert.Equal(ciphertext[..^blockSizeBytes], destinationBuffer[..^blockSizeBytes].ToArray());
+                }
+                else
+                {
+                    Assert.Equal(ciphertext, destinationBuffer.ToArray());
+                }
+
+                AssertFilledWith(0xCC, largeBuffer.Slice(ciphertext.Length));
+            }
+        }
+
+        [Theory]
+        [MemberData(nameof(EcbTestCases))]
+        public static void TryDecryptEcb_Overlaps(byte[] plaintext, byte[] ciphertext, PaddingMode padding)
+        {
+            (int plaintextOffset, int ciphertextOffset)[] offsets =
+            {
+                (0, 0), (8, 0), (0, 8), (8, 8),
+            };
+
+            foreach ((int plaintextOffset, int ciphertextOffset) in offsets)
+            {
+                using (Aes aes = AesFactory.Create())
+                {
+                    aes.Key = s_aes128OneShotKey;
+
+                    int expectedPlaintextSize = padding == PaddingMode.Zeros ? ciphertext.Length : plaintext.Length;
+                    int destinationSize = Math.Max(expectedPlaintextSize, ciphertext.Length) + Math.Max(plaintextOffset, ciphertextOffset);
+                    Span<byte> buffer = new byte[destinationSize];
+                    Span<byte> destinationBuffer = buffer.Slice(plaintextOffset, expectedPlaintextSize);
+                    Span<byte> ciphertextBuffer = buffer.Slice(ciphertextOffset, ciphertext.Length);
+                    ciphertext.AsSpan().CopyTo(ciphertextBuffer);
+
+                    bool result = aes.TryDecryptEcb(ciphertextBuffer, destinationBuffer, padding, out int bytesWritten);
+                    Assert.True(result, "TryDecryptEcb");
+                    Assert.Equal(destinationBuffer.Length, bytesWritten);
+
+                    if (padding == PaddingMode.Zeros)
+                    {
+                        Assert.Equal(plaintext, destinationBuffer.Slice(0, plaintext.Length).ToArray());
+                        AssertFilledWith(0, destinationBuffer.Slice(plaintext.Length));
+                    }
+                    else
+                    {
+                        Assert.Equal(plaintext, destinationBuffer.ToArray());
+                        Assert.True(destinationBuffer.Overlaps(ciphertextBuffer) || plaintext.Length == 0 || ciphertext.Length == 0);
+                    }
+                }
+            }
+        }
+
+        [Theory]
+        [MemberData(nameof(EcbTestCases))]
+        public static void TryEncryptEcb_Overlaps(byte[] plaintext, byte[] ciphertext, PaddingMode padding)
+        {
+            (int plaintextOffset, int ciphertextOffset)[] offsets =
+            {
+                (0, 0), (8, 0), (0, 8), (8, 8),
+            };
+
+            foreach ((int plaintextOffset, int ciphertextOffset) in offsets)
+            {
+                using (Aes aes = AesFactory.Create())
+                {
+                    aes.Key = s_aes128OneShotKey;
+
+                    int destinationSize = ciphertext.Length + Math.Max(plaintextOffset, ciphertextOffset);
+                    Span<byte> buffer = new byte[destinationSize];
+                    Span<byte> destinationBuffer = buffer.Slice(ciphertextOffset, ciphertext.Length);
+                    Span<byte> plaintextBuffer = buffer.Slice(plaintextOffset, plaintext.Length);
+                    plaintext.AsSpan().CopyTo(plaintextBuffer);
+
+                    bool result = aes.TryEncryptEcb(plaintextBuffer, destinationBuffer, padding, out int bytesWritten);
+                    Assert.True(result, "TryEncryptEcb");
+                    Assert.Equal(destinationBuffer.Length, bytesWritten);
+
+                    if (padding == PaddingMode.ISO10126)
+                    {
+                        int blockSizeBytes = aes.BlockSize / 8;
+                        Assert.Equal(ciphertext[..^blockSizeBytes], destinationBuffer[..^blockSizeBytes].ToArray());
+                    }
+                    else
+                    {
+                        Assert.Equal(ciphertext, destinationBuffer.ToArray());
+                        Assert.True(destinationBuffer.Overlaps(plaintextBuffer) || plaintext.Length == 0 || ciphertext.Length == 0);
+                    }
+                }
+            }
+        }
+
+        [Theory]
+        [MemberData(nameof(EcbTestCases))]
+        public static void DecryptEcb_Span(byte[] plaintext, byte[] ciphertext, PaddingMode padding)
+        {
+            using (Aes aes = AesFactory.Create())
+            {
+                aes.Key = s_aes128OneShotKey;
+                byte[] decrypted = aes.DecryptEcb(ciphertext.AsSpan(), padding);
+
+                if (padding == PaddingMode.Zeros)
+                {
+                    Assert.Equal(plaintext, decrypted.AsSpan(0, plaintext.Length).ToArray());
+                    AssertFilledWith(0, decrypted.AsSpan(plaintext.Length));
+                }
+                else
+                {
+                    Assert.Equal(plaintext, decrypted);
+                }
+            }
+        }
+
+        [Theory]
+        [MemberData(nameof(EcbTestCases))]
+        public static void EncryptEcb_Span(byte[] plaintext, byte[] ciphertext, PaddingMode padding)
+        {
+            using (Aes aes = AesFactory.Create())
+            {
+                aes.Key = s_aes128OneShotKey;
+                byte[] encrypted = aes.EncryptEcb(plaintext.AsSpan(), padding);
+
+                if (padding == PaddingMode.ISO10126)
+                {
+                    int blockSizeBytes = aes.BlockSize / 8;
+                    Assert.Equal(ciphertext[..^blockSizeBytes], encrypted[..^blockSizeBytes]);
+                }
+                else
+                {
+                    Assert.Equal(ciphertext, encrypted);
+                }
+            }
+        }
+
+        [Theory]
+        [MemberData(nameof(EcbTestCases))]
+        public static void DecryptEcb_Array(byte[] plaintext, byte[] ciphertext, PaddingMode padding)
+        {
+            using (Aes aes = AesFactory.Create())
+            {
+                aes.Key = s_aes128OneShotKey;
+                byte[] decrypted = aes.DecryptEcb(ciphertext, padding);
+
+                if (padding == PaddingMode.Zeros)
+                {
+                    Assert.Equal(plaintext, decrypted.AsSpan(0, plaintext.Length).ToArray());
+                    AssertFilledWith(0, decrypted.AsSpan(plaintext.Length));
+                }
+                else
+                {
+                    Assert.Equal(plaintext, decrypted);
+                }
+            }
+        }
+
+        [Theory]
+        [MemberData(nameof(EcbTestCases))]
+        public static void EncryptEcb_Array(byte[] plaintext, byte[] ciphertext, PaddingMode padding)
+        {
+            using (Aes aes = AesFactory.Create())
+            {
+                aes.Key = s_aes128OneShotKey;
+                byte[] encrypted = aes.EncryptEcb(plaintext, padding);
+
+                if (padding == PaddingMode.ISO10126)
+                {
+                    int blockSizeBytes = aes.BlockSize / 8;
+                    Assert.Equal(ciphertext[..^blockSizeBytes], encrypted[..^blockSizeBytes]);
+                }
+                else
+                {
+                    Assert.Equal(ciphertext, encrypted);
+                }
+            }
+        }
+
+        private static void AssertFilledWith(byte value, ReadOnlySpan<byte> span)
+        {
+            for (int i = 0; i < span.Length; i++)
+            {
+                Assert.Equal(value, span[i]);
+            }
+        }
+
+        public static IEnumerable<object[]> EcbTestCases
+        {
+            get
+            {
+                // plaintext requires no padding
+                yield return new object[]
+                {
+                    // plaintext
+                    new byte[]
+                    {
+                        0x50, 0x68, 0x12, 0xA4, 0x5F, 0x08, 0xC8, 0x89,
+                        0xB9, 0x7F, 0x59, 0x80, 0x03, 0x8B, 0x83, 0x59,
+                    },
+
+                    // ciphertext
+                    new byte[]
+                    {
+                        0xD8, 0xF5, 0x32, 0x53, 0x82, 0x89, 0xEF, 0x7D,
+                        0x06, 0xB5, 0x06, 0xA4, 0xFD, 0x5B, 0xE9, 0xC9,
+                        0x6D, 0xE5, 0xF6, 0x07, 0xAB, 0x7E, 0xB8, 0x20,
+                        0x2F, 0x39, 0x57, 0x70, 0x3B, 0x04, 0xE8, 0xB5,
+                    },
+
+                    PaddingMode.PKCS7,
+                };
+
+                yield return new object[]
+                {
+                    // plaintext
+                    new byte[]
+                    {
+                        0x50, 0x68, 0x12, 0xA4, 0x5F, 0x08, 0xC8, 0x89,
+                        0xB9, 0x7F, 0x59, 0x80, 0x03, 0x8B, 0x83, 0x59,
+                    },
+
+                    // ciphertext
+                    new byte[]
+                    {
+                        0xD8, 0xF5, 0x32, 0x53, 0x82, 0x89, 0xEF, 0x7D,
+                        0x06, 0xB5, 0x06, 0xA4, 0xFD, 0x5B, 0xE9, 0xC9,
+                    },
+
+                    PaddingMode.None,
+                };
+
+                yield return new object[]
+                {
+                    // plaintext
+                    new byte[]
+                    {
+                        0x50, 0x68, 0x12, 0xA4, 0x5F, 0x08, 0xC8, 0x89,
+                        0xB9, 0x7F, 0x59, 0x80, 0x03, 0x8B, 0x83, 0x59,
+                    },
+
+                    // ciphertext
+                    new byte[]
+                    {
+                        0xD8, 0xF5, 0x32, 0x53, 0x82, 0x89, 0xEF, 0x7D,
+                        0x06, 0xB5, 0x06, 0xA4, 0xFD, 0x5B, 0xE9, 0xC9,
+                    },
+
+                    PaddingMode.Zeros,
+                };
+
+                yield return new object[]
+                {
+                    // plaintext
+                    new byte[]
+                    {
+                        0x50, 0x68, 0x12, 0xA4, 0x5F, 0x08, 0xC8, 0x89,
+                        0xB9, 0x7F, 0x59, 0x80, 0x03, 0x8B, 0x83, 0x59,
+                    },
+
+                    // ciphertext
+                    new byte[]
+                    {
+                        0xD8, 0xF5, 0x32, 0x53, 0x82, 0x89, 0xEF, 0x7D,
+                        0x06, 0xB5, 0x06, 0xA4, 0xFD, 0x5B, 0xE9, 0xC9,
+                        0xC1, 0xCA, 0x44, 0xE8, 0x05, 0xFF, 0xCB, 0x6F,
+                        0x4D, 0x7F, 0xE9, 0x17, 0x12, 0xFE, 0xBB, 0xAC,
+                    },
+
+                    PaddingMode.ANSIX923,
+                };
+
+                yield return new object[]
+                {
+                    // plaintext
+                    new byte[]
+                    {
+                        0x50, 0x68, 0x12, 0xA4, 0x5F, 0x08, 0xC8, 0x89,
+                        0xB9, 0x7F, 0x59, 0x80, 0x03, 0x8B, 0x83, 0x59,
+                    },
+
+                    // ciphertext
+                    new byte[]
+                    {
+                        0xD8, 0xF5, 0x32, 0x53, 0x82, 0x89, 0xEF, 0x7D,
+                        0x06, 0xB5, 0x06, 0xA4, 0xFD, 0x5B, 0xE9, 0xC9,
+                        0xD3, 0xAA, 0x33, 0x5B, 0x93, 0xC2, 0x3D, 0x96,
+                        0xFD, 0x89, 0xB1, 0x8C, 0x47, 0x75, 0x65, 0xA8,
+                    },
+
+                    PaddingMode.ISO10126,
+                };
+
+                // plaintext requires padding
+                yield return new object[]
+                {
+                    // plaintext
+                    new byte[]
+                    {
+                        0x99, 0x50, 0x68, 0x12, 0xA4, 0x5F, 0x08, 0xC8,
+                        0x89, 0xB9, 0x7F, 0x59, 0x80, 0x03, 0x8B, 0x83,
+                        0x59,
+                    },
+
+                    // ciphertext
+                    new byte[]
+                    {
+                        0xC3, 0x03, 0x87, 0xCD, 0x79, 0x19, 0xB1, 0xC3,
+                        0x50, 0x2C, 0x9D, 0x7B, 0x1F, 0x8A, 0xBE, 0x0F,
+                        0x82, 0x8D, 0x60, 0xDC, 0x44, 0x26, 0xCF, 0xDE,
+                        0xC9, 0x54, 0x33, 0x47, 0xE2, 0x9E, 0xF0, 0x8C,
+                    },
+
+                    PaddingMode.PKCS7,
+                };
+
+                yield return new object[]
+                {
+                    // plaintext
+                    new byte[]
+                    {
+                        0x99, 0x50, 0x68, 0x12, 0xA4, 0x5F, 0x08, 0xC8,
+                        0x89, 0xB9, 0x7F, 0x59, 0x80, 0x03, 0x8B, 0x83,
+                        0x59,
+                    },
+
+                    // ciphertext
+                    new byte[]
+                    {
+                        0xC3, 0x03, 0x87, 0xCD, 0x79, 0x19, 0xB1, 0xC3,
+                        0x50, 0x2C, 0x9D, 0x7B, 0x1F, 0x8A, 0xBE, 0x0F,
+                        0x49, 0x39, 0x1B, 0x69, 0xA1, 0xF3, 0x66, 0xE4,
+                        0x3E, 0x40, 0x51, 0xB8, 0x05, 0x60, 0xDC, 0xFD,
+                    },
+
+                    PaddingMode.Zeros,
+                };
+
+                yield return new object[]
+                {
+                    // plaintext
+                    new byte[]
+                    {
+                        0x99, 0x50, 0x68, 0x12, 0xA4, 0x5F, 0x08, 0xC8,
+                        0x89, 0xB9, 0x7F, 0x59, 0x80, 0x03, 0x8B, 0x83,
+                        0x59,
+                    },
+
+                    // ciphertext
+                    new byte[]
+                    {
+                        0xC3, 0x03, 0x87, 0xCD, 0x79, 0x19, 0xB1, 0xC3,
+                        0x50, 0x2C, 0x9D, 0x7B, 0x1F, 0x8A, 0xBE, 0x0F,
+                        0xCD, 0x0D, 0xCD, 0xEA, 0xA2, 0x1F, 0xC1, 0xC3,
+                        0x81, 0xEE, 0x8A, 0x63, 0x94, 0x5F, 0x85, 0x43,
+                    },
+
+                    PaddingMode.ANSIX923,
+                };
+
+                yield return new object[]
+                {
+                    // plaintext
+                    new byte[]
+                    {
+                        0x99, 0x50, 0x68, 0x12, 0xA4, 0x5F, 0x08, 0xC8,
+                        0x89, 0xB9, 0x7F, 0x59, 0x80, 0x03, 0x8B, 0x83,
+                        0x59,
+                    },
+
+                    // ciphertext
+                    new byte[]
+                    {
+                        0xC3, 0x03, 0x87, 0xCD, 0x79, 0x19, 0xB1, 0xC3,
+                        0x50, 0x2C, 0x9D, 0x7B, 0x1F, 0x8A, 0xBE, 0x0F,
+                        0x9C, 0xE4, 0x0D, 0x2F, 0xCD, 0x82, 0x25, 0x0E,
+                        0x13, 0xAB, 0x4B, 0x6B, 0xC0, 0x9A, 0x21, 0x2E,
+                    },
+
+                    PaddingMode.ISO10126,
+                };
+
+                yield return new object[]
+                {
+                    // plaintext
+                    Array.Empty<byte>(),
+
+                    // ciphertext
+                    Array.Empty<byte>(),
+
+                    PaddingMode.Zeros,
+                };
+
+                yield return new object[]
+                {
+                    // plaintext
+                    Array.Empty<byte>(),
+
+                    // ciphertext
+                    Array.Empty<byte>(),
+
+                    PaddingMode.None,
+                };
+
+                yield return new object[]
+                {
+                    // plaintext
+                    Array.Empty<byte>(),
+
+                    // ciphertext
+                    new byte[]
+                    {
+                        0x6D, 0xE5, 0xF6, 0x07, 0xAB, 0x7E, 0xB8, 0x20,
+                        0x2F, 0x39, 0x57, 0x70, 0x3B, 0x04, 0xE8, 0xB5,
+                    },
+
+                    PaddingMode.PKCS7,
+                };
+            }
+        }
+    }
+}
index e1f5da5..2d7e237 100644 (file)
@@ -1066,6 +1066,7 @@ namespace System.Security.Cryptography.Encryption.Aes.Tests
             int? feedbackSize = default)
         {
             byte[] decryptedBytes;
+            byte[] oneShotDecryptedBytes = null;
 
             using (Aes aes = AesFactory.Create())
             {
@@ -1089,10 +1090,20 @@ namespace System.Security.Cryptography.Encryption.Aes.Tests
                     cryptoStream.CopyTo(output);
                     decryptedBytes = output.ToArray();
                 }
+
+                if (mode == CipherMode.ECB)
+                {
+                    oneShotDecryptedBytes = aes.DecryptEcb(encryptedBytes, aes.Padding);
+                }
             }
 
             Assert.NotEqual(encryptedBytes, decryptedBytes);
             Assert.Equal(expectedAnswer, decryptedBytes);
+
+            if (oneShotDecryptedBytes is not null)
+            {
+                Assert.Equal(expectedAnswer, oneShotDecryptedBytes);
+            }
         }
 
         private static void TestAesTransformDirectKey(
@@ -1106,6 +1117,8 @@ namespace System.Security.Cryptography.Encryption.Aes.Tests
         {
             byte[] liveEncryptBytes;
             byte[] liveDecryptBytes;
+            byte[] liveOneShotDecryptBytes = null;
+            byte[] liveOneShotEncryptBytes = null;
 
             using (Aes aes = AesFactory.Create())
             {
@@ -1119,10 +1132,27 @@ namespace System.Security.Cryptography.Encryption.Aes.Tests
 
                 liveEncryptBytes = AesEncryptDirectKey(aes, key, iv, plainBytes);
                 liveDecryptBytes = AesDecryptDirectKey(aes, key, iv, cipherBytes);
+
+                if (cipherMode == CipherMode.ECB)
+                {
+                    aes.Key = key;
+                    liveOneShotDecryptBytes = aes.DecryptEcb(cipherBytes, paddingMode);
+                    liveOneShotEncryptBytes = aes.EncryptEcb(plainBytes, paddingMode);
+                }
             }
 
             Assert.Equal(cipherBytes, liveEncryptBytes);
             Assert.Equal(plainBytes, liveDecryptBytes);
+
+            if (liveOneShotDecryptBytes is not null)
+            {
+                Assert.Equal(plainBytes, liveOneShotDecryptBytes);
+            }
+
+            if (liveOneShotEncryptBytes is not null)
+            {
+                Assert.Equal(cipherBytes, liveOneShotEncryptBytes);
+            }
         }
 
         private static byte[] AesEncryptDirectKey(Aes aes, byte[] key, byte[] iv, byte[] plainBytes)
diff --git a/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/DES/DESCipherTests.OneShot.cs b/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/DES/DESCipherTests.OneShot.cs
new file mode 100644 (file)
index 0000000..6f51068
--- /dev/null
@@ -0,0 +1,624 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using System.Security.Cryptography;
+using Test.Cryptography;
+using Xunit;
+
+namespace System.Security.Cryptography.Encryption.Des.Tests
+{
+    public partial class DesCipherTests
+    {
+        private static byte[] s_desOneShotKey = new byte[]
+            {
+                0x74, 0x4B, 0x93, 0x3A, 0x96, 0x33, 0x61, 0xD6
+            };
+
+        [Theory]
+        [MemberData(nameof(EcbTestCases))]
+        public static void EcbRoundtrip(byte[] plaintext, byte[] ciphertext, PaddingMode padding)
+        {
+            using (DES des = DESFactory.Create())
+            {
+                des.Key = s_desOneShotKey;
+
+                byte[] encrypted = des.EncryptEcb(plaintext, padding);
+                byte[] decrypted = des.DecryptEcb(encrypted, padding);
+
+                if (padding == PaddingMode.Zeros)
+                {
+                    Assert.Equal(plaintext, decrypted[..plaintext.Length]);
+                    AssertFilledWith(0, plaintext.AsSpan(plaintext.Length));
+                }
+                else
+                {
+                    Assert.Equal(plaintext, decrypted);
+                }
+
+                decrypted = des.DecryptEcb(ciphertext, padding);
+                encrypted = des.EncryptEcb(decrypted, padding);
+
+                if (padding == PaddingMode.ISO10126)
+                {
+                    int blockSizeBytes = des.BlockSize / 8;
+                    Assert.Equal(ciphertext[..^blockSizeBytes], encrypted[..^blockSizeBytes]);
+                }
+                else
+                {
+                    Assert.Equal(ciphertext, encrypted);
+                }
+            }
+        }
+
+        [Theory]
+        [MemberData(nameof(EcbTestCases))]
+        public static void TryDecryptEcb_DestinationTooSmall(byte[] plaintext, byte[] ciphertext, PaddingMode padding)
+        {
+            if (plaintext.Length == 0)
+            {
+                // Can't have a ciphertext length shorter than zero.
+                return;
+            }
+
+            using (DES des = DESFactory.Create())
+            {
+                des.Key = s_desOneShotKey;
+
+                Span<byte> destinationBuffer = new byte[plaintext.Length - 1];
+
+                bool result = des.TryDecryptEcb(ciphertext, destinationBuffer, padding, out int bytesWritten);
+                Assert.False(result, "TryDecryptEcb");
+                Assert.Equal(0, bytesWritten);
+            }
+        }
+
+        [Theory]
+        [MemberData(nameof(EcbTestCases))]
+        public static void TryEncryptEcb_DestinationTooSmall(byte[] plaintext, byte[] ciphertext, PaddingMode padding)
+        {
+            if (ciphertext.Length == 0)
+            {
+                // Can't have a too small buffer for zero.
+                return;
+            }
+
+            using (DES des = DESFactory.Create())
+            {
+                des.Key = s_desOneShotKey;
+
+                Span<byte> destinationBuffer = new byte[ciphertext.Length - 1];
+
+                bool result = des.TryEncryptEcb(plaintext, destinationBuffer, padding, out int bytesWritten);
+                Assert.False(result, "TryDecryptEcb");
+                Assert.Equal(0, bytesWritten);
+            }
+        }
+
+        [Theory]
+        [MemberData(nameof(EcbTestCases))]
+        public static void TryDecryptEcb_DestinationJustRight(byte[] plaintext, byte[] ciphertext, PaddingMode padding)
+        {
+            using (DES des = DESFactory.Create())
+            {
+                des.Key = s_desOneShotKey;
+
+                int expectedPlaintextSize = padding == PaddingMode.Zeros ? ciphertext.Length : plaintext.Length;
+                Span<byte> destinationBuffer = new byte[expectedPlaintextSize];
+
+                bool result = des.TryDecryptEcb(ciphertext, destinationBuffer, padding, out int bytesWritten);
+                Assert.True(result, "TryDecryptEcb");
+                Assert.Equal(destinationBuffer.Length, bytesWritten);
+
+                if (padding == PaddingMode.Zeros)
+                {
+                    Assert.Equal(plaintext, destinationBuffer.Slice(0, plaintext.Length).ToArray());
+                    AssertFilledWith(0, destinationBuffer.Slice(plaintext.Length));
+                }
+                else
+                {
+                    Assert.Equal(plaintext, destinationBuffer.ToArray());
+                }
+            }
+        }
+
+        [Theory]
+        [MemberData(nameof(EcbTestCases))]
+        public static void TryEncryptEcb_DestinationJustRight(byte[] plaintext, byte[] ciphertext, PaddingMode padding)
+        {
+            using (DES des = DESFactory.Create())
+            {
+                des.Key = s_desOneShotKey;
+
+                int expectedCiphertextSize = des.GetCiphertextLengthEcb(plaintext.Length, padding);
+                Span<byte> destinationBuffer = new byte[expectedCiphertextSize];
+
+                bool result = des.TryEncryptEcb(plaintext, destinationBuffer, padding, out int bytesWritten);
+                Assert.True(result, "TryEncryptEcb");
+                Assert.Equal(expectedCiphertextSize, bytesWritten);
+
+                if (padding == PaddingMode.ISO10126)
+                {
+                    int blockSizeBytes = des.BlockSize / 8;
+                    // Padding is random so we can't validate the last block.
+                    Assert.Equal(ciphertext[..^blockSizeBytes], destinationBuffer[..^blockSizeBytes].ToArray());
+                }
+                else
+                {
+                    Assert.Equal(ciphertext, destinationBuffer.ToArray());
+                }
+            }
+        }
+
+        [Theory]
+        [MemberData(nameof(EcbTestCases))]
+        public static void TryDecryptEcb_DestinationLarger(byte[] plaintext, byte[] ciphertext, PaddingMode padding)
+        {
+            using (DES des = DESFactory.Create())
+            {
+                des.Key = s_desOneShotKey;
+                int expectedPlaintextSize = padding == PaddingMode.Zeros ? ciphertext.Length : plaintext.Length;
+
+                Span<byte> largeBuffer = new byte[expectedPlaintextSize + 10];
+                Span<byte> destinationBuffer = largeBuffer.Slice(0, expectedPlaintextSize);
+                largeBuffer.Fill(0xCC);
+
+                bool result = des.TryDecryptEcb(
+                    ciphertext,
+                    destinationBuffer,
+                    padding,
+                    out int bytesWritten);
+
+                Assert.True(result, "TryDecryptEcb");
+                Assert.Equal(destinationBuffer.Length, bytesWritten);
+
+                if (padding == PaddingMode.Zeros)
+                {
+                    Assert.Equal(plaintext, destinationBuffer.Slice(0, plaintext.Length).ToArray());
+                    AssertFilledWith(0, destinationBuffer.Slice(plaintext.Length));
+                }
+                else
+                {
+                    Assert.Equal(plaintext, destinationBuffer.ToArray());
+                }
+
+                Span<byte> excess = largeBuffer.Slice(destinationBuffer.Length);
+                AssertFilledWith(0xCC, excess);
+            }
+        }
+
+        [Theory]
+        [MemberData(nameof(EcbTestCases))]
+        public static void TryEncryptEcb_DestinationLarger(byte[] plaintext, byte[] ciphertext, PaddingMode padding)
+        {
+            using (DES des = DESFactory.Create())
+            {
+                des.Key = s_desOneShotKey;
+
+                Span<byte> largeBuffer = new byte[ciphertext.Length + 10];
+                Span<byte> destinationBuffer = largeBuffer.Slice(0, ciphertext.Length);
+                largeBuffer.Fill(0xCC);
+
+                bool result = des.TryEncryptEcb(
+                    plaintext,
+                    destinationBuffer,
+                    padding,
+                    out int bytesWritten);
+
+                Assert.True(result, "TryEncryptEcb");
+                Assert.Equal(destinationBuffer.Length, bytesWritten);
+
+                if (padding == PaddingMode.ISO10126)
+                {
+                    int blockSizeBytes = des.BlockSize / 8;
+                    Assert.Equal(ciphertext[..^blockSizeBytes], destinationBuffer[..^blockSizeBytes].ToArray());
+                }
+                else
+                {
+                    Assert.Equal(ciphertext, destinationBuffer.ToArray());
+                }
+
+                AssertFilledWith(0xCC, largeBuffer.Slice(ciphertext.Length));
+            }
+        }
+
+        [Theory]
+        [MemberData(nameof(EcbTestCases))]
+        public static void TryDecryptEcb_Overlaps(byte[] plaintext, byte[] ciphertext, PaddingMode padding)
+        {
+            (int plaintextOffset, int ciphertextOffset)[] offsets =
+            {
+                (0, 0), (8, 0), (0, 8), (8, 8),
+            };
+
+            foreach ((int plaintextOffset, int ciphertextOffset) in offsets)
+            {
+                using (DES des = DESFactory.Create())
+                {
+                    des.Key = s_desOneShotKey;
+
+                    int expectedPlaintextSize = padding == PaddingMode.Zeros ? ciphertext.Length : plaintext.Length;
+                    int destinationSize = Math.Max(expectedPlaintextSize, ciphertext.Length) + Math.Max(plaintextOffset, ciphertextOffset);
+                    Span<byte> buffer = new byte[destinationSize];
+                    Span<byte> destinationBuffer = buffer.Slice(plaintextOffset, expectedPlaintextSize);
+                    Span<byte> ciphertextBuffer = buffer.Slice(ciphertextOffset, ciphertext.Length);
+                    ciphertext.AsSpan().CopyTo(ciphertextBuffer);
+
+                    bool result = des.TryDecryptEcb(ciphertextBuffer, destinationBuffer, padding, out int bytesWritten);
+                    Assert.True(result, "TryDecryptEcb");
+                    Assert.Equal(destinationBuffer.Length, bytesWritten);
+
+                    if (padding == PaddingMode.Zeros)
+                    {
+                        Assert.Equal(plaintext, destinationBuffer.Slice(0, plaintext.Length).ToArray());
+                        AssertFilledWith(0, destinationBuffer.Slice(plaintext.Length));
+                    }
+                    else
+                    {
+                        Assert.Equal(plaintext, destinationBuffer.ToArray());
+                        Assert.True(destinationBuffer.Overlaps(ciphertextBuffer) || plaintext.Length == 0 || ciphertext.Length == 0);
+                    }
+                }
+            }
+        }
+
+        [Theory]
+        [MemberData(nameof(EcbTestCases))]
+        public static void TryEncryptEcb_Overlaps(byte[] plaintext, byte[] ciphertext, PaddingMode padding)
+        {
+            (int plaintextOffset, int ciphertextOffset)[] offsets =
+            {
+                (0, 0), (8, 0), (0, 8), (8, 8),
+            };
+
+            foreach ((int plaintextOffset, int ciphertextOffset) in offsets)
+            {
+                using (DES des = DESFactory.Create())
+                {
+                    des.Key = s_desOneShotKey;
+
+                    int destinationSize = ciphertext.Length + Math.Max(plaintextOffset, ciphertextOffset);
+                    Span<byte> buffer = new byte[destinationSize];
+                    Span<byte> destinationBuffer = buffer.Slice(ciphertextOffset, ciphertext.Length);
+                    Span<byte> plaintextBuffer = buffer.Slice(plaintextOffset, plaintext.Length);
+                    plaintext.AsSpan().CopyTo(plaintextBuffer);
+
+                    bool result = des.TryEncryptEcb(plaintextBuffer, destinationBuffer, padding, out int bytesWritten);
+                    Assert.True(result, "TryEncryptEcb");
+                    Assert.Equal(destinationBuffer.Length, bytesWritten);
+
+                    if (padding == PaddingMode.ISO10126)
+                    {
+                        int blockSizeBytes = des.BlockSize / 8;
+                        Assert.Equal(ciphertext[..^blockSizeBytes], destinationBuffer[..^blockSizeBytes].ToArray());
+                    }
+                    else
+                    {
+                        Assert.Equal(ciphertext, destinationBuffer.ToArray());
+                        Assert.True(destinationBuffer.Overlaps(plaintextBuffer) || plaintext.Length == 0 || ciphertext.Length == 0);
+                    }
+                }
+            }
+        }
+
+        [Theory]
+        [MemberData(nameof(EcbTestCases))]
+        public static void DecryptEcb_Span(byte[] plaintext, byte[] ciphertext, PaddingMode padding)
+        {
+            using (DES des = DESFactory.Create())
+            {
+                des.Key = s_desOneShotKey;
+                byte[] decrypted = des.DecryptEcb(ciphertext.AsSpan(), padding);
+
+                if (padding == PaddingMode.Zeros)
+                {
+                    Assert.Equal(plaintext, decrypted.AsSpan(0, plaintext.Length).ToArray());
+                    AssertFilledWith(0, decrypted.AsSpan(plaintext.Length));
+                }
+                else
+                {
+                    Assert.Equal(plaintext, decrypted);
+                }
+            }
+        }
+
+        [Theory]
+        [MemberData(nameof(EcbTestCases))]
+        public static void EncryptEcb_Span(byte[] plaintext, byte[] ciphertext, PaddingMode padding)
+        {
+            using (DES des = DESFactory.Create())
+            {
+                des.Key = s_desOneShotKey;
+                byte[] encrypted = des.EncryptEcb(plaintext.AsSpan(), padding);
+
+                if (padding == PaddingMode.ISO10126)
+                {
+                    int blockSizeBytes = des.BlockSize / 8;
+                    Assert.Equal(ciphertext[..^blockSizeBytes], encrypted[..^blockSizeBytes]);
+                }
+                else
+                {
+                    Assert.Equal(ciphertext, encrypted);
+                }
+            }
+        }
+
+        [Theory]
+        [MemberData(nameof(EcbTestCases))]
+        public static void DecryptEcb_Array(byte[] plaintext, byte[] ciphertext, PaddingMode padding)
+        {
+            using (DES des = DESFactory.Create())
+            {
+                des.Key = s_desOneShotKey;
+                byte[] decrypted = des.DecryptEcb(ciphertext, padding);
+
+                if (padding == PaddingMode.Zeros)
+                {
+                    Assert.Equal(plaintext, decrypted.AsSpan(0, plaintext.Length).ToArray());
+                    AssertFilledWith(0, decrypted.AsSpan(plaintext.Length));
+                }
+                else
+                {
+                    Assert.Equal(plaintext, decrypted);
+                }
+            }
+        }
+
+        [Theory]
+        [MemberData(nameof(EcbTestCases))]
+        public static void EncryptEcb_Array(byte[] plaintext, byte[] ciphertext, PaddingMode padding)
+        {
+            using (DES des = DESFactory.Create())
+            {
+                des.Key = s_desOneShotKey;
+                byte[] encrypted = des.EncryptEcb(plaintext, padding);
+
+                if (padding == PaddingMode.ISO10126)
+                {
+                    int blockSizeBytes = des.BlockSize / 8;
+                    Assert.Equal(ciphertext[..^blockSizeBytes], encrypted[..^blockSizeBytes]);
+                }
+                else
+                {
+                    Assert.Equal(ciphertext, encrypted);
+                }
+            }
+        }
+
+        private static void AssertFilledWith(byte value, ReadOnlySpan<byte> span)
+        {
+            for (int i = 0; i < span.Length; i++)
+            {
+                Assert.Equal(value, span[i]);
+            }
+        }
+
+        public static IEnumerable<object[]> EcbTestCases
+        {
+            get
+            {
+                // plaintext requires no padding
+                yield return new object[]
+                {
+                    // plaintext
+                    new byte[]
+                    {
+                        0x50, 0x68, 0x12, 0xA4, 0x5F, 0x08, 0xC8, 0x89,
+                        0xB9, 0x7F, 0x59, 0x80, 0x03, 0x8B, 0x83, 0x59,
+                    },
+
+                    // ciphertext
+                    new byte[]
+                    {
+                        0xEE, 0x8B, 0xA7, 0xEE, 0x11, 0x84, 0x1D, 0xA2,
+                        0xC4, 0x16, 0xB4, 0x05, 0x83, 0xA0, 0x60, 0x37,
+                        0xED, 0xD9, 0xE3, 0xFC, 0xC6, 0x55, 0xDC, 0x32,
+                    },
+
+                    PaddingMode.PKCS7,
+                };
+
+                yield return new object[]
+                {
+                    // plaintext
+                    new byte[]
+                    {
+                        0x50, 0x68, 0x12, 0xA4, 0x5F, 0x08, 0xC8, 0x89,
+                        0xB9, 0x7F, 0x59, 0x80, 0x03, 0x8B, 0x83, 0x59,
+                    },
+
+                    // ciphertext
+                    new byte[]
+                    {
+                        0xEE, 0x8B, 0xA7, 0xEE, 0x11, 0x84, 0x1D, 0xA2,
+                        0xC4, 0x16, 0xB4, 0x05, 0x83, 0xA0, 0x60, 0x37,
+                    },
+
+                    PaddingMode.None,
+                };
+
+                yield return new object[]
+                {
+                    // plaintext
+                    new byte[]
+                    {
+                        0x50, 0x68, 0x12, 0xA4, 0x5F, 0x08, 0xC8, 0x89,
+                        0xB9, 0x7F, 0x59, 0x80, 0x03, 0x8B, 0x83, 0x59,
+                    },
+
+                    // ciphertext
+                    new byte[]
+                    {
+                        0xEE, 0x8B, 0xA7, 0xEE, 0x11, 0x84, 0x1D, 0xA2,
+                        0xC4, 0x16, 0xB4, 0x05, 0x83, 0xA0, 0x60, 0x37,
+                    },
+
+                    PaddingMode.Zeros,
+                };
+
+                yield return new object[]
+                {
+                    // plaintext
+                    new byte[]
+                    {
+                        0x50, 0x68, 0x12, 0xA4, 0x5F, 0x08, 0xC8, 0x89,
+                        0xB9, 0x7F, 0x59, 0x80, 0x03, 0x8B, 0x83, 0x59,
+                    },
+
+                    // ciphertext
+                    new byte[]
+                    {
+                        0xEE, 0x8B, 0xA7, 0xEE, 0x11, 0x84, 0x1D, 0xA2,
+                        0xC4, 0x16, 0xB4, 0x05, 0x83, 0xA0, 0x60, 0x37,
+                        0xEC, 0x52, 0xA1, 0x7E, 0x52, 0x54, 0x6E, 0x9E,
+                    },
+
+                    PaddingMode.ANSIX923,
+                };
+
+                yield return new object[]
+                {
+                    // plaintext
+                    new byte[]
+                    {
+                        0x50, 0x68, 0x12, 0xA4, 0x5F, 0x08, 0xC8, 0x89,
+                        0xB9, 0x7F, 0x59, 0x80, 0x03, 0x8B, 0x83, 0x59,
+                    },
+
+                    // ciphertext
+                    new byte[]
+                    {
+                        0xEE, 0x8B, 0xA7, 0xEE, 0x11, 0x84, 0x1D, 0xA2,
+                        0xC4, 0x16, 0xB4, 0x05, 0x83, 0xA0, 0x60, 0x37,
+                        0x44, 0x4C, 0xA5, 0xC2, 0xCC, 0x54, 0xAC, 0xF9,
+                    },
+
+                    PaddingMode.ISO10126,
+                };
+
+                // plaintext requires padding
+                yield return new object[]
+                {
+                    // plaintext
+                    new byte[]
+                    {
+                        0x99, 0x50, 0x68, 0x12, 0xA4, 0x5F, 0x08, 0xC8,
+                        0x89, 0xB9, 0x7F, 0x59, 0x80, 0x03, 0x8B, 0x83,
+                        0x59,
+                    },
+
+                    // ciphertext
+                    new byte[]
+                    {
+                        0xEA, 0x91, 0x68, 0xFE, 0x02, 0xFE, 0x57, 0x6F,
+                        0x60, 0x17, 0x05, 0xD5, 0x94, 0xA2, 0xF8, 0xE2,
+                        0x60, 0x8E, 0xC3, 0xB8, 0x09, 0x84, 0xCF, 0x3B,
+                    },
+
+                    PaddingMode.PKCS7,
+                };
+
+                yield return new object[]
+                {
+                    // plaintext
+                    new byte[]
+                    {
+                        0x99, 0x50, 0x68, 0x12, 0xA4, 0x5F, 0x08, 0xC8,
+                        0x89, 0xB9, 0x7F, 0x59, 0x80, 0x03, 0x8B, 0x83,
+                        0x59,
+                    },
+
+                    // ciphertext
+                    new byte[]
+                    {
+                        0xEA, 0x91, 0x68, 0xFE, 0x02, 0xFE, 0x57, 0x6F,
+                        0x60, 0x17, 0x05, 0xD5, 0x94, 0xA2, 0xF8, 0xE2,
+                        0xE7, 0xA4, 0x10, 0xF1, 0x7B, 0xFF, 0x32, 0x4A,
+                    },
+
+                    PaddingMode.Zeros,
+                };
+
+                yield return new object[]
+                {
+                    // plaintext
+                    new byte[]
+                    {
+                        0x99, 0x50, 0x68, 0x12, 0xA4, 0x5F, 0x08, 0xC8,
+                        0x89, 0xB9, 0x7F, 0x59, 0x80, 0x03, 0x8B, 0x83,
+                        0x59,
+                    },
+
+                    // ciphertext
+                    new byte[]
+                    {
+                        0xEA, 0x91, 0x68, 0xFE, 0x02, 0xFE, 0x57, 0x6F,
+                        0x60, 0x17, 0x05, 0xD5, 0x94, 0xA2, 0xF8, 0xE2,
+                        0x92, 0x9A, 0x36, 0xFE, 0xA4, 0xB3, 0xEC, 0xA0,
+                    },
+
+                    PaddingMode.ANSIX923,
+                };
+
+                yield return new object[]
+                {
+                    // plaintext
+                    new byte[]
+                    {
+                        0x99, 0x50, 0x68, 0x12, 0xA4, 0x5F, 0x08, 0xC8,
+                        0x89, 0xB9, 0x7F, 0x59, 0x80, 0x03, 0x8B, 0x83,
+                        0x59,
+                    },
+
+                    // ciphertext
+                    new byte[]
+                    {
+                        0xEA, 0x91, 0x68, 0xFE, 0x02, 0xFE, 0x57, 0x6F,
+                        0x60, 0x17, 0x05, 0xD5, 0x94, 0xA2, 0xF8, 0xE2,
+                        0xDB, 0x86, 0xA4, 0xAB, 0xDE, 0x05, 0xE4, 0xE7,
+                    },
+
+                    PaddingMode.ISO10126,
+                };
+
+                yield return new object[]
+                {
+                    // plaintext
+                    Array.Empty<byte>(),
+
+                    // ciphertext
+                    Array.Empty<byte>(),
+
+                    PaddingMode.Zeros,
+                };
+
+                yield return new object[]
+                {
+                    // plaintext
+                    Array.Empty<byte>(),
+
+                    // ciphertext
+                    Array.Empty<byte>(),
+
+                    PaddingMode.None,
+                };
+
+                yield return new object[]
+                {
+                    // plaintext
+                    Array.Empty<byte>(),
+
+                    // ciphertext
+                    new byte[]
+                    {
+                        0xED, 0xD9, 0xE3, 0xFC, 0xC6, 0x55, 0xDC, 0x32,
+                    },
+
+                    PaddingMode.PKCS7,
+                };
+            }
+        }
+    }
+}
index d007a41..dae5048 100644 (file)
@@ -10,7 +10,7 @@ using Xunit;
 namespace System.Security.Cryptography.Encryption.Des.Tests
 {
     [SkipOnPlatform(TestPlatforms.Browser, "Not supported on Browser")]
-    public static class DesCipherTests
+    public static partial class DesCipherTests
     {
         // These are the expected output of many decryptions. Changing these values requires re-generating test input.
         private static readonly string s_multiBlockString = new ASCIIEncoding().GetBytes(
diff --git a/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/RC2/RC2CipherTests.OneShot.cs b/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/RC2/RC2CipherTests.OneShot.cs
new file mode 100644 (file)
index 0000000..4a22728
--- /dev/null
@@ -0,0 +1,627 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using System.Security.Cryptography;
+using Test.Cryptography;
+using Xunit;
+
+namespace System.Security.Cryptography.Encryption.RC2.Tests
+{
+    using RC2 = System.Security.Cryptography.RC2;
+
+    public partial class RC2CipherTests
+    {
+        private static byte[] s_rc2OneShotKey = new byte[]
+            {
+                0x83, 0x2F, 0x81, 0x1B, 0x61, 0x02, 0xCC, 0x8F,
+                0x2F, 0x78, 0x10, 0x68, 0x06, 0xA6, 0x35, 0x50,
+            };
+
+        [Theory]
+        [MemberData(nameof(EcbTestCases))]
+        public static void EcbRoundtrip(byte[] plaintext, byte[] ciphertext, PaddingMode padding)
+        {
+            using (RC2 rc2 = RC2Factory.Create())
+            {
+                rc2.Key = s_rc2OneShotKey;
+
+                byte[] encrypted = rc2.EncryptEcb(plaintext, padding);
+                byte[] decrypted = rc2.DecryptEcb(encrypted, padding);
+
+                if (padding == PaddingMode.Zeros)
+                {
+                    Assert.Equal(plaintext, decrypted[..plaintext.Length]);
+                    AssertFilledWith(0, plaintext.AsSpan(plaintext.Length));
+                }
+                else
+                {
+                    Assert.Equal(plaintext, decrypted);
+                }
+
+                decrypted = rc2.DecryptEcb(ciphertext, padding);
+                encrypted = rc2.EncryptEcb(decrypted, padding);
+
+                if (padding == PaddingMode.ISO10126)
+                {
+                    int blockSizeBytes = rc2.BlockSize / 8;
+                    Assert.Equal(ciphertext[..^blockSizeBytes], encrypted[..^blockSizeBytes]);
+                }
+                else
+                {
+                    Assert.Equal(ciphertext, encrypted);
+                }
+            }
+        }
+
+        [Theory]
+        [MemberData(nameof(EcbTestCases))]
+        public static void TryDecryptEcb_DestinationTooSmall(byte[] plaintext, byte[] ciphertext, PaddingMode padding)
+        {
+            if (plaintext.Length == 0)
+            {
+                // Can't have a ciphertext length shorter than zero.
+                return;
+            }
+
+            using (RC2 rc2 = RC2Factory.Create())
+            {
+                rc2.Key = s_rc2OneShotKey;
+
+                Span<byte> destinationBuffer = new byte[plaintext.Length - 1];
+
+                bool result = rc2.TryDecryptEcb(ciphertext, destinationBuffer, padding, out int bytesWritten);
+                Assert.False(result, "TryDecryptEcb");
+                Assert.Equal(0, bytesWritten);
+            }
+        }
+
+        [Theory]
+        [MemberData(nameof(EcbTestCases))]
+        public static void TryEncryptEcb_DestinationTooSmall(byte[] plaintext, byte[] ciphertext, PaddingMode padding)
+        {
+            if (ciphertext.Length == 0)
+            {
+                // Can't have a too small buffer for zero.
+                return;
+            }
+
+            using (RC2 rc2 = RC2Factory.Create())
+            {
+                rc2.Key = s_rc2OneShotKey;
+
+                Span<byte> destinationBuffer = new byte[ciphertext.Length - 1];
+
+                bool result = rc2.TryEncryptEcb(plaintext, destinationBuffer, padding, out int bytesWritten);
+                Assert.False(result, "TryDecryptEcb");
+                Assert.Equal(0, bytesWritten);
+            }
+        }
+
+        [Theory]
+        [MemberData(nameof(EcbTestCases))]
+        public static void TryDecryptEcb_DestinationJustRight(byte[] plaintext, byte[] ciphertext, PaddingMode padding)
+        {
+            using (RC2 rc2 = RC2Factory.Create())
+            {
+                rc2.Key = s_rc2OneShotKey;
+
+                int expectedPlaintextSize = padding == PaddingMode.Zeros ? ciphertext.Length : plaintext.Length;
+                Span<byte> destinationBuffer = new byte[expectedPlaintextSize];
+
+                bool result = rc2.TryDecryptEcb(ciphertext, destinationBuffer, padding, out int bytesWritten);
+                Assert.True(result, "TryDecryptEcb");
+                Assert.Equal(destinationBuffer.Length, bytesWritten);
+
+                if (padding == PaddingMode.Zeros)
+                {
+                    Assert.Equal(plaintext, destinationBuffer.Slice(0, plaintext.Length).ToArray());
+                    AssertFilledWith(0, destinationBuffer.Slice(plaintext.Length));
+                }
+                else
+                {
+                    Assert.Equal(plaintext, destinationBuffer.ToArray());
+                }
+            }
+        }
+
+        [Theory]
+        [MemberData(nameof(EcbTestCases))]
+        public static void TryEncryptEcb_DestinationJustRight(byte[] plaintext, byte[] ciphertext, PaddingMode padding)
+        {
+            using (RC2 rc2 = RC2Factory.Create())
+            {
+                rc2.Key = s_rc2OneShotKey;
+
+                int expectedCiphertextSize = rc2.GetCiphertextLengthEcb(plaintext.Length, padding);
+                Span<byte> destinationBuffer = new byte[expectedCiphertextSize];
+
+                bool result = rc2.TryEncryptEcb(plaintext, destinationBuffer, padding, out int bytesWritten);
+                Assert.True(result, "TryEncryptEcb");
+                Assert.Equal(expectedCiphertextSize, bytesWritten);
+
+                if (padding == PaddingMode.ISO10126)
+                {
+                    int blockSizeBytes = rc2.BlockSize / 8;
+                    // Padding is random so we can't validate the last block.
+                    Assert.Equal(ciphertext[..^blockSizeBytes], destinationBuffer[..^blockSizeBytes].ToArray());
+                }
+                else
+                {
+                    Assert.Equal(ciphertext, destinationBuffer.ToArray());
+                }
+            }
+        }
+
+        [Theory]
+        [MemberData(nameof(EcbTestCases))]
+        public static void TryDecryptEcb_DestinationLarger(byte[] plaintext, byte[] ciphertext, PaddingMode padding)
+        {
+            using (RC2 rc2 = RC2Factory.Create())
+            {
+                rc2.Key = s_rc2OneShotKey;
+                int expectedPlaintextSize = padding == PaddingMode.Zeros ? ciphertext.Length : plaintext.Length;
+
+                Span<byte> largeBuffer = new byte[expectedPlaintextSize + 10];
+                Span<byte> destinationBuffer = largeBuffer.Slice(0, expectedPlaintextSize);
+                largeBuffer.Fill(0xCC);
+
+                bool result = rc2.TryDecryptEcb(
+                    ciphertext,
+                    destinationBuffer,
+                    padding,
+                    out int bytesWritten);
+
+                Assert.True(result, "TryDecryptEcb");
+                Assert.Equal(destinationBuffer.Length, bytesWritten);
+
+                if (padding == PaddingMode.Zeros)
+                {
+                    Assert.Equal(plaintext, destinationBuffer.Slice(0, plaintext.Length).ToArray());
+                    AssertFilledWith(0, destinationBuffer.Slice(plaintext.Length));
+                }
+                else
+                {
+                    Assert.Equal(plaintext, destinationBuffer.ToArray());
+                }
+
+                Span<byte> excess = largeBuffer.Slice(destinationBuffer.Length);
+                AssertFilledWith(0xCC, excess);
+            }
+        }
+
+        [Theory]
+        [MemberData(nameof(EcbTestCases))]
+        public static void TryEncryptEcb_DestinationLarger(byte[] plaintext, byte[] ciphertext, PaddingMode padding)
+        {
+            using (RC2 rc2 = RC2Factory.Create())
+            {
+                rc2.Key = s_rc2OneShotKey;
+
+                Span<byte> largeBuffer = new byte[ciphertext.Length + 10];
+                Span<byte> destinationBuffer = largeBuffer.Slice(0, ciphertext.Length);
+                largeBuffer.Fill(0xCC);
+
+                bool result = rc2.TryEncryptEcb(
+                    plaintext,
+                    destinationBuffer,
+                    padding,
+                    out int bytesWritten);
+
+                Assert.True(result, "TryEncryptEcb");
+                Assert.Equal(destinationBuffer.Length, bytesWritten);
+
+                if (padding == PaddingMode.ISO10126)
+                {
+                    int blockSizeBytes = rc2.BlockSize / 8;
+                    Assert.Equal(ciphertext[..^blockSizeBytes], destinationBuffer[..^blockSizeBytes].ToArray());
+                }
+                else
+                {
+                    Assert.Equal(ciphertext, destinationBuffer.ToArray());
+                }
+
+                AssertFilledWith(0xCC, largeBuffer.Slice(ciphertext.Length));
+            }
+        }
+
+        [Theory]
+        [MemberData(nameof(EcbTestCases))]
+        public static void TryDecryptEcb_Overlaps(byte[] plaintext, byte[] ciphertext, PaddingMode padding)
+        {
+            (int plaintextOffset, int ciphertextOffset)[] offsets =
+            {
+                (0, 0), (8, 0), (0, 8), (8, 8),
+            };
+
+            foreach ((int plaintextOffset, int ciphertextOffset) in offsets)
+            {
+                using (RC2 rc2 = RC2Factory.Create())
+                {
+                    rc2.Key = s_rc2OneShotKey;
+
+                    int expectedPlaintextSize = padding == PaddingMode.Zeros ? ciphertext.Length : plaintext.Length;
+                    int destinationSize = Math.Max(expectedPlaintextSize, ciphertext.Length) + Math.Max(plaintextOffset, ciphertextOffset);
+                    Span<byte> buffer = new byte[destinationSize];
+                    Span<byte> destinationBuffer = buffer.Slice(plaintextOffset, expectedPlaintextSize);
+                    Span<byte> ciphertextBuffer = buffer.Slice(ciphertextOffset, ciphertext.Length);
+                    ciphertext.AsSpan().CopyTo(ciphertextBuffer);
+
+                    bool result = rc2.TryDecryptEcb(ciphertextBuffer, destinationBuffer, padding, out int bytesWritten);
+                    Assert.True(result, "TryDecryptEcb");
+                    Assert.Equal(destinationBuffer.Length, bytesWritten);
+
+                    if (padding == PaddingMode.Zeros)
+                    {
+                        Assert.Equal(plaintext, destinationBuffer.Slice(0, plaintext.Length).ToArray());
+                        AssertFilledWith(0, destinationBuffer.Slice(plaintext.Length));
+                    }
+                    else
+                    {
+                        Assert.Equal(plaintext, destinationBuffer.ToArray());
+                        Assert.True(destinationBuffer.Overlaps(ciphertextBuffer) || plaintext.Length == 0 || ciphertext.Length == 0);
+                    }
+                }
+            }
+        }
+
+        [Theory]
+        [MemberData(nameof(EcbTestCases))]
+        public static void TryEncryptEcb_Overlaps(byte[] plaintext, byte[] ciphertext, PaddingMode padding)
+        {
+            (int plaintextOffset, int ciphertextOffset)[] offsets =
+            {
+                (0, 0), (8, 0), (0, 8), (8, 8),
+            };
+
+            foreach ((int plaintextOffset, int ciphertextOffset) in offsets)
+            {
+                using (RC2 rc2 = RC2Factory.Create())
+                {
+                    rc2.Key = s_rc2OneShotKey;
+
+                    int destinationSize = ciphertext.Length + Math.Max(plaintextOffset, ciphertextOffset);
+                    Span<byte> buffer = new byte[destinationSize];
+                    Span<byte> destinationBuffer = buffer.Slice(ciphertextOffset, ciphertext.Length);
+                    Span<byte> plaintextBuffer = buffer.Slice(plaintextOffset, plaintext.Length);
+                    plaintext.AsSpan().CopyTo(plaintextBuffer);
+
+                    bool result = rc2.TryEncryptEcb(plaintextBuffer, destinationBuffer, padding, out int bytesWritten);
+                    Assert.True(result, "TryEncryptEcb");
+                    Assert.Equal(destinationBuffer.Length, bytesWritten);
+
+                    if (padding == PaddingMode.ISO10126)
+                    {
+                        int blockSizeBytes = rc2.BlockSize / 8;
+                        Assert.Equal(ciphertext[..^blockSizeBytes], destinationBuffer[..^blockSizeBytes].ToArray());
+                    }
+                    else
+                    {
+                        Assert.Equal(ciphertext, destinationBuffer.ToArray());
+                        Assert.True(destinationBuffer.Overlaps(plaintextBuffer) || plaintext.Length == 0 || ciphertext.Length == 0);
+                    }
+                }
+            }
+        }
+
+        [Theory]
+        [MemberData(nameof(EcbTestCases))]
+        public static void DecryptEcb_Span(byte[] plaintext, byte[] ciphertext, PaddingMode padding)
+        {
+            using (RC2 rc2 = RC2Factory.Create())
+            {
+                rc2.Key = s_rc2OneShotKey;
+                byte[] decrypted = rc2.DecryptEcb(ciphertext.AsSpan(), padding);
+
+                if (padding == PaddingMode.Zeros)
+                {
+                    Assert.Equal(plaintext, decrypted.AsSpan(0, plaintext.Length).ToArray());
+                    AssertFilledWith(0, decrypted.AsSpan(plaintext.Length));
+                }
+                else
+                {
+                    Assert.Equal(plaintext, decrypted);
+                }
+            }
+        }
+
+        [Theory]
+        [MemberData(nameof(EcbTestCases))]
+        public static void EncryptEcb_Span(byte[] plaintext, byte[] ciphertext, PaddingMode padding)
+        {
+            using (RC2 rc2 = RC2Factory.Create())
+            {
+                rc2.Key = s_rc2OneShotKey;
+                byte[] encrypted = rc2.EncryptEcb(plaintext.AsSpan(), padding);
+
+                if (padding == PaddingMode.ISO10126)
+                {
+                    int blockSizeBytes = rc2.BlockSize / 8;
+                    Assert.Equal(ciphertext[..^blockSizeBytes], encrypted[..^blockSizeBytes]);
+                }
+                else
+                {
+                    Assert.Equal(ciphertext, encrypted);
+                }
+            }
+        }
+
+        [Theory]
+        [MemberData(nameof(EcbTestCases))]
+        public static void DecryptEcb_Array(byte[] plaintext, byte[] ciphertext, PaddingMode padding)
+        {
+            using (RC2 rc2 = RC2Factory.Create())
+            {
+                rc2.Key = s_rc2OneShotKey;
+                byte[] decrypted = rc2.DecryptEcb(ciphertext, padding);
+
+                if (padding == PaddingMode.Zeros)
+                {
+                    Assert.Equal(plaintext, decrypted.AsSpan(0, plaintext.Length).ToArray());
+                    AssertFilledWith(0, decrypted.AsSpan(plaintext.Length));
+                }
+                else
+                {
+                    Assert.Equal(plaintext, decrypted);
+                }
+            }
+        }
+
+        [Theory]
+        [MemberData(nameof(EcbTestCases))]
+        public static void EncryptEcb_Array(byte[] plaintext, byte[] ciphertext, PaddingMode padding)
+        {
+            using (RC2 rc2 = RC2Factory.Create())
+            {
+                rc2.Key = s_rc2OneShotKey;
+                byte[] encrypted = rc2.EncryptEcb(plaintext, padding);
+
+                if (padding == PaddingMode.ISO10126)
+                {
+                    int blockSizeBytes = rc2.BlockSize / 8;
+                    Assert.Equal(ciphertext[..^blockSizeBytes], encrypted[..^blockSizeBytes]);
+                }
+                else
+                {
+                    Assert.Equal(ciphertext, encrypted);
+                }
+            }
+        }
+
+        private static void AssertFilledWith(byte value, ReadOnlySpan<byte> span)
+        {
+            for (int i = 0; i < span.Length; i++)
+            {
+                Assert.Equal(value, span[i]);
+            }
+        }
+
+        public static IEnumerable<object[]> EcbTestCases
+        {
+            get
+            {
+                // plaintext that is block aligned
+                yield return new object[]
+                {
+                    // plaintext
+                    new byte[]
+                    {
+                        0x50, 0x68, 0x12, 0xA4, 0x5F, 0x08, 0xC8, 0x89,
+                        0xB9, 0x7F, 0x59, 0x80, 0x03, 0x8B, 0x83, 0x59,
+                    },
+
+                    // ciphertext
+                    new byte[]
+                    {
+                        0x8A, 0x65, 0xEB, 0x8C, 0x62, 0x14, 0xDD, 0x83,
+                        0x71, 0x0F, 0x1B, 0x21, 0xAD, 0x5F, 0xCD, 0xC1,
+                        0x9D, 0x70, 0x70, 0x58, 0x47, 0x5A, 0xD0, 0xC8,
+                    },
+
+                    PaddingMode.PKCS7,
+                };
+
+                yield return new object[]
+                {
+                    // plaintext
+                    new byte[]
+                    {
+                        0x50, 0x68, 0x12, 0xA4, 0x5F, 0x08, 0xC8, 0x89,
+                        0xB9, 0x7F, 0x59, 0x80, 0x03, 0x8B, 0x83, 0x59,
+                    },
+
+                    // ciphertext
+                    new byte[]
+                    {
+                        0x8A, 0x65, 0xEB, 0x8C, 0x62, 0x14, 0xDD, 0x83,
+                        0x71, 0x0F, 0x1B, 0x21, 0xAD, 0x5F, 0xCD, 0xC1,
+                    },
+
+                    PaddingMode.None,
+                };
+
+                yield return new object[]
+                {
+                    // plaintext
+                    new byte[]
+                    {
+                        0x50, 0x68, 0x12, 0xA4, 0x5F, 0x08, 0xC8, 0x89,
+                        0xB9, 0x7F, 0x59, 0x80, 0x03, 0x8B, 0x83, 0x59,
+                    },
+
+                    // ciphertext
+                    new byte[]
+                    {
+                        0x8A, 0x65, 0xEB, 0x8C, 0x62, 0x14, 0xDD, 0x83,
+                        0x71, 0x0F, 0x1B, 0x21, 0xAD, 0x5F, 0xCD, 0xC1,
+                    },
+
+                    PaddingMode.Zeros,
+                };
+
+                yield return new object[]
+                {
+                    // plaintext
+                    new byte[]
+                    {
+                        0x50, 0x68, 0x12, 0xA4, 0x5F, 0x08, 0xC8, 0x89,
+                        0xB9, 0x7F, 0x59, 0x80, 0x03, 0x8B, 0x83, 0x59,
+                    },
+
+                    // ciphertext
+                    new byte[]
+                    {
+                        0x8A, 0x65, 0xEB, 0x8C, 0x62, 0x14, 0xDD, 0x83,
+                        0x71, 0x0F, 0x1B, 0x21, 0xAD, 0x5F, 0xCD, 0xC1,
+                        0x72, 0x8A, 0x57, 0x94, 0x2D, 0x79, 0xBD, 0xAA,
+                    },
+
+                    PaddingMode.ANSIX923,
+                };
+
+                yield return new object[]
+                {
+                    // plaintext
+                    new byte[]
+                    {
+                        0x50, 0x68, 0x12, 0xA4, 0x5F, 0x08, 0xC8, 0x89,
+                        0xB9, 0x7F, 0x59, 0x80, 0x03, 0x8B, 0x83, 0x59,
+                    },
+
+                    // ciphertext
+                    new byte[]
+                    {
+                        0x8A, 0x65, 0xEB, 0x8C, 0x62, 0x14, 0xDD, 0x83,
+                        0x71, 0x0F, 0x1B, 0x21, 0xAD, 0x5F, 0xCD, 0xC1,
+                        0xEB, 0x5E, 0x2E, 0xB9, 0x1A, 0x1E, 0x1B, 0xE4,
+                    },
+
+                    PaddingMode.ISO10126,
+                };
+
+                // plaintext requires padding
+                yield return new object[]
+                {
+                    // plaintext
+                    new byte[]
+                    {
+                        0x99, 0x50, 0x68, 0x12, 0xA4, 0x5F, 0x08, 0xC8,
+                        0x89, 0xB9, 0x7F, 0x59, 0x80, 0x03, 0x8B, 0x83,
+                        0x59,
+                    },
+
+                    // ciphertext
+                    new byte[]
+                    {
+                        0x14, 0x76, 0x59, 0x63, 0xBD, 0x9E, 0xF3, 0x2E,
+                        0xF6, 0xA1, 0x05, 0x03, 0x44, 0x59, 0xF5, 0x88,
+                        0xF2, 0x94, 0x11, 0xA3, 0xE8, 0xAD, 0xA7, 0xE6,
+                    },
+
+                    PaddingMode.PKCS7,
+                };
+
+                yield return new object[]
+                {
+                    // plaintext
+                    new byte[]
+                    {
+                        0x99, 0x50, 0x68, 0x12, 0xA4, 0x5F, 0x08, 0xC8,
+                        0x89, 0xB9, 0x7F, 0x59, 0x80, 0x03, 0x8B, 0x83,
+                        0x59,
+                    },
+
+                    // ciphertext
+                    new byte[]
+                    {
+                        0x14, 0x76, 0x59, 0x63, 0xBD, 0x9E, 0xF3, 0x2E,
+                        0xF6, 0xA1, 0x05, 0x03, 0x44, 0x59, 0xF5, 0x88,
+                        0xE3, 0xB2, 0x3D, 0xAA, 0x91, 0x6A, 0xD0, 0x06,
+                    },
+
+                    PaddingMode.Zeros,
+                };
+
+                yield return new object[]
+                {
+                    // plaintext
+                    new byte[]
+                    {
+                        0x99, 0x50, 0x68, 0x12, 0xA4, 0x5F, 0x08, 0xC8,
+                        0x89, 0xB9, 0x7F, 0x59, 0x80, 0x03, 0x8B, 0x83,
+                        0x59,
+                    },
+
+                    // ciphertext
+                    new byte[]
+                    {
+                        0x14, 0x76, 0x59, 0x63, 0xBD, 0x9E, 0xF3, 0x2E,
+                        0xF6, 0xA1, 0x05, 0x03, 0x44, 0x59, 0xF5, 0x88,
+                        0x17, 0x97, 0x3A, 0x77, 0x69, 0x5E, 0x79, 0xE9,
+                    },
+
+                    PaddingMode.ANSIX923,
+                };
+
+                yield return new object[]
+                {
+                    // plaintext
+                    new byte[]
+                    {
+                        0x99, 0x50, 0x68, 0x12, 0xA4, 0x5F, 0x08, 0xC8,
+                        0x89, 0xB9, 0x7F, 0x59, 0x80, 0x03, 0x8B, 0x83,
+                        0x59,
+                    },
+
+                    // ciphertext
+                    new byte[]
+                    {
+                        0x14, 0x76, 0x59, 0x63, 0xBD, 0x9E, 0xF3, 0x2E,
+                        0xF6, 0xA1, 0x05, 0x03, 0x44, 0x59, 0xF5, 0x88,
+                        0x22, 0xC0, 0x50, 0x52, 0x56, 0x5A, 0x15, 0xFD,
+                    },
+
+                    PaddingMode.ISO10126,
+                };
+
+                yield return new object[]
+                {
+                    // plaintext
+                    Array.Empty<byte>(),
+
+                    // ciphertext
+                    Array.Empty<byte>(),
+
+                    PaddingMode.Zeros,
+                };
+
+                yield return new object[]
+                {
+                    // plaintext
+                    Array.Empty<byte>(),
+
+                    // ciphertext
+                    Array.Empty<byte>(),
+
+                    PaddingMode.None,
+                };
+
+                yield return new object[]
+                {
+                    // plaintext
+                    Array.Empty<byte>(),
+
+                    // ciphertext
+                    new byte[]
+                    {
+                        0x9D, 0x70, 0x70, 0x58, 0x47, 0x5A, 0xD0, 0xC8,
+                    },
+
+                    PaddingMode.PKCS7,
+                };
+            }
+        }
+    }
+}
index 7823cc8..7b065d2 100644 (file)
@@ -12,7 +12,7 @@ namespace System.Security.Cryptography.Encryption.RC2.Tests
 
     [SkipOnPlatform(TestPlatforms.Browser, "Not supported on Browser")]
     [ConditionalClass(typeof(RC2Factory), nameof(RC2Factory.IsSupported))]
-    public static class RC2CipherTests
+    public static partial class RC2CipherTests
     {
         // These are the expected output of many decryptions. Changing these values requires re-generating test input.
         private static readonly string s_multiBlockString = new ASCIIEncoding().GetBytes(
diff --git a/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/TripleDES/TripleDESCipherTests.OneShot.cs b/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/TripleDES/TripleDESCipherTests.OneShot.cs
new file mode 100644 (file)
index 0000000..3469f4c
--- /dev/null
@@ -0,0 +1,633 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using System.Security.Cryptography;
+using Test.Cryptography;
+using Xunit;
+
+namespace System.Security.Cryptography.Encryption.TripleDes.Tests
+{
+    public partial class TripleDESCipherTests
+    {
+        private static byte[] s_tdes192OneShotKey = new byte[]
+            {
+                0x00, 0x01, 0x02, 0x03, 0x05, 0x06, 0x07, 0x08,
+                0x0A, 0x0B, 0x0C, 0x0D, 0x0F, 0x10, 0x11, 0x12,
+                0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0xA0,
+            };
+
+        [Theory]
+        [MemberData(nameof(EcbTestCases))]
+        public static void EcbRoundtrip(byte[] plaintext, byte[] ciphertext, PaddingMode padding)
+        {
+            using (TripleDES tdes = TripleDESFactory.Create())
+            {
+                tdes.Key = s_tdes192OneShotKey;
+
+                // Even though we have set the instance to use CFB, the Ecb one shots should
+                // always be done in ECB.
+                tdes.FeedbackSize = 8;
+                tdes.Mode = CipherMode.CFB;
+                tdes.Padding = padding == PaddingMode.None ? PaddingMode.PKCS7 : PaddingMode.None;
+
+                byte[] encrypted = tdes.EncryptEcb(plaintext, padding);
+                byte[] decrypted = tdes.DecryptEcb(encrypted, padding);
+
+                if (padding == PaddingMode.Zeros)
+                {
+                    Assert.Equal(plaintext, decrypted[..plaintext.Length]);
+                    AssertFilledWith(0, plaintext.AsSpan(plaintext.Length));
+                }
+                else
+                {
+                    Assert.Equal(plaintext, decrypted);
+                }
+
+                decrypted = tdes.DecryptEcb(ciphertext, padding);
+                encrypted = tdes.EncryptEcb(decrypted, padding);
+
+                if (padding == PaddingMode.ISO10126)
+                {
+                    int blockSizeBytes = tdes.BlockSize / 8;
+                    Assert.Equal(ciphertext[..^blockSizeBytes], encrypted[..^blockSizeBytes]);
+                }
+                else
+                {
+                    Assert.Equal(ciphertext, encrypted);
+                }
+            }
+        }
+
+        [Theory]
+        [MemberData(nameof(EcbTestCases))]
+        public static void TryDecryptEcb_DestinationTooSmall(byte[] plaintext, byte[] ciphertext, PaddingMode padding)
+        {
+            if (plaintext.Length == 0)
+            {
+                // Can't have a ciphertext length shorter than zero.
+                return;
+            }
+
+            using (TripleDES tdes = TripleDESFactory.Create())
+            {
+                tdes.Key = s_tdes192OneShotKey;
+
+                Span<byte> destinationBuffer = new byte[plaintext.Length - 1];
+
+                bool result = tdes.TryDecryptEcb(ciphertext, destinationBuffer, padding, out int bytesWritten);
+                Assert.False(result, "TryDecryptEcb");
+                Assert.Equal(0, bytesWritten);
+            }
+        }
+
+        [Theory]
+        [MemberData(nameof(EcbTestCases))]
+        public static void TryEncryptEcb_DestinationTooSmall(byte[] plaintext, byte[] ciphertext, PaddingMode padding)
+        {
+            if (ciphertext.Length == 0)
+            {
+                // Can't have a too small buffer for zero.
+                return;
+            }
+
+            using (TripleDES tdes = TripleDESFactory.Create())
+            {
+                tdes.Key = s_tdes192OneShotKey;
+
+                Span<byte> destinationBuffer = new byte[ciphertext.Length - 1];
+
+                bool result = tdes.TryEncryptEcb(plaintext, destinationBuffer, padding, out int bytesWritten);
+                Assert.False(result, "TryDecryptEcb");
+                Assert.Equal(0, bytesWritten);
+            }
+        }
+
+        [Theory]
+        [MemberData(nameof(EcbTestCases))]
+        public static void TryDecryptEcb_DestinationJustRight(byte[] plaintext, byte[] ciphertext, PaddingMode padding)
+        {
+            using (TripleDES tdes = TripleDESFactory.Create())
+            {
+                tdes.Key = s_tdes192OneShotKey;
+
+                int expectedPlaintextSize = padding == PaddingMode.Zeros ? ciphertext.Length : plaintext.Length;
+                Span<byte> destinationBuffer = new byte[expectedPlaintextSize];
+
+                bool result = tdes.TryDecryptEcb(ciphertext, destinationBuffer, padding, out int bytesWritten);
+                Assert.True(result, "TryDecryptEcb");
+                Assert.Equal(destinationBuffer.Length, bytesWritten);
+
+                if (padding == PaddingMode.Zeros)
+                {
+                    Assert.Equal(plaintext, destinationBuffer.Slice(0, plaintext.Length).ToArray());
+                    AssertFilledWith(0, destinationBuffer.Slice(plaintext.Length));
+                }
+                else
+                {
+                    Assert.Equal(plaintext, destinationBuffer.ToArray());
+                }
+            }
+        }
+
+        [Theory]
+        [MemberData(nameof(EcbTestCases))]
+        public static void TryEncryptEcb_DestinationJustRight(byte[] plaintext, byte[] ciphertext, PaddingMode padding)
+        {
+            using (TripleDES tdes = TripleDESFactory.Create())
+            {
+                tdes.Key = s_tdes192OneShotKey;
+
+                int expectedCiphertextSize = tdes.GetCiphertextLengthEcb(plaintext.Length, padding);
+                Span<byte> destinationBuffer = new byte[expectedCiphertextSize];
+
+                bool result = tdes.TryEncryptEcb(plaintext, destinationBuffer, padding, out int bytesWritten);
+                Assert.True(result, "TryEncryptEcb");
+                Assert.Equal(expectedCiphertextSize, bytesWritten);
+
+                if (padding == PaddingMode.ISO10126)
+                {
+                    int blockSizeBytes = tdes.BlockSize / 8;
+                    // Padding is random so we can't validate the last block.
+                    Assert.Equal(ciphertext[..^blockSizeBytes], destinationBuffer[..^blockSizeBytes].ToArray());
+                }
+                else
+                {
+                    Assert.Equal(ciphertext, destinationBuffer.ToArray());
+                }
+            }
+        }
+
+        [Theory]
+        [MemberData(nameof(EcbTestCases))]
+        public static void TryDecryptEcb_DestinationLarger(byte[] plaintext, byte[] ciphertext, PaddingMode padding)
+        {
+            using (TripleDES tdes = TripleDESFactory.Create())
+            {
+                tdes.Key = s_tdes192OneShotKey;
+                int expectedPlaintextSize = padding == PaddingMode.Zeros ? ciphertext.Length : plaintext.Length;
+
+                Span<byte> largeBuffer = new byte[expectedPlaintextSize + 10];
+                Span<byte> destinationBuffer = largeBuffer.Slice(0, expectedPlaintextSize);
+                largeBuffer.Fill(0xCC);
+
+                bool result = tdes.TryDecryptEcb(
+                    ciphertext,
+                    destinationBuffer,
+                    padding,
+                    out int bytesWritten);
+
+                Assert.True(result, "TryDecryptEcb");
+                Assert.Equal(destinationBuffer.Length, bytesWritten);
+
+                if (padding == PaddingMode.Zeros)
+                {
+                    Assert.Equal(plaintext, destinationBuffer.Slice(0, plaintext.Length).ToArray());
+                    AssertFilledWith(0, destinationBuffer.Slice(plaintext.Length));
+                }
+                else
+                {
+                    Assert.Equal(plaintext, destinationBuffer.ToArray());
+                }
+
+                Span<byte> excess = largeBuffer.Slice(destinationBuffer.Length);
+                AssertFilledWith(0xCC, excess);
+            }
+        }
+
+        [Theory]
+        [MemberData(nameof(EcbTestCases))]
+        public static void TryEncryptEcb_DestinationLarger(byte[] plaintext, byte[] ciphertext, PaddingMode padding)
+        {
+            using (TripleDES tdes = TripleDESFactory.Create())
+            {
+                tdes.Key = s_tdes192OneShotKey;
+
+                Span<byte> largeBuffer = new byte[ciphertext.Length + 10];
+                Span<byte> destinationBuffer = largeBuffer.Slice(0, ciphertext.Length);
+                largeBuffer.Fill(0xCC);
+
+                bool result = tdes.TryEncryptEcb(
+                    plaintext,
+                    destinationBuffer,
+                    padding,
+                    out int bytesWritten);
+
+                Assert.True(result, "TryEncryptEcb");
+                Assert.Equal(destinationBuffer.Length, bytesWritten);
+
+                if (padding == PaddingMode.ISO10126)
+                {
+                    int blockSizeBytes = tdes.BlockSize / 8;
+                    Assert.Equal(ciphertext[..^blockSizeBytes], destinationBuffer[..^blockSizeBytes].ToArray());
+                }
+                else
+                {
+                    Assert.Equal(ciphertext, destinationBuffer.ToArray());
+                }
+
+                AssertFilledWith(0xCC, largeBuffer.Slice(ciphertext.Length));
+            }
+        }
+
+        [Theory]
+        [MemberData(nameof(EcbTestCases))]
+        public static void TryDecryptEcb_Overlaps(byte[] plaintext, byte[] ciphertext, PaddingMode padding)
+        {
+            (int plaintextOffset, int ciphertextOffset)[] offsets =
+            {
+                (0, 0), (8, 0), (0, 8), (8, 8),
+            };
+
+            foreach ((int plaintextOffset, int ciphertextOffset) in offsets)
+            {
+                using (TripleDES tdes = TripleDESFactory.Create())
+                {
+                    tdes.Key = s_tdes192OneShotKey;
+
+                    int expectedPlaintextSize = padding == PaddingMode.Zeros ? ciphertext.Length : plaintext.Length;
+                    int destinationSize = Math.Max(expectedPlaintextSize, ciphertext.Length) + Math.Max(plaintextOffset, ciphertextOffset);
+                    Span<byte> buffer = new byte[destinationSize];
+                    Span<byte> destinationBuffer = buffer.Slice(plaintextOffset, expectedPlaintextSize);
+                    Span<byte> ciphertextBuffer = buffer.Slice(ciphertextOffset, ciphertext.Length);
+                    ciphertext.AsSpan().CopyTo(ciphertextBuffer);
+
+                    bool result = tdes.TryDecryptEcb(ciphertextBuffer, destinationBuffer, padding, out int bytesWritten);
+                    Assert.True(result, "TryDecryptEcb");
+                    Assert.Equal(destinationBuffer.Length, bytesWritten);
+
+                    if (padding == PaddingMode.Zeros)
+                    {
+                        Assert.Equal(plaintext, destinationBuffer.Slice(0, plaintext.Length).ToArray());
+                        AssertFilledWith(0, destinationBuffer.Slice(plaintext.Length));
+                    }
+                    else
+                    {
+                        Assert.Equal(plaintext, destinationBuffer.ToArray());
+                        Assert.True(destinationBuffer.Overlaps(ciphertextBuffer) || plaintext.Length == 0 || ciphertext.Length == 0);
+                    }
+                }
+            }
+        }
+
+        [Theory]
+        [MemberData(nameof(EcbTestCases))]
+        public static void TryEncryptEcb_Overlaps(byte[] plaintext, byte[] ciphertext, PaddingMode padding)
+        {
+            (int plaintextOffset, int ciphertextOffset)[] offsets =
+            {
+                (0, 0), (8, 0), (0, 8), (8, 8),
+            };
+
+            foreach ((int plaintextOffset, int ciphertextOffset) in offsets)
+            {
+                using (TripleDES tdes = TripleDESFactory.Create())
+                {
+                    tdes.Key = s_tdes192OneShotKey;
+
+                    int destinationSize = ciphertext.Length + Math.Max(plaintextOffset, ciphertextOffset);
+                    Span<byte> buffer = new byte[destinationSize];
+                    Span<byte> destinationBuffer = buffer.Slice(ciphertextOffset, ciphertext.Length);
+                    Span<byte> plaintextBuffer = buffer.Slice(plaintextOffset, plaintext.Length);
+                    plaintext.AsSpan().CopyTo(plaintextBuffer);
+
+                    bool result = tdes.TryEncryptEcb(plaintextBuffer, destinationBuffer, padding, out int bytesWritten);
+                    Assert.True(result, "TryEncryptEcb");
+                    Assert.Equal(destinationBuffer.Length, bytesWritten);
+
+                    if (padding == PaddingMode.ISO10126)
+                    {
+                        int blockSizeBytes = tdes.BlockSize / 8;
+                        Assert.Equal(ciphertext[..^blockSizeBytes], destinationBuffer[..^blockSizeBytes].ToArray());
+                    }
+                    else
+                    {
+                        Assert.Equal(ciphertext, destinationBuffer.ToArray());
+                        Assert.True(destinationBuffer.Overlaps(plaintextBuffer) || plaintext.Length == 0 || ciphertext.Length == 0);
+                    }
+                }
+            }
+        }
+
+        [Theory]
+        [MemberData(nameof(EcbTestCases))]
+        public static void DecryptEcb_Span(byte[] plaintext, byte[] ciphertext, PaddingMode padding)
+        {
+            using (TripleDES tdes = TripleDESFactory.Create())
+            {
+                tdes.Key = s_tdes192OneShotKey;
+                byte[] decrypted = tdes.DecryptEcb(ciphertext.AsSpan(), padding);
+
+                if (padding == PaddingMode.Zeros)
+                {
+                    Assert.Equal(plaintext, decrypted.AsSpan(0, plaintext.Length).ToArray());
+                    AssertFilledWith(0, decrypted.AsSpan(plaintext.Length));
+                }
+                else
+                {
+                    Assert.Equal(plaintext, decrypted);
+                }
+            }
+        }
+
+        [Theory]
+        [MemberData(nameof(EcbTestCases))]
+        public static void EncryptEcb_Span(byte[] plaintext, byte[] ciphertext, PaddingMode padding)
+        {
+            using (TripleDES tdes = TripleDESFactory.Create())
+            {
+                tdes.Key = s_tdes192OneShotKey;
+                byte[] encrypted = tdes.EncryptEcb(plaintext.AsSpan(), padding);
+
+                if (padding == PaddingMode.ISO10126)
+                {
+                    int blockSizeBytes = tdes.BlockSize / 8;
+                    Assert.Equal(ciphertext[..^blockSizeBytes], encrypted[..^blockSizeBytes]);
+                }
+                else
+                {
+                    Assert.Equal(ciphertext, encrypted);
+                }
+            }
+        }
+
+        [Theory]
+        [MemberData(nameof(EcbTestCases))]
+        public static void DecryptEcb_Array(byte[] plaintext, byte[] ciphertext, PaddingMode padding)
+        {
+            using (TripleDES tdes = TripleDESFactory.Create())
+            {
+                tdes.Key = s_tdes192OneShotKey;
+                byte[] decrypted = tdes.DecryptEcb(ciphertext, padding);
+
+                if (padding == PaddingMode.Zeros)
+                {
+                    Assert.Equal(plaintext, decrypted.AsSpan(0, plaintext.Length).ToArray());
+                    AssertFilledWith(0, decrypted.AsSpan(plaintext.Length));
+                }
+                else
+                {
+                    Assert.Equal(plaintext, decrypted);
+                }
+            }
+        }
+
+        [Theory]
+        [MemberData(nameof(EcbTestCases))]
+        public static void EncryptEcb_Array(byte[] plaintext, byte[] ciphertext, PaddingMode padding)
+        {
+            using (TripleDES tdes = TripleDESFactory.Create())
+            {
+                tdes.Key = s_tdes192OneShotKey;
+                byte[] encrypted = tdes.EncryptEcb(plaintext, padding);
+
+                if (padding == PaddingMode.ISO10126)
+                {
+                    int blockSizeBytes = tdes.BlockSize / 8;
+                    Assert.Equal(ciphertext[..^blockSizeBytes], encrypted[..^blockSizeBytes]);
+                }
+                else
+                {
+                    Assert.Equal(ciphertext, encrypted);
+                }
+            }
+        }
+
+        private static void AssertFilledWith(byte value, ReadOnlySpan<byte> span)
+        {
+            for (int i = 0; i < span.Length; i++)
+            {
+                Assert.Equal(value, span[i]);
+            }
+        }
+
+        public static IEnumerable<object[]> EcbTestCases
+        {
+            get
+            {
+                // plaintext requires no padding
+                yield return new object[]
+                {
+                    // plaintext
+                    new byte[]
+                    {
+                        0x50, 0x68, 0x12, 0xA4, 0x5F, 0x08, 0xC8, 0x89,
+                        0xB9, 0x7F, 0x59, 0x80, 0x03, 0x8B, 0x83, 0x59,
+                    },
+
+                    // ciphertext
+                    new byte[]
+                    {
+                        0x2C, 0xE7, 0xF1, 0x5C, 0x7B, 0xA8, 0x40, 0x0C,
+                        0x1A, 0x09, 0xDC, 0x63, 0x43, 0xC9, 0x1A, 0x63,
+                        0x65, 0xE4, 0x9C, 0xD3, 0xE6, 0xBE, 0xB8, 0x40,
+                    },
+
+                    PaddingMode.PKCS7,
+                };
+
+
+                yield return new object[]
+                {
+                    // plaintext
+                    new byte[]
+                    {
+                        0x50, 0x68, 0x12, 0xA4, 0x5F, 0x08, 0xC8, 0x89,
+                        0xB9, 0x7F, 0x59, 0x80, 0x03, 0x8B, 0x83, 0x59,
+                    },
+
+                    // ciphertext
+                    new byte[]
+                    {
+                        0x2C, 0xE7, 0xF1, 0x5C, 0x7B, 0xA8, 0x40, 0x0C,
+                        0x1A, 0x09, 0xDC, 0x63, 0x43, 0xC9, 0x1A, 0x63,
+                    },
+
+                    PaddingMode.None,
+                };
+
+                yield return new object[]
+                {
+                    // plaintext
+                    new byte[]
+                    {
+                        0x50, 0x68, 0x12, 0xA4, 0x5F, 0x08, 0xC8, 0x89,
+                        0xB9, 0x7F, 0x59, 0x80, 0x03, 0x8B, 0x83, 0x59,
+                    },
+
+                    // ciphertext
+                    new byte[]
+                    {
+                        0x2C, 0xE7, 0xF1, 0x5C, 0x7B, 0xA8, 0x40, 0x0C,
+                        0x1A, 0x09, 0xDC, 0x63, 0x43, 0xC9, 0x1A, 0x63,
+                    },
+
+                    PaddingMode.Zeros,
+                };
+
+                yield return new object[]
+                {
+                    // plaintext
+                    new byte[]
+                    {
+                        0x50, 0x68, 0x12, 0xA4, 0x5F, 0x08, 0xC8, 0x89,
+                        0xB9, 0x7F, 0x59, 0x80, 0x03, 0x8B, 0x83, 0x59,
+                    },
+
+                    // ciphertext
+                    new byte[]
+                    {
+                        0x2C, 0xE7, 0xF1, 0x5C, 0x7B, 0xA8, 0x40, 0x0C,
+                        0x1A, 0x09, 0xDC, 0x63, 0x43, 0xC9, 0x1A, 0x63,
+                        0x34, 0xE6, 0x86, 0x6D, 0x94, 0x2E, 0x98, 0x0F,
+                    },
+
+                    PaddingMode.ANSIX923,
+                };
+
+                yield return new object[]
+                {
+                    // plaintext
+                    new byte[]
+                    {
+                        0x50, 0x68, 0x12, 0xA4, 0x5F, 0x08, 0xC8, 0x89,
+                        0xB9, 0x7F, 0x59, 0x80, 0x03, 0x8B, 0x83, 0x59,
+                    },
+
+                    // ciphertext
+                    new byte[]
+                    {
+                        0x2C, 0xE7, 0xF1, 0x5C, 0x7B, 0xA8, 0x40, 0x0C,
+                        0x1A, 0x09, 0xDC, 0x63, 0x43, 0xC9, 0x1A, 0x63,
+                        0x5E, 0xEE, 0x73, 0xBB, 0x94, 0xED, 0x29, 0x7A,
+                    },
+
+                    PaddingMode.ISO10126,
+                };
+
+                // plaintext requires padding
+                yield return new object[]
+                {
+                    // plaintext
+                    new byte[]
+                    {
+                        0x99, 0x50, 0x68, 0x12, 0xA4, 0x5F, 0x08, 0xC8,
+                        0x89, 0xB9, 0x7F, 0x59, 0x80, 0x03, 0x8B, 0x83,
+                        0x59,
+                    },
+
+                    // ciphertext
+                    new byte[]
+                    {
+                        0xD5, 0xD5, 0x80, 0x3F, 0xC3, 0x7E, 0x4A, 0xE4,
+                        0xF2, 0x93, 0x9B, 0xC3, 0xDC, 0x4F, 0xA0, 0x23,
+                        0xB1, 0x3D, 0x05, 0x93, 0x98, 0xE6, 0x2C, 0xDF,
+                    },
+
+                    PaddingMode.PKCS7,
+                };
+
+                yield return new object[]
+                {
+                    // plaintext
+                    new byte[]
+                    {
+                        0x99, 0x50, 0x68, 0x12, 0xA4, 0x5F, 0x08, 0xC8,
+                        0x89, 0xB9, 0x7F, 0x59, 0x80, 0x03, 0x8B, 0x83,
+                        0x59,
+                    },
+
+                    // ciphertext
+                    new byte[]
+                    {
+                        0xD5, 0xD5, 0x80, 0x3F, 0xC3, 0x7E, 0x4A, 0xE4,
+                        0xF2, 0x93, 0x9B, 0xC3, 0xDC, 0x4F, 0xA0, 0x23,
+                        0xC9, 0x52, 0x8F, 0xC1, 0x30, 0xC0, 0x7C, 0x63,
+                    },
+
+                    PaddingMode.Zeros,
+                };
+
+                yield return new object[]
+                {
+                    // plaintext
+                    new byte[]
+                    {
+                        0x99, 0x50, 0x68, 0x12, 0xA4, 0x5F, 0x08, 0xC8,
+                        0x89, 0xB9, 0x7F, 0x59, 0x80, 0x03, 0x8B, 0x83,
+                        0x59,
+                    },
+
+                    // ciphertext
+                    new byte[]
+                    {
+                        0xD5, 0xD5, 0x80, 0x3F, 0xC3, 0x7E, 0x4A, 0xE4,
+                        0xF2, 0x93, 0x9B, 0xC3, 0xDC, 0x4F, 0xA0, 0x23,
+                        0x6A, 0x97, 0x38, 0x85, 0x3B, 0x48, 0x81, 0x5E,
+                    },
+
+                    PaddingMode.ANSIX923,
+                };
+
+                yield return new object[]
+                {
+                    // plaintext
+                    new byte[]
+                    {
+                        0x99, 0x50, 0x68, 0x12, 0xA4, 0x5F, 0x08, 0xC8,
+                        0x89, 0xB9, 0x7F, 0x59, 0x80, 0x03, 0x8B, 0x83,
+                        0x59,
+                    },
+
+                    // ciphertext
+                    new byte[]
+                    {
+                        0xD5, 0xD5, 0x80, 0x3F, 0xC3, 0x7E, 0x4A, 0xE4,
+                        0xF2, 0x93, 0x9B, 0xC3, 0xDC, 0x4F, 0xA0, 0x23,
+                        0x33, 0x58, 0x09, 0x2C, 0xD8, 0xB5, 0x36, 0xAD,
+                    },
+
+                    PaddingMode.ISO10126,
+                };
+
+                yield return new object[]
+                {
+                    // plaintext
+                    Array.Empty<byte>(),
+
+                    // ciphertext
+                    Array.Empty<byte>(),
+
+                    PaddingMode.Zeros,
+                };
+
+                yield return new object[]
+                {
+                    // plaintext
+                    Array.Empty<byte>(),
+
+                    // ciphertext
+                    Array.Empty<byte>(),
+
+                    PaddingMode.None,
+                };
+
+                yield return new object[]
+                {
+                    // plaintext
+                    Array.Empty<byte>(),
+
+                    // ciphertext
+                    new byte[]
+                    {
+                        0x65, 0xE4, 0x9C, 0xD3, 0xE6, 0xBE, 0xB8, 0x40,
+                    },
+
+                    PaddingMode.PKCS7,
+                };
+            }
+        }
+    }
+}
index ce92a62..9bc2b80 100644 (file)
@@ -9,7 +9,7 @@ using Xunit;
 namespace System.Security.Cryptography.Encryption.TripleDes.Tests
 {
     [SkipOnPlatform(TestPlatforms.Browser, "Not supported on Browser")]
-    public static class TripleDESCipherTests
+    public static partial class TripleDESCipherTests
     {
         [Fact]
         public static void TripleDESDefaults()
index 1c7fbb1..9439209 100644 (file)
@@ -7,7 +7,7 @@ namespace Internal.Cryptography
 {
     internal sealed partial class AesImplementation
     {
-        private static ICryptoTransform CreateTransformCore(
+        private static UniversalCryptoTransform CreateTransformCore(
             CipherMode cipherMode,
             PaddingMode paddingMode,
             byte[] key,
index 378b4cf..1fd8e00 100644 (file)
@@ -8,7 +8,7 @@ namespace Internal.Cryptography
 {
     internal sealed partial class AesImplementation
     {
-        private static ICryptoTransform CreateTransformCore(
+        private static UniversalCryptoTransform CreateTransformCore(
             CipherMode cipherMode,
             PaddingMode paddingMode,
             byte[] key,
index 953bcde..9bb0a9e 100644 (file)
@@ -8,7 +8,7 @@ namespace Internal.Cryptography
 {
     internal sealed partial class AesImplementation
     {
-        private static ICryptoTransform CreateTransformCore(
+        private static UniversalCryptoTransform CreateTransformCore(
             CipherMode cipherMode,
             PaddingMode paddingMode,
             byte[] key,
index 9041682..f00ee55 100644 (file)
@@ -43,6 +43,50 @@ namespace Internal.Cryptography
             base.Dispose(disposing);
         }
 
+        protected override bool TryDecryptEcbCore(
+            ReadOnlySpan<byte> ciphertext,
+            Span<byte> destination,
+            PaddingMode paddingMode,
+            out int bytesWritten)
+        {
+            UniversalCryptoTransform transform = CreateTransformCore(
+                CipherMode.ECB,
+                paddingMode,
+                Key,
+                iv: null,
+                blockSize: BlockSize / BitsPerByte,
+                paddingSize: BlockSize / BitsPerByte,
+                0, /*feedback size */
+                encrypting: false);
+
+            using (transform)
+            {
+                return transform.TransformOneShot(ciphertext, destination, out bytesWritten);
+            }
+        }
+
+        protected override bool TryEncryptEcbCore(
+            ReadOnlySpan<byte> plaintext,
+            Span<byte> destination,
+            PaddingMode paddingMode,
+            out int bytesWritten)
+        {
+            UniversalCryptoTransform transform = CreateTransformCore(
+                CipherMode.ECB,
+                paddingMode,
+                Key,
+                iv: null,
+                blockSize: BlockSize / BitsPerByte,
+                paddingSize: BlockSize / BitsPerByte,
+                0, /*feedback size */
+                encrypting: true);
+
+            using (transform)
+            {
+                return transform.TransformOneShot(plaintext, destination, out bytesWritten);
+            }
+        }
+
         private ICryptoTransform CreateTransform(byte[] rgbKey, byte[]? rgbIV, bool encrypting)
         {
             // note: rbgIV is guaranteed to be cloned before this method, so no need to clone it again
@@ -66,7 +110,15 @@ namespace Internal.Cryptography
                 ValidateCFBFeedbackSize(FeedbackSize);
             }
 
-            return CreateTransformCore(Mode, Padding, rgbKey, rgbIV, BlockSize / BitsPerByte, this.GetPaddingSize(), FeedbackSize / BitsPerByte, encrypting);
+            return CreateTransformCore(
+                Mode,
+                Padding,
+                rgbKey,
+                rgbIV,
+                BlockSize / BitsPerByte,
+                this.GetPaddingSize(Mode, FeedbackSize),
+                FeedbackSize / BitsPerByte,
+                encrypting);
         }
 
         private static void ValidateCFBFeedbackSize(int feedback)
index 0400c5c..9c406f8 100644 (file)
@@ -70,7 +70,27 @@ namespace Internal.Cryptography
             Debug.Assert((input.Length % PaddingSizeInBytes) == 0);
             Debug.Assert(input.Length <= output.Length);
 
-            int written = ProcessFinalBlock(input, output);
+            int written = 0;
+
+            if (input.Overlaps(output, out int offset) && offset != 0)
+            {
+                byte[] rented = CryptoPool.Rent(output.Length);
+
+                try
+                {
+                    written = ProcessFinalBlock(input, rented);
+                    rented.AsSpan(0, written).CopyTo(output);
+                }
+                finally
+                {
+                    CryptoPool.Return(rented, clearSize: written);
+                }
+            }
+            else
+            {
+                written = ProcessFinalBlock(input, output);
+            }
+
             Reset();
             return written;
         }
index 7e8ebb8..7596652 100644 (file)
@@ -9,7 +9,7 @@ namespace Internal.Cryptography
 {
     internal sealed partial class DesImplementation
     {
-        private static ICryptoTransform CreateTransformCore(
+        private static UniversalCryptoTransform CreateTransformCore(
             CipherMode cipherMode,
             PaddingMode paddingMode,
             byte[] key,
index 72714b4..015c3a0 100644 (file)
@@ -7,7 +7,7 @@ namespace Internal.Cryptography
 {
     internal sealed partial class DesImplementation
     {
-        private static ICryptoTransform CreateTransformCore(
+        private static UniversalCryptoTransform CreateTransformCore(
             CipherMode cipherMode,
             PaddingMode paddingMode,
             byte[] key,
index f9f2119..d4f7d6d 100644 (file)
@@ -9,7 +9,7 @@ namespace Internal.Cryptography
 {
     internal sealed partial class DesImplementation
     {
-        private static ICryptoTransform CreateTransformCore(
+        private static UniversalCryptoTransform CreateTransformCore(
             CipherMode cipherMode,
             PaddingMode paddingMode,
             byte[] key,
index dba5858..cdc8e28 100644 (file)
@@ -8,7 +8,7 @@ namespace Internal.Cryptography
 {
     internal sealed partial class DesImplementation
     {
-        private static ICryptoTransform CreateTransformCore(
+        private static UniversalCryptoTransform CreateTransformCore(
             CipherMode cipherMode,
             PaddingMode paddingMode,
             byte[] key,
index 9c1bf00..34f0fc6 100644 (file)
@@ -76,7 +76,59 @@ namespace Internal.Cryptography
                 ValidateCFBFeedbackSize(FeedbackSize);
             }
 
-            return CreateTransformCore(Mode, Padding, rgbKey, rgbIV, BlockSize / BitsPerByte, FeedbackSize / BitsPerByte, this.GetPaddingSize(), encrypting);
+            return CreateTransformCore(
+                Mode,
+                Padding,
+                rgbKey,
+                rgbIV,
+                BlockSize / BitsPerByte,
+                FeedbackSize / BitsPerByte,
+                this.GetPaddingSize(Mode, FeedbackSize),
+                encrypting);
+        }
+
+        protected override bool TryDecryptEcbCore(
+            ReadOnlySpan<byte> ciphertext,
+            Span<byte> destination,
+            PaddingMode paddingMode,
+            out int bytesWritten)
+        {
+            UniversalCryptoTransform transform = CreateTransformCore(
+                CipherMode.ECB,
+                paddingMode,
+                Key,
+                iv: null,
+                blockSize: BlockSize / BitsPerByte,
+                0, /*feedback size */
+                paddingSize: BlockSize / BitsPerByte,
+                encrypting: false);
+
+            using (transform)
+            {
+                return transform.TransformOneShot(ciphertext, destination, out bytesWritten);
+            }
+        }
+
+        protected override bool TryEncryptEcbCore(
+            ReadOnlySpan<byte> plaintext,
+            Span<byte> destination,
+            PaddingMode paddingMode,
+            out int bytesWritten)
+        {
+            UniversalCryptoTransform transform = CreateTransformCore(
+                CipherMode.ECB,
+                paddingMode,
+                Key,
+                iv: null,
+                blockSize: BlockSize / BitsPerByte,
+                0, /*feedback size */
+                paddingSize: BlockSize / BitsPerByte,
+                encrypting: true);
+
+            using (transform)
+            {
+                return transform.TransformOneShot(plaintext, destination, out bytesWritten);
+            }
         }
 
         private static void ValidateCFBFeedbackSize(int feedback)
index fa5604a..3944890 100644 (file)
@@ -9,7 +9,7 @@ namespace Internal.Cryptography
     [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA5350", Justification = "We are providing the implementation for RC2, not consuming it.")]
     internal sealed partial class RC2Implementation : RC2
     {
-        private static ICryptoTransform CreateTransformCore(
+        private static UniversalCryptoTransform CreateTransformCore(
             CipherMode cipherMode,
             PaddingMode paddingMode,
             byte[] key,
index 1473dc7..af2340f 100644 (file)
@@ -7,7 +7,7 @@ namespace Internal.Cryptography
 {
     internal sealed partial class RC2Implementation
     {
-        private static ICryptoTransform CreateTransformCore(
+        private static UniversalCryptoTransform CreateTransformCore(
             CipherMode cipherMode,
             PaddingMode paddingMode,
             byte[] key,
index 4f85d08..d33174d 100644 (file)
@@ -9,7 +9,7 @@ namespace Internal.Cryptography
 {
     internal sealed partial class RC2Implementation
     {
-        private static ICryptoTransform CreateTransformCore(
+        private static UniversalCryptoTransform CreateTransformCore(
             CipherMode cipherMode,
             PaddingMode paddingMode,
             byte[] key,
index 5c038c3..1a934ec 100644 (file)
@@ -9,7 +9,7 @@ namespace Internal.Cryptography
 {
     internal sealed partial class RC2Implementation
     {
-        private static ICryptoTransform CreateTransformCore(
+        private static UniversalCryptoTransform CreateTransformCore(
             CipherMode cipherMode,
             PaddingMode paddingMode,
             byte[] key,
index f4f1ed1..b4c8029 100644 (file)
@@ -61,8 +61,7 @@ namespace Internal.Cryptography
             if (rgbKey == null)
                 throw new ArgumentNullException(nameof(rgbKey));
 
-            long keySize = rgbKey.Length * (long)BitsPerByte;
-            if (keySize > int.MaxValue || !((int)keySize).IsLegalSize(LegalKeySizes))
+            if (!ValidKeySize(rgbKey.Length, out int keySize))
                 throw new ArgumentException(SR.Cryptography_InvalidKeySize, nameof(rgbKey));
 
             if (rgbIV != null)
@@ -77,10 +76,64 @@ namespace Internal.Cryptography
                 ValidateCFBFeedbackSize(FeedbackSize);
             }
 
-            int effectiveKeySize = EffectiveKeySizeValue == 0 ? (int)keySize : EffectiveKeySize;
+            int effectiveKeySize = EffectiveKeySizeValue == 0 ? keySize : EffectiveKeySize;
             return CreateTransformCore(Mode, Padding, rgbKey, effectiveKeySize, rgbIV, BlockSize / BitsPerByte, FeedbackSize / BitsPerByte, GetPaddingSize(), encrypting);
         }
 
+        protected override bool TryDecryptEcbCore(
+            ReadOnlySpan<byte> ciphertext,
+            Span<byte> destination,
+            PaddingMode paddingMode,
+            out int bytesWritten)
+        {
+            if (!ValidKeySize(Key.Length, out int keySize))
+                throw new InvalidOperationException(SR.Cryptography_InvalidKeySize);
+
+            int effectiveKeySize = EffectiveKeySizeValue == 0 ? keySize : EffectiveKeySize;
+            UniversalCryptoTransform transform = CreateTransformCore(
+                CipherMode.ECB,
+                paddingMode,
+                Key,
+                effectiveKeyLength: effectiveKeySize,
+                iv: null,
+                blockSize: BlockSize / BitsPerByte,
+                0, /*feedback size */
+                paddingSize: BlockSize / BitsPerByte,
+                encrypting: false);
+
+            using (transform)
+            {
+                return transform.TransformOneShot(ciphertext, destination, out bytesWritten);
+            }
+        }
+
+        protected override bool TryEncryptEcbCore(
+            ReadOnlySpan<byte> plaintext,
+            Span<byte> destination,
+            PaddingMode paddingMode,
+            out int bytesWritten)
+        {
+            if (!ValidKeySize(Key.Length, out int keySize))
+                throw new InvalidOperationException(SR.Cryptography_InvalidKeySize);
+
+            int effectiveKeySize = EffectiveKeySizeValue == 0 ? keySize : EffectiveKeySize;
+            UniversalCryptoTransform transform = CreateTransformCore(
+                CipherMode.ECB,
+                paddingMode,
+                Key,
+                effectiveKeyLength: effectiveKeySize,
+                iv: null,
+                blockSize: BlockSize / BitsPerByte,
+                0, /*feedback size */
+                paddingSize: BlockSize / BitsPerByte,
+                encrypting: true);
+
+            using (transform)
+            {
+                return transform.TransformOneShot(plaintext, destination, out bytesWritten);
+            }
+        }
+
         private static void ValidateCFBFeedbackSize(int feedback)
         {
             // CFB not supported at all
@@ -91,5 +144,17 @@ namespace Internal.Cryptography
         {
             return BlockSize / BitsPerByte;
         }
+
+        private bool ValidKeySize(int keySizeBytes, out int keySizeBits)
+        {
+            if (keySizeBytes > (int.MaxValue / BitsPerByte))
+            {
+                keySizeBits = 0;
+                return false;
+            }
+
+            keySizeBits = keySizeBytes << 3;
+            return keySizeBits.IsLegalSize(LegalKeySizes);
+        }
     }
 }
index 34d3dcf..461a07a 100644 (file)
@@ -7,7 +7,7 @@ namespace Internal.Cryptography
 {
     internal sealed partial class TripleDesImplementation
     {
-        private static ICryptoTransform CreateTransformCore(
+        private static UniversalCryptoTransform CreateTransformCore(
             CipherMode cipherMode,
             PaddingMode paddingMode,
             byte[] key,
index 2044b3f..b9335a1 100644 (file)
@@ -8,7 +8,7 @@ namespace Internal.Cryptography
 {
     internal sealed partial class TripleDesImplementation
     {
-        private static ICryptoTransform CreateTransformCore(
+        private static UniversalCryptoTransform CreateTransformCore(
             CipherMode cipherMode,
             PaddingMode paddingMode,
             byte[] key,
index b6ea4fc..577c771 100644 (file)
@@ -8,7 +8,7 @@ namespace Internal.Cryptography
 {
     internal sealed partial class TripleDesImplementation
     {
-        private static ICryptoTransform CreateTransformCore(
+        private static UniversalCryptoTransform CreateTransformCore(
             CipherMode cipherMode,
             PaddingMode paddingMode,
             byte[] key,
index 691c573..8c0e282 100644 (file)
@@ -81,7 +81,59 @@ namespace Internal.Cryptography
                 ValidateCFBFeedbackSize(FeedbackSize);
             }
 
-            return CreateTransformCore(Mode, Padding, rgbKey, rgbIV, BlockSize / BitsPerByte, this.GetPaddingSize(), FeedbackSize / BitsPerByte, encrypting);
+            return CreateTransformCore(
+                Mode,
+                Padding,
+                rgbKey,
+                rgbIV,
+                BlockSize / BitsPerByte,
+                this.GetPaddingSize(Mode, FeedbackSize),
+                FeedbackSize / BitsPerByte,
+                encrypting);
+        }
+
+        protected override bool TryDecryptEcbCore(
+            ReadOnlySpan<byte> ciphertext,
+            Span<byte> destination,
+            PaddingMode paddingMode,
+            out int bytesWritten)
+        {
+            UniversalCryptoTransform transform = CreateTransformCore(
+                CipherMode.ECB,
+                paddingMode,
+                Key,
+                iv: null,
+                blockSize: BlockSize / BitsPerByte,
+                paddingSize: BlockSize / BitsPerByte,
+                0, /*feedback size */
+                encrypting: false);
+
+            using (transform)
+            {
+                return transform.TransformOneShot(ciphertext, destination, out bytesWritten);
+            }
+        }
+
+        protected override bool TryEncryptEcbCore(
+            ReadOnlySpan<byte> plaintext,
+            Span<byte> destination,
+            PaddingMode paddingMode,
+            out int bytesWritten)
+        {
+            UniversalCryptoTransform transform = CreateTransformCore(
+                CipherMode.ECB,
+                paddingMode,
+                Key,
+                iv: null,
+                blockSize: BlockSize / BitsPerByte,
+                paddingSize: BlockSize / BitsPerByte,
+                0, /*feedback size */
+                encrypting: true);
+
+            using (transform)
+            {
+                return transform.TransformOneShot(plaintext, destination, out bytesWritten);
+            }
         }
 
         private static void ValidateCFBFeedbackSize(int feedback)
index 5577ac6..01c484e 100644 (file)
@@ -15,6 +15,8 @@
              Link="CommonTest\System\Security\Cryptography\AlgorithmImplementations\AES\AesCipherTests.Data.cs" />
     <Compile Include="$(CommonTestPath)System\Security\Cryptography\AlgorithmImplementations\AES\AesCipherTests.cs"
              Link="CommonTest\System\Security\Cryptography\AlgorithmImplementations\AES\AesCipherTests.cs" />
+    <Compile Include="$(CommonTestPath)System\Security\Cryptography\AlgorithmImplementations\AES\AesCipherTests.OneShot.cs"
+             Link="CommonTest\System\Security\Cryptography\AlgorithmImplementations\AES\AesCipherTests.OneShot.cs" />
     <Compile Include="$(CommonTestPath)System\Security\Cryptography\AlgorithmImplementations\AES\AesContractTests.cs"
              Link="CommonTest\System\Security\Cryptography\AlgorithmImplementations\AES\AesContractTests.cs" />
     <Compile Include="$(CommonTestPath)System\Security\Cryptography\AlgorithmImplementations\AES\AesCornerTests.cs"
@@ -65,6 +67,8 @@
              Link="CommonTest\System\Security\Cryptography\AlgorithmImplementations\RSA\TestData.cs" />
     <Compile Include="$(CommonTestPath)System\Security\Cryptography\AlgorithmImplementations\TripleDES\TripleDESCipherTests.cs"
              Link="CommonTest\System\Security\Cryptography\AlgorithmImplementations\TripleDES\TripleDESCipherTests.cs" />
+    <Compile Include="$(CommonTestPath)System\Security\Cryptography\AlgorithmImplementations\TripleDES\TripleDESCipherTests.OneShot.cs"
+             Link="CommonTest\System\Security\Cryptography\AlgorithmImplementations\TripleDES\TripleDESCipherTests.OneShot.cs" />
     <Compile Include="$(CommonTestPath)System\Security\Cryptography\AlgorithmImplementations\TripleDES\TripleDESContractTests.cs"
              Link="CommonTest\System\Security\Cryptography\AlgorithmImplementations\TripleDES\TripleDESContractTests.cs" />
     <Compile Include="$(CommonTestPath)System\Security\Cryptography\AlgorithmImplementations\TripleDES\TripleDESFactory.cs"
              Link="CommonTest\System\Security\Cryptography\AsymmetricSignatureFormatter.cs" />
     <Compile Include="$(CommonTestPath)System\Security\Cryptography\AlgorithmImplementations\DES\DESCipherTests.cs"
              Link="CommonTest\System\Security\Cryptography\AlgorithmImplementations\DES\DESCipherTests.cs" />
+    <Compile Include="$(CommonTestPath)System\Security\Cryptography\AlgorithmImplementations\DES\DESCipherTests.OneShot.cs"
+             Link="CommonTest\System\Security\Cryptography\AlgorithmImplementations\DES\DESCipherTests.OneShot.cs" />
     <Compile Include="$(CommonTestPath)System\Security\Cryptography\AlgorithmImplementations\DES\DESContractTests.cs"
              Link="CommonTest\System\Security\Cryptography\AlgorithmImplementations\DES\DESContractTests.cs" />
     <Compile Include="$(CommonTestPath)System\Security\Cryptography\AlgorithmImplementations\DES\DESFactory.cs"
              Link="CommonTest\System\Security\Cryptography\AlgorithmImplementations\ECDsa\ECDsaXml.cs" />
     <Compile Include="$(CommonTestPath)System\Security\Cryptography\AlgorithmImplementations\RC2\RC2CipherTests.cs"
              Link="CommonTest\System\Security\Cryptography\AlgorithmImplementations\RC2\RC2CipherTests.cs" />
+    <Compile Include="$(CommonTestPath)System\Security\Cryptography\AlgorithmImplementations\RC2\RC2CipherTests.OneShot.cs"
+             Link="CommonTest\System\Security\Cryptography\AlgorithmImplementations\RC2\RC2CipherTests.OneShot.cs" />
     <Compile Include="$(CommonTestPath)System\Security\Cryptography\AlgorithmImplementations\RC2\RC2ContractTests.cs"
              Link="CommonTest\System\Security\Cryptography\AlgorithmImplementations\RC2\RC2ContractTests.cs" />
     <Compile Include="$(CommonTestPath)System\Security\Cryptography\AlgorithmImplementations\RC2\RC2Factory.cs"
index 2fee494..4efd717 100644 (file)
@@ -21,7 +21,7 @@ namespace Internal.Cryptography
         //
         // The delegate must instantiate a new CngKey, based on a new underlying NCryptKeyHandle, each time is called.
         //
-        public BasicSymmetricCipherNCrypt(Func<CngKey> cngKeyFactory, CipherMode cipherMode, int blockSizeInBytes, byte[] iv, bool encrypting, int feedbackSizeInBytes, int paddingSize)
+        public BasicSymmetricCipherNCrypt(Func<CngKey> cngKeyFactory, CipherMode cipherMode, int blockSizeInBytes, byte[]? iv, bool encrypting, int feedbackSizeInBytes, int paddingSize)
             : base(iv, blockSizeInBytes, paddingSize)
         {
             _encrypting = encrypting;
index 6aa654e..6b323cc 100644 (file)
@@ -111,25 +111,35 @@ namespace Internal.Cryptography
 
         public ICryptoTransform CreateEncryptor(byte[] rgbKey, byte[]? rgbIV)
         {
-            return CreateCryptoTransform(rgbKey, rgbIV, encrypting: true);
+            return CreateCryptoTransform(rgbKey, rgbIV, encrypting: true, _outer.Padding, _outer.Mode);
         }
 
         public ICryptoTransform CreateDecryptor(byte[] rgbKey, byte[]? rgbIV)
         {
-            return CreateCryptoTransform(rgbKey, rgbIV, encrypting: false);
+            return CreateCryptoTransform(rgbKey, rgbIV, encrypting: false, _outer.Padding, _outer.Mode);
         }
 
         private ICryptoTransform CreateCryptoTransform(bool encrypting)
         {
             if (KeyInPlainText)
             {
-                return CreateCryptoTransform(_outer.BaseKey, _outer.IV, encrypting);
+                return CreateCryptoTransform(_outer.BaseKey, _outer.IV, encrypting, _outer.Padding, _outer.Mode);
             }
 
-            return CreatePersistedCryptoTransformCore(ProduceCngKey, _outer.IV, encrypting);
+            return CreatePersistedCryptoTransformCore(ProduceCngKey, _outer.IV, encrypting, _outer.Padding, _outer.Mode);
         }
 
-        private ICryptoTransform CreateCryptoTransform(byte[] rgbKey, byte[]? rgbIV, bool encrypting)
+        public UniversalCryptoTransform CreateCryptoTransform(byte[]? iv, bool encrypting, PaddingMode padding, CipherMode mode)
+        {
+            if (KeyInPlainText)
+            {
+                return CreateCryptoTransform(_outer.BaseKey, iv, encrypting, padding, mode);
+            }
+
+            return CreatePersistedCryptoTransformCore(ProduceCngKey, iv, encrypting, padding, mode);
+        }
+
+        private UniversalCryptoTransform CreateCryptoTransform(byte[] rgbKey, byte[]? rgbIV, bool encrypting, PaddingMode padding, CipherMode mode)
         {
             if (rgbKey == null)
                 throw new ArgumentNullException(nameof(rgbKey));
@@ -148,39 +158,46 @@ namespace Internal.Cryptography
 
             // CloneByteArray is null-preserving. So even when GetCipherIv returns null the iv variable
             // is correct, and detached from the input parameter.
-            byte[]? iv = _outer.Mode.GetCipherIv(rgbIV).CloneByteArray();
+            byte[]? iv = mode.GetCipherIv(rgbIV).CloneByteArray();
 
             key = _outer.PreprocessKey(key);
 
-            return CreateEphemeralCryptoTransformCore(key, iv, encrypting);
+            return CreateEphemeralCryptoTransformCore(key, iv, encrypting, padding, mode);
         }
 
-        private ICryptoTransform CreateEphemeralCryptoTransformCore(byte[] key, byte[]? iv, bool encrypting)
+        private UniversalCryptoTransform CreateEphemeralCryptoTransformCore(byte[] key, byte[]? iv, bool encrypting, PaddingMode padding, CipherMode mode)
         {
             int blockSizeInBytes = _outer.BlockSize.BitSizeToByteSize();
-            SafeAlgorithmHandle algorithmModeHandle = _outer.GetEphemeralModeHandle();
+            SafeAlgorithmHandle algorithmModeHandle = _outer.GetEphemeralModeHandle(mode);
 
             BasicSymmetricCipher cipher = new BasicSymmetricCipherBCrypt(
                 algorithmModeHandle,
-                _outer.Mode,
+                mode,
                 blockSizeInBytes,
-                _outer.GetPaddingSize(),
+                _outer.GetPaddingSize(mode, _outer.FeedbackSize),
                 key,
-                false,
+                ownsParentHandle: false,
                 iv,
                 encrypting);
 
-            return UniversalCryptoTransform.Create(_outer.Padding, cipher, encrypting);
+            return UniversalCryptoTransform.Create(padding, cipher, encrypting);
         }
 
-        private ICryptoTransform CreatePersistedCryptoTransformCore(Func<CngKey> cngKeyFactory, byte[] iv, bool encrypting)
+        private UniversalCryptoTransform CreatePersistedCryptoTransformCore(Func<CngKey> cngKeyFactory, byte[]? iv, bool encrypting, PaddingMode padding, CipherMode mode)
         {
             // note: iv is guaranteed to be cloned before this method, so no need to clone it again
 
             int blockSizeInBytes = _outer.BlockSize.BitSizeToByteSize();
             int feedbackSizeInBytes = _outer.FeedbackSize;
-            BasicSymmetricCipher cipher = new BasicSymmetricCipherNCrypt(cngKeyFactory, _outer.Mode, blockSizeInBytes, iv, encrypting, feedbackSizeInBytes, _outer.GetPaddingSize());
-            return UniversalCryptoTransform.Create(_outer.Padding, cipher, encrypting);
+            BasicSymmetricCipher cipher = new BasicSymmetricCipherNCrypt(
+                cngKeyFactory,
+                mode,
+                blockSizeInBytes,
+                iv,
+                encrypting,
+                feedbackSizeInBytes,
+                _outer.GetPaddingSize(mode, _outer.FeedbackSize));
+            return UniversalCryptoTransform.Create(padding, cipher, encrypting);
         }
 
         private CngKey ProduceCngKey()
index 8fdf656..1e1edf7 100644 (file)
@@ -28,9 +28,9 @@ namespace Internal.Cryptography
 
         // Other members.
         bool IsWeakKey(byte[] key);
-        SafeAlgorithmHandle GetEphemeralModeHandle();
+        SafeAlgorithmHandle GetEphemeralModeHandle(CipherMode mode);
         string GetNCryptAlgorithmIdentifier();
         byte[] PreprocessKey(byte[] key);
-        int GetPaddingSize();
+        int GetPaddingSize(CipherMode mode, int feedbackSizeBits);
     }
 }
index 44553ea..7a19ca0 100644 (file)
@@ -92,6 +92,42 @@ namespace System.Security.Cryptography
             _core.GenerateIV();
         }
 
+        protected override bool TryDecryptEcbCore(
+            ReadOnlySpan<byte> ciphertext,
+            Span<byte> destination,
+            PaddingMode paddingMode,
+            out int bytesWritten)
+        {
+            UniversalCryptoTransform transform = _core.CreateCryptoTransform(
+                iv: null,
+                encrypting: false,
+                paddingMode,
+                CipherMode.ECB);
+
+            using (transform)
+            {
+                return transform.TransformOneShot(ciphertext, destination, out bytesWritten);
+            }
+        }
+
+        protected override bool TryEncryptEcbCore(
+            ReadOnlySpan<byte> plaintext,
+            Span<byte> destination,
+            PaddingMode paddingMode,
+            out int bytesWritten)
+        {
+            UniversalCryptoTransform transform = _core.CreateCryptoTransform(
+                iv: null,
+                encrypting: true,
+                paddingMode,
+                CipherMode.ECB);
+
+            using (transform)
+            {
+                return transform.TransformOneShot(plaintext, destination, out bytesWritten);
+            }
+        }
+
         protected override void Dispose(bool disposing)
         {
             base.Dispose(disposing);
@@ -105,16 +141,16 @@ namespace System.Security.Cryptography
             return false;
         }
 
-        int ICngSymmetricAlgorithm.GetPaddingSize()
+        int ICngSymmetricAlgorithm.GetPaddingSize(CipherMode mode, int feedbackSizeBits)
         {
-            return this.GetPaddingSize();
+            return this.GetPaddingSize(mode, feedbackSizeBits);
         }
 
-        SafeAlgorithmHandle ICngSymmetricAlgorithm.GetEphemeralModeHandle()
+        SafeAlgorithmHandle ICngSymmetricAlgorithm.GetEphemeralModeHandle(CipherMode mode)
         {
             try
             {
-                return AesBCryptModes.GetSharedHandle(Mode, FeedbackSize / 8);
+                return AesBCryptModes.GetSharedHandle(mode, FeedbackSize / 8);
             }
             catch (NotSupportedException)
             {
index 708acee..3687df1 100644 (file)
@@ -93,6 +93,42 @@ namespace System.Security.Cryptography
             _core.GenerateIV();
         }
 
+        protected override bool TryDecryptEcbCore(
+            ReadOnlySpan<byte> ciphertext,
+            Span<byte> destination,
+            PaddingMode paddingMode,
+            out int bytesWritten)
+        {
+            UniversalCryptoTransform transform = _core.CreateCryptoTransform(
+                iv: null,
+                encrypting: false,
+                paddingMode,
+                CipherMode.ECB);
+
+            using (transform)
+            {
+                return transform.TransformOneShot(ciphertext, destination, out bytesWritten);
+            }
+        }
+
+        protected override bool TryEncryptEcbCore(
+            ReadOnlySpan<byte> plaintext,
+            Span<byte> destination,
+            PaddingMode paddingMode,
+            out int bytesWritten)
+        {
+            UniversalCryptoTransform transform = _core.CreateCryptoTransform(
+                iv: null,
+                encrypting: true,
+                paddingMode,
+                CipherMode.ECB);
+
+            using (transform)
+            {
+                return transform.TransformOneShot(plaintext, destination, out bytesWritten);
+            }
+        }
+
         protected override void Dispose(bool disposing)
         {
             base.Dispose(disposing);
@@ -106,14 +142,14 @@ namespace System.Security.Cryptography
             return TripleDES.IsWeakKey(key);
         }
 
-        int ICngSymmetricAlgorithm.GetPaddingSize()
+        int ICngSymmetricAlgorithm.GetPaddingSize(CipherMode mode, int feedbackSizeBits)
         {
-            return this.GetPaddingSize();
+            return this.GetPaddingSize(mode, feedbackSizeBits);
         }
 
-        SafeAlgorithmHandle ICngSymmetricAlgorithm.GetEphemeralModeHandle()
+        SafeAlgorithmHandle ICngSymmetricAlgorithm.GetEphemeralModeHandle(CipherMode mode)
         {
-            return TripleDesBCryptModes.GetSharedHandle(Mode, FeedbackSize / 8);
+            return TripleDesBCryptModes.GetSharedHandle(mode, FeedbackSize / 8);
         }
 
         string ICngSymmetricAlgorithm.GetNCryptAlgorithmIdentifier()
index 981242f..dc6d484 100644 (file)
@@ -15,6 +15,10 @@ namespace System.Security.Cryptography.Cng.Tests
         [ConditionalTheory(nameof(SupportsPersistedSymmetricKeys))]
         // AES128-ECB-NoPadding 2 blocks.
         [InlineData(128, 2 * BlockSizeBytes, CipherMode.ECB, PaddingMode.None)]
+        // AES128-ECB-Zeros 2 blocks.
+        [InlineData(128, 2 * BlockSizeBytes, CipherMode.ECB, PaddingMode.Zeros)]
+        // AES128-ECB-Zeros 1.5 blocks.
+        [InlineData(128, BlockSizeBytes + BlockSizeBytes / 2, CipherMode.ECB, PaddingMode.Zeros)]
         // AES128-CBC-NoPadding at 2 blocks
         [InlineData(128, 2 * BlockSizeBytes, CipherMode.CBC, PaddingMode.None)]
         // AES256-CBC-Zeros at 1.5 blocks
index 2b71447..9f1a604 100644 (file)
@@ -100,6 +100,33 @@ namespace System.Security.Cryptography.Cng.Tests
 
                     Assert.Equal(expectedBytes, persistedDecrypted);
                 }
+
+                byte[] oneShotPersistedEncrypted = null;
+                byte[] oneShotEphemeralEncrypted = null;
+                byte[] oneShotPersistedDecrypted = null;
+
+                if (cipherMode == CipherMode.ECB)
+                {
+                    oneShotPersistedEncrypted = persisted.EncryptEcb(plainBytes, paddingMode);
+                    oneShotEphemeralEncrypted = ephemeral.EncryptEcb(plainBytes, paddingMode);
+                    oneShotPersistedDecrypted = persisted.DecryptEcb(oneShotEphemeralEncrypted, paddingMode);
+                }
+
+                if (oneShotPersistedEncrypted is not null)
+                {
+                    Assert.Equal(oneShotEphemeralEncrypted, oneShotPersistedEncrypted);
+
+                    if (paddingMode == PaddingMode.Zeros)
+                    {
+                        byte[] plainPadded = new byte[oneShotPersistedDecrypted.Length];
+                        plainBytes.AsSpan().CopyTo(plainPadded);
+                        Assert.Equal(plainPadded, oneShotPersistedDecrypted);
+                    }
+                    else
+                    {
+                        Assert.Equal(plainBytes, oneShotPersistedDecrypted);
+                    }
+                }
             }
         }
 
index 1e494d2..1761b8d 100644 (file)
              Link="CommonTest\AlgorithmImplementations\AES\AesContractTests.cs" />
     <Compile Include="$(CommonTestPath)System\Security\Cryptography\AlgorithmImplementations\TripleDES\TripleDESCipherTests.cs"
              Link="CommonTest\System\Security\Cryptography\AlgorithmImplementations\TripleDES\TripleDESCipherTests.cs" />
+    <Compile Include="$(CommonTestPath)System\Security\Cryptography\AlgorithmImplementations\TripleDES\TripleDESCipherTests.OneShot.cs"
+             Link="CommonTest\System\Security\Cryptography\AlgorithmImplementations\TripleDES\TripleDESCipherTests.OneShot.cs" />
     <Compile Include="$(CommonTestPath)System\Security\Cryptography\AlgorithmImplementations\AES\AesModeTests.cs"
              Link="CommonTest\AlgorithmImplementations\AES\AesModeTests.cs" />
     <Compile Condition="'$(TargetsWindows)' == 'true'" Include="$(CommonTestPath)System\Security\Cryptography\AlgorithmImplementations\TripleDES\TripleDESReusabilityTests.cs"
              Link="CommonTest\System\Security\Cryptography\AlgorithmImplementations\TripleDES\TripleDESReusabilityTests.cs" />
     <Compile Condition="'$(TargetsWindows)' == 'true'" Include="$(CommonTestPath)System\Security\Cryptography\AlgorithmImplementations\AES\AesCipherTests.cs"
              Link="CommonTest\AlgorithmImplementations\AES\AesCipherTests.cs" />
+    <Compile Condition="'$(TargetsWindows)' == 'true'" Include="$(CommonTestPath)System\Security\Cryptography\AlgorithmImplementations\AES\AesCipherTests.OneShot.cs"
+             Link="CommonTest\AlgorithmImplementations\AES\AesCipherTests.OneShot" />
     <Compile Include="ECDiffieHellmanCngTests.cs" />
     <Compile Include="$(CommonTestPath)System\Security\Cryptography\AlgorithmImplementations\ECDiffieHellman\ECDiffieHellmanTests.cs"
              Link="CommonTest\System\Security\Cryptography\AlgorithmImplementations\ECDiffieHellman\ECDiffieHellmanTests.cs" />
index 0786018..d68aa59 100644 (file)
@@ -29,6 +29,10 @@ namespace System.Security.Cryptography.Cng.Tests
         [ConditionalTheory(nameof(SupportsPersistedSymmetricKeys))]
         // 3DES192-ECB-NoPadding 2 blocks.
         [InlineData(2 * BlockSizeBytes, CipherMode.ECB, PaddingMode.None)]
+        // 3DES192-ECB-Zeros 2 blocks.
+        [InlineData(2 * BlockSizeBytes, CipherMode.ECB, PaddingMode.Zeros)]
+        // 3DES192-ECB-Zeros 1.5 blocks.
+        [InlineData(BlockSizeBytes + BlockSizeBytes / 2, CipherMode.ECB, PaddingMode.Zeros)]
         // 3DES192-CBC-NoPadding at 2 blocks
         [InlineData(2 * BlockSizeBytes, CipherMode.CBC, PaddingMode.None)]
         // 3DES192-CBC-Zeros at 1.5 blocks
index d2536fd..2bdd859 100644 (file)
@@ -95,7 +95,17 @@ namespace System.Security.Cryptography
                     throw new CryptographicException(SR.Cryptography_InvalidIVSize);
             }
 
-            BasicSymmetricCipher cipher = new BasicSymmetricCipherCsp(CapiHelper.CALG_DES, Mode, BlockSize / BitsPerByte, rgbKey, 0, false, rgbIV, encrypting, FeedbackSize, this.GetPaddingSize());
+            BasicSymmetricCipher cipher = new BasicSymmetricCipherCsp(
+                CapiHelper.CALG_DES,
+                Mode,
+                BlockSize / BitsPerByte,
+                rgbKey,
+                effectiveKeyLength: 0,
+                addNoSaltFlag: false,
+                rgbIV,
+                encrypting,
+                FeedbackSize,
+                this.GetPaddingSize(Mode, FeedbackSize));
             return UniversalCryptoTransform.Create(Padding, cipher, encrypting);
         }
     }
index b2019d7..00a71bd 100644 (file)
@@ -77,6 +77,9 @@ namespace System.Security.Cryptography.Csp.Tests
                 "TrySignDataCore",
                 "VerifyDataCore",
                 "VerifySignatureCore",
+                // CryptoServiceProviders will not get one-shot APIs as they are being deprecated
+                "TryEncryptEcbCore",
+                "TryDecryptEcbCore",
             };
 
             IEnumerable<MethodInfo> baseMethods = shimType.
index d363d1d..49510d6 100644 (file)
@@ -253,13 +253,23 @@ namespace System.Security.Cryptography
         public abstract System.Security.Cryptography.ICryptoTransform CreateDecryptor(byte[] rgbKey, byte[]? rgbIV);
         public virtual System.Security.Cryptography.ICryptoTransform CreateEncryptor() { throw null; }
         public abstract System.Security.Cryptography.ICryptoTransform CreateEncryptor(byte[] rgbKey, byte[]? rgbIV);
+        public byte[] DecryptEcb(byte[] ciphertext, System.Security.Cryptography.PaddingMode paddingMode) { throw null; }
+        public byte[] DecryptEcb(System.ReadOnlySpan<byte> ciphertext, System.Security.Cryptography.PaddingMode paddingMode) { throw null; }
+        public int DecryptEcb(System.ReadOnlySpan<byte> ciphertext, System.Span<byte> destination, System.Security.Cryptography.PaddingMode paddingMode) { throw null; }
         public void Dispose() { }
         protected virtual void Dispose(bool disposing) { }
+        public byte[] EncryptEcb(byte[] plaintext, System.Security.Cryptography.PaddingMode paddingMode) { throw null; }
+        public byte[] EncryptEcb(System.ReadOnlySpan<byte> plaintext, System.Security.Cryptography.PaddingMode paddingMode) { throw null; }
+        public int EncryptEcb(System.ReadOnlySpan<byte> plaintext, System.Span<byte> destination, System.Security.Cryptography.PaddingMode paddingMode) { throw null; }
         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 TryDecryptEcb(System.ReadOnlySpan<byte> ciphertext, System.Span<byte> destination, System.Security.Cryptography.PaddingMode paddingMode, out int bytesWritten) { throw null; }
+        protected virtual bool TryDecryptEcbCore(System.ReadOnlySpan<byte> ciphertext, System.Span<byte> destination, System.Security.Cryptography.PaddingMode paddingMode, out int bytesWritten) { throw null; }
+        public bool TryEncryptEcb(System.ReadOnlySpan<byte> plaintext, System.Span<byte> destination, System.Security.Cryptography.PaddingMode paddingMode, out int bytesWritten) { throw null; }
+        protected virtual bool TryEncryptEcbCore(System.ReadOnlySpan<byte> plaintext, System.Span<byte> destination, System.Security.Cryptography.PaddingMode paddingMode, out int bytesWritten) { throw null; }
         public bool ValidKeySize(int bitLength) { throw null; }
     }
 }
index 75639b4..250a6ec 100644 (file)
@@ -60,6 +60,9 @@
   <data name="Arg_CryptographyException" xml:space="preserve">
     <value>Error occurred during a cryptographic operation.</value>
   </data>
+  <data name="Argument_DestinationTooShort" xml:space="preserve">
+    <value>Destination is too short.</value>
+  </data>
   <data name="Argument_InvalidOffLen" xml:space="preserve">
     <value>Offset and length were out of bounds for the array or count is greater than the number of elements from index to the end of the source collection.</value>
   </data>
   <data name="Cryptography_PlaintextTooLarge" xml:space="preserve">
     <value>The specified plaintext size is too large.</value>
   </data>
+  <data name="Cryptography_EncryptedIncorrectLength" xml:space="preserve">
+    <value>{0} unexpectedly produced a ciphertext with the incorrect length.</value>
+  </data>
   <data name="NotSupported_SubclassOverride" xml:space="preserve">
     <value>Method not supported. Derived class must override.</value>
   </data>
index c140062..a79a220 100644 (file)
@@ -428,6 +428,303 @@ namespace System.Security.Cryptography
             }
         }
 
+        /// <summary>
+        ///   Decrypts data using ECB mode with the specified padding mode.
+        /// </summary>
+        /// <param name="ciphertext">The data to decrypt.</param>
+        /// <param name="paddingMode">The padding mode used to produce the ciphertext and remove during decryption.</param>
+        /// <returns>The decrypted plaintext data.</returns>
+        /// <exception cref="ArgumentNullException">
+        ///   <paramref name="ciphertext" /> is <see langword="null" />.
+        /// </exception>
+        /// <exception cref="ArgumentOutOfRangeException">
+        ///   <paramref name="paddingMode" /> is not a valid padding mode.
+        /// </exception>
+        /// <exception cref="CryptographicException">
+        ///   The ciphertext could not be decrypted successfully.
+        /// </exception>
+        /// <remarks>
+        ///   This method's behavior is defined by <see cref="TryDecryptEcbCore" />.
+        /// </remarks>
+        public byte[] DecryptEcb(byte[] ciphertext, PaddingMode paddingMode)
+        {
+            // Padding mode is validated by callee.
+            if (ciphertext is null)
+                throw new ArgumentNullException(nameof(ciphertext));
+
+            return DecryptEcb(new ReadOnlySpan<byte>(ciphertext), paddingMode);
+        }
+
+        /// <summary>
+        ///   Decrypts data using ECB mode with the specified padding mode.
+        /// </summary>
+        /// <param name="ciphertext">The data to decrypt.</param>
+        /// <param name="paddingMode">The padding mode used to produce the ciphertext and remove during decryption.</param>
+        /// <returns>The decrypted plaintext data.</returns>
+        /// <exception cref="ArgumentOutOfRangeException">
+        ///   <paramref name="paddingMode" /> is not a valid padding mode.
+        /// </exception>
+        /// <exception cref="CryptographicException">
+        ///   The ciphertext could not be decrypted successfully.
+        /// </exception>
+        /// <remarks>
+        ///   This method's behavior is defined by <see cref="TryDecryptEcbCore" />.
+        /// </remarks>
+        public byte[] DecryptEcb(ReadOnlySpan<byte> ciphertext, PaddingMode paddingMode)
+        {
+            CheckPaddingMode(paddingMode);
+
+            // This could get returned directly to the caller if we there was no padding
+            // that needed to get removed, so don't rent from a pool.
+            byte[] decryptBuffer = GC.AllocateUninitializedArray<byte>(ciphertext.Length);
+
+            if (!TryDecryptEcbCore(ciphertext, decryptBuffer, paddingMode, out int written)
+                || (uint)written > decryptBuffer.Length)
+            {
+                // This means decrypting the ciphertext grew in to a larger plaintext or overflowed.
+                // A user-derived class could do this, but it is not expected in any of the
+                // implementations that we ship.
+
+                throw new CryptographicException(SR.Argument_DestinationTooShort);
+            }
+
+            // Array.Resize will no-op if the array does not need to be resized.
+            Array.Resize(ref decryptBuffer, written);
+            return decryptBuffer;
+        }
+
+        /// <summary>
+        ///   Decrypts data into the specified buffer, using ECB mode with the specified padding mode.
+        /// </summary>
+        /// <param name="ciphertext">The data to decrypt.</param>
+        /// <param name="destination">The buffer to receive the plaintext data.</param>
+        /// <param name="paddingMode">The padding mode used to produce the ciphertext and remove during decryption.</param>
+        /// <returns>The total number of bytes written to <paramref name="destination" /></returns>
+        /// <exception cref="ArgumentOutOfRangeException">
+        ///   <paramref name="paddingMode" /> is not a valid padding mode.
+        /// </exception>
+        /// <exception cref="CryptographicException">
+        ///   The ciphertext could not be decrypted successfully.
+        /// </exception>
+        /// <exception cref="ArgumentException">
+        ///   The buffer in <paramref name="destination"/> is too small to hold the plaintext data.
+        /// </exception>
+        /// <remarks>
+        ///   This method's behavior is defined by <see cref="TryDecryptEcbCore" />.
+        /// </remarks>
+        public int DecryptEcb(ReadOnlySpan<byte> ciphertext, Span<byte> destination, PaddingMode paddingMode)
+        {
+            CheckPaddingMode(paddingMode);
+
+            if (!TryDecryptEcbCore(ciphertext, destination, paddingMode, out int written))
+            {
+                throw new ArgumentException(SR.Argument_DestinationTooShort, nameof(destination));
+            }
+
+            return written;
+        }
+
+        /// <summary>
+        ///   Attempts to decrypt data into the specified buffer, using ECB mode with the specified padding mode.
+        /// </summary>
+        /// <param name="ciphertext">The data to decrypt.</param>
+        /// <param name="destination">The buffer to receive the plaintext data.</param>
+        /// <param name="paddingMode">The padding mode used to produce the ciphertext and remove during decryption.</param>
+        /// <param name="bytesWritten">When this method returns, the total number of bytes written to <paramref name="destination" />.</param>
+        /// <returns><see langword="true"/> if <paramref name="destination"/> was large enough to receive the decrypted data; otherwise, <see langword="false" />.</returns>
+        /// <exception cref="ArgumentOutOfRangeException">
+        ///   <paramref name="paddingMode" /> is not a valid padding mode.
+        /// </exception>
+        /// <exception cref="CryptographicException">
+        ///   The ciphertext could not be decrypted successfully.
+        /// </exception>
+        /// <remarks>
+        ///   This method's behavior is defined by <see cref="TryDecryptEcbCore" />.
+        /// </remarks>
+        public bool TryDecryptEcb(ReadOnlySpan<byte> ciphertext, Span<byte> destination, PaddingMode paddingMode, out int bytesWritten)
+        {
+            CheckPaddingMode(paddingMode);
+            return TryDecryptEcbCore(ciphertext, destination, paddingMode, out bytesWritten);
+        }
+
+        /// <summary>
+        ///   Encrypts data using ECB mode with the specified padding mode.
+        /// </summary>
+        /// <param name="plaintext">The data to encrypt.</param>
+        /// <param name="paddingMode">The padding mode used to produce the ciphertext and remove during decryption.</param>
+        /// <returns>The encrypted ciphertext data.</returns>
+        /// <exception cref="ArgumentNullException">
+        ///   <paramref name="plaintext" /> is <see langword="null" />.
+        /// </exception>
+        /// <exception cref="ArgumentOutOfRangeException">
+        ///   <paramref name="paddingMode" /> is not a valid padding mode.
+        /// </exception>
+        /// <exception cref="CryptographicException">
+        ///   <see cref="TryEncryptEcbCore" /> could not encrypt the plaintext.
+        /// </exception>
+        /// <remarks>
+        ///   This method's behavior is defined by <see cref="TryEncryptEcbCore" />.
+        /// </remarks>
+        public byte[] EncryptEcb(byte[] plaintext, PaddingMode paddingMode)
+        {
+            // paddingMode is validated by callee
+            if (plaintext is null)
+                throw new ArgumentNullException(nameof(plaintext));
+
+            return EncryptEcb(new ReadOnlySpan<byte>(plaintext), paddingMode);
+        }
+
+        /// <summary>
+        ///   Encrypts data using ECB mode with the specified padding mode.
+        /// </summary>
+        /// <param name="plaintext">The data to encrypt.</param>
+        /// <param name="paddingMode">The padding mode used to produce the ciphertext and remove during decryption.</param>
+        /// <returns>The encrypted ciphertext data.</returns>
+        /// <exception cref="ArgumentOutOfRangeException">
+        ///   <paramref name="paddingMode" /> is not a valid padding mode.
+        /// </exception>
+        /// <exception cref="CryptographicException">
+        ///   The plaintext could not be encrypted successfully.
+        /// </exception>
+        /// <remarks>
+        ///   This method's behavior is defined by <see cref="TryEncryptEcbCore" />.
+        /// </remarks>
+        public byte[] EncryptEcb(ReadOnlySpan<byte> plaintext, PaddingMode paddingMode)
+        {
+            CheckPaddingMode(paddingMode);
+
+            int ciphertextLength = GetCiphertextLengthEcb(plaintext.Length, paddingMode);
+
+            // We expect most if not all uses to encrypt to exactly the ciphertextLength
+            byte[] buffer = GC.AllocateUninitializedArray<byte>(ciphertextLength);
+
+            if (!TryEncryptEcbCore(plaintext, buffer, paddingMode, out int written) ||
+                written != ciphertextLength)
+            {
+                // This means a user-derived imiplementation added more padding than we expected or
+                // did something non-standard (encrypt to a partial block). This can't happen for
+                // multiple padding blocks since the buffer would have been too small in the first
+                // place. It doesn't make sense to try and support partial block encryption, likely
+                // something went very wrong. So throw.
+                throw new CryptographicException(SR.Format(SR.Cryptography_EncryptedIncorrectLength, nameof(TryEncryptEcbCore)));
+            }
+
+            return buffer;
+        }
+
+        /// <summary>
+        ///   Encrypts data into the specified buffer, using ECB mode with the specified padding mode.
+        /// </summary>
+        /// <param name="plaintext">The data to encrypt.</param>
+        /// <param name="destination">The buffer to receive the ciphertext data.</param>
+        /// <param name="paddingMode">The padding mode used to produce the ciphertext and remove during decryption.</param>
+        /// <returns>The total number of bytes written to <paramref name="destination" />.</returns>
+        /// <exception cref="ArgumentOutOfRangeException">
+        ///   <paramref name="paddingMode" /> is not a valid padding mode.
+        /// </exception>
+        /// <exception cref="CryptographicException">
+        ///   The plaintext could not be encrypted successfully.
+        /// </exception>
+        /// <exception cref="ArgumentException">
+        ///   The buffer in <paramref name="destination"/> is too small to hold the ciphertext data.
+        /// </exception>
+        /// <remarks>
+        ///   This method's behavior is defined by <see cref="TryEncryptEcbCore" />.
+        /// </remarks>
+        public int EncryptEcb(ReadOnlySpan<byte> plaintext, Span<byte> destination, PaddingMode paddingMode)
+        {
+            CheckPaddingMode(paddingMode);
+
+            if (!TryEncryptEcbCore(plaintext, destination, paddingMode, out int written))
+            {
+                throw new ArgumentException(SR.Argument_DestinationTooShort, nameof(destination));
+            }
+
+            return written;
+        }
+
+        /// <summary>
+        ///   Attempts to encrypt data into the specified buffer, using ECB mode with the specified padding mode.
+        /// </summary>
+        /// <param name="plaintext">The data to encrypt.</param>
+        /// <param name="destination">The buffer to receive the ciphertext data.</param>
+        /// <param name="paddingMode">The padding mode used to produce the ciphertext and remove during decryption.</param>
+        /// <param name="bytesWritten">When this method returns, the total number of bytes written to <paramref name="destination" />.</param>
+        /// <returns><see langword="true"/> if <paramref name="destination"/> was large enough to receive the encrypted data; otherwise, <see langword="false" />.</returns>
+        /// <exception cref="ArgumentOutOfRangeException">
+        ///   <paramref name="paddingMode" /> is not a valid padding mode.
+        /// </exception>
+        /// <exception cref="CryptographicException">
+        ///   The plaintext could not be encrypted successfully.
+        /// </exception>
+        /// <remarks>
+        ///   This method's behavior is defined by <see cref="TryEncryptEcbCore" />.
+        /// </remarks>
+        public bool TryEncryptEcb(ReadOnlySpan<byte> plaintext, Span<byte> destination, PaddingMode paddingMode, out int bytesWritten)
+        {
+            CheckPaddingMode(paddingMode);
+            return TryEncryptEcbCore(plaintext, destination, paddingMode, out bytesWritten);
+        }
+
+        /// <summary>
+        ///   When overridden in a derived class, attempts to encrypt data into the specified
+        ///   buffer, using ECB mode with the specified padding mode.
+        /// </summary>
+        /// <param name="plaintext">The data to encrypt.</param>
+        /// <param name="destination">The buffer to receive the ciphertext data.</param>
+        /// <param name="paddingMode">The padding mode used to produce the ciphertext and remove during decryption.</param>
+        /// <param name="bytesWritten">When this method returns, the total number of bytes written to <paramref name="destination" />.</param>
+        /// <returns><see langword="true"/> if <paramref name="destination"/> was large enough to receive the encrypted data; otherwise, <see langword="false" />.</returns>
+        /// <exception cref="NotSupportedException">
+        ///   A derived class has not provided an implementation.
+        /// </exception>
+        /// <remarks>
+        ///   <para>Derived classes must override this and provide an implementation.</para>
+        ///   <para>
+        ///      Implementations of this method must write precisely
+        ///      <c>GetCiphertextLengthEcb(plaintext.Length, paddingMode)</c> bytes to <paramref name="destination"/>
+        ///      and report that via <paramref name="bytesWritten"/>.
+        ///   </para>
+        /// </remarks>
+        protected virtual bool TryEncryptEcbCore(
+            ReadOnlySpan<byte> plaintext,
+            Span<byte> destination,
+            PaddingMode paddingMode,
+            out int bytesWritten)
+        {
+            throw new NotSupportedException(SR.NotSupported_SubclassOverride);
+        }
+
+        /// <summary>
+        ///   When overridden in a derived class, attempts to decrypt data
+        ///   into the specified buffer, using ECB mode with the specified padding mode.
+        /// </summary>
+        /// <param name="ciphertext">The data to decrypt.</param>
+        /// <param name="destination">The buffer to receive the plaintext data.</param>
+        /// <param name="paddingMode">The padding mode used to produce the ciphertext and remove during decryption.</param>
+        /// <param name="bytesWritten">When this method returns, the total number of bytes written to <paramref name="destination" />.</param>
+        /// <returns><see langword="true"/> if <paramref name="destination"/> was large enough to receive the decrypted data; otherwise, <see langword="false" />.</returns>
+        /// <exception cref="NotSupportedException">
+        ///   A derived class has not provided an implementation.
+        /// </exception>
+        /// <remarks>
+        ///   Derived classes must override this and provide an implementation.
+        /// </remarks>
+        protected virtual bool TryDecryptEcbCore(
+            ReadOnlySpan<byte> ciphertext,
+            Span<byte> destination,
+            PaddingMode paddingMode,
+            out int bytesWritten)
+        {
+            throw new NotSupportedException(SR.NotSupported_SubclassOverride);
+        }
+
+        private static void CheckPaddingMode(PaddingMode paddingMode)
+        {
+            if (paddingMode < PaddingMode.None || paddingMode > PaddingMode.ISO10126)
+                throw new ArgumentOutOfRangeException(nameof(paddingMode), SR.Cryptography_InvalidPaddingMode);
+        }
+
         protected CipherMode ModeValue;
         protected PaddingMode PaddingValue;
         protected byte[]? KeyValue;
index 8edfa2b..65fa6db 100644 (file)
@@ -133,6 +133,138 @@ namespace System.Security.Cryptography.Primitives.Tests
                 alg.GetCiphertextLengthCfb(17, PaddingMode.None, feedbackSizeInBits: 128));
         }
 
+        [Fact]
+        public static void EncryptEcb_NotSupportedInDerived()
+        {
+            AnySizeAlgorithm alg = new AnySizeAlgorithm { BlockSize = 128 };
+
+            Assert.Throws<NotSupportedException>(() =>
+                alg.EncryptEcb(Array.Empty<byte>(), PaddingMode.None));
+        }
+
+        [Fact]
+        public static void DecryptEcb_NotSupportedInDerived()
+        {
+            AnySizeAlgorithm alg = new AnySizeAlgorithm { BlockSize = 128 };
+
+            Assert.Throws<NotSupportedException>(() =>
+                alg.DecryptEcb(Array.Empty<byte>(), PaddingMode.None));
+        }
+
+        [Fact]
+        public static void EncryptEcb_EncryptProducesIncorrectlyPaddedValue()
+        {
+            static bool EncryptImpl(ReadOnlySpan<byte> ciphertext, Span<byte> destination, PaddingMode paddingMode, out int bytesWritten)
+            {
+                bytesWritten = destination.Length + 1;
+                return true;
+            }
+
+            EcbSymmetricAlgorithm alg = new EcbSymmetricAlgorithm
+            {
+                BlockSize = 128,
+                TryEncryptEcbCoreImpl = EncryptImpl,
+            };
+
+            Assert.Throws<CryptographicException>(() =>
+                alg.EncryptEcb(Array.Empty<byte>(), PaddingMode.None));
+        }
+
+        [Fact]
+        public static void DecryptEcb_DecryptBytesWrittenLies()
+        {
+            static bool DecryptImpl(ReadOnlySpan<byte> ciphertext, Span<byte> destination, PaddingMode paddingMode, out int bytesWritten)
+            {
+                bytesWritten = destination.Length + 1;
+                return true;
+            }
+
+            EcbSymmetricAlgorithm alg = new EcbSymmetricAlgorithm
+            {
+                BlockSize = 128,
+                TryDecryptEcbCoreImpl = DecryptImpl,
+            };
+
+            Assert.Throws<CryptographicException>(() =>
+                alg.DecryptEcb(new byte[128 / 8], PaddingMode.None));
+        }
+
+        [Fact]
+        public static void EncryptEcb_EncryptCoreFails()
+        {
+            static bool EncryptImpl(ReadOnlySpan<byte> ciphertext, Span<byte> destination, PaddingMode paddingMode, out int bytesWritten)
+            {
+                bytesWritten = 0;
+                return false;
+            }
+
+            EcbSymmetricAlgorithm alg = new EcbSymmetricAlgorithm
+            {
+                BlockSize = 128,
+                TryEncryptEcbCoreImpl = EncryptImpl,
+            };
+
+            Assert.Throws<CryptographicException>(() =>
+                alg.EncryptEcb(Array.Empty<byte>(), PaddingMode.None));
+        }
+
+        [Fact]
+        public static void EncryptEcb_EncryptCoreOverflowWritten()
+        {
+            static bool EncryptImpl(ReadOnlySpan<byte> ciphertext, Span<byte> destination, PaddingMode paddingMode, out int bytesWritten)
+            {
+                bytesWritten = -1;
+                return true;
+            }
+
+            EcbSymmetricAlgorithm alg = new EcbSymmetricAlgorithm
+            {
+                BlockSize = 128,
+                TryEncryptEcbCoreImpl = EncryptImpl,
+            };
+
+            Assert.Throws<CryptographicException>(() =>
+                alg.EncryptEcb(Array.Empty<byte>(), PaddingMode.None));
+        }
+
+        [Fact]
+        public static void DecryptEcb_DecryptCoreFails()
+        {
+            static bool DecryptImpl(ReadOnlySpan<byte> plaintext, Span<byte> destination, PaddingMode paddingMode, out int bytesWritten)
+            {
+                bytesWritten = 0;
+                return false;
+            }
+
+            EcbSymmetricAlgorithm alg = new EcbSymmetricAlgorithm
+            {
+                BlockSize = 128,
+                TryDecryptEcbCoreImpl = DecryptImpl,
+            };
+
+            Assert.Throws<CryptographicException>(() =>
+                alg.DecryptEcb(Array.Empty<byte>(), PaddingMode.None));
+        }
+
+        [Fact]
+        public static void DecryptEcb_DecryptCoreOverflowWritten()
+        {
+            static bool DecryptImpl(ReadOnlySpan<byte> plaintext, Span<byte> destination, PaddingMode paddingMode, out int bytesWritten)
+            {
+                bytesWritten = -1;
+                return true;
+            }
+
+            EcbSymmetricAlgorithm alg = new EcbSymmetricAlgorithm
+            {
+                BlockSize = 128,
+                TryDecryptEcbCoreImpl = DecryptImpl,
+            };
+
+            Assert.Throws<CryptographicException>(() =>
+                alg.DecryptEcb(Array.Empty<byte>(), PaddingMode.None));
+        }
+
         public static IEnumerable<object[]> CiphertextLengthTheories
         {
             get
@@ -231,5 +363,35 @@ namespace System.Security.Cryptography.Primitives.Tests
             public override void GenerateIV() => throw new NotImplementedException();
             public override void GenerateKey() => throw new NotImplementedException();
         }
+
+        private class EcbSymmetricAlgorithm : AnySizeAlgorithm
+        {
+            public delegate bool TryEncryptEcbCoreFunc(
+                ReadOnlySpan<byte> plaintext,
+                Span<byte> destination,
+                PaddingMode paddingMode,
+                out int bytesWritten);
+
+            public delegate bool TryDecryptEcbCoreFunc(
+                ReadOnlySpan<byte> ciphertext,
+                Span<byte> destination,
+                PaddingMode paddingMode,
+                out int bytesWritten);
+
+            public TryEncryptEcbCoreFunc TryEncryptEcbCoreImpl { get; set; }
+            public TryDecryptEcbCoreFunc TryDecryptEcbCoreImpl { get; set; }
+
+            protected override bool TryEncryptEcbCore(
+                ReadOnlySpan<byte> plaintext,
+                Span<byte> destination,
+                PaddingMode paddingMode,
+                out int bytesWritten) => TryEncryptEcbCoreImpl(plaintext, destination, paddingMode, out bytesWritten);
+
+            protected override bool TryDecryptEcbCore(
+                ReadOnlySpan<byte> ciphertext,
+                Span<byte> destination,
+                PaddingMode paddingMode,
+                out int bytesWritten) => TryDecryptEcbCoreImpl(ciphertext, destination, paddingMode, out bytesWritten);
+        }
     }
 }