dotnet / runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.
https://docs.microsoft.com/dotnet/core/
MIT License
15.34k stars 4.74k forks source link

Cryptographic error "Padding is invalid and cannot be removed" after upgrading from .NET Core 3.2 to .NET 7.0.3 - hosted on IIS 10.0.17763.1 - (works on dev machine) #83381

Closed tinonetic closed 1 year ago

tinonetic commented 1 year ago

I have seen the following related issues and can't seem resolve mine given the solutions:

  1. https://github.com/dotnet/runtime/issues/76389
  2. https://github.com/dotnet/runtime/issues/46672

Background

  1. I upgraded a production application from .NET Core 3.2 to .NET 7.0.3
  2. It is published on a server running IIS 10.0.17763.1, Windows Server 2019, running latest hosting bundle
  3. This issue does not happen when tested on a developer machine running Visual Studio 2022 Version 17.4.5 on Window 10 Pro v 21H2

Old/Expected behaviour

  1. Encrypting and decrypting data symmetrically
  2. Worked both on developer machine and IIS Server when it was running .NET Core 3.2
  3. Encryted without error
  4. Decrypted without error

New/Current behaviour

  1. Encrypts without error on developer machine
  2. Decrypts without error on developer machine
  3. Encrypts without error on IIS
  4. Decrypts with following error on IIS:
System.Security.Cryptography.CryptographicException: Padding is invalid and cannot be removed.
   at System.Security.Cryptography.SymmetricPadding.GetPaddingLength(ReadOnlySpan`1 block, PaddingMode paddingMode, Int32 blockSize)
   at System.Security.Cryptography.UniversalCryptoDecryptor.UncheckedTransformFinalBlock(ReadOnlySpan`1 inputBuffer, Span`1 outputBuffer)
   at System.Security.Cryptography.UniversalCryptoDecryptor.UncheckedTransformFinalBlock(Byte[] inputBuffer, Int32 inputOffset, Int32 inputCount)
   at System.Security.Cryptography.UniversalCryptoTransform.TransformFinalBlock(Byte[] inputBuffer, Int32 inputOffset, Int32 inputCount)
   at System.Security.Cryptography.CryptoStream.ReadAsyncCore(Memory`1 buffer, CancellationToken cancellationToken, Boolean useAsync)
   at System.Security.Cryptography.CryptoStream.Read(Byte[] buffer, Int32 offset, Int32 count)

The failing code

        public virtual string Encrypt(string plainText, string passPhrase = null, byte[] salt = null)
        {
            if (plainText == null)
            {
                return null;
            }

            if (passPhrase == null)
            {
                passPhrase = Options.DefaultPassPhrase;
            }

            if (salt == null)
            {
                salt = Options.DefaultSalt;
            }

            var plainTextBytes = Encoding.UTF8.GetBytes(plainText);
            using (var password = new Rfc2898DeriveBytes(passPhrase, salt))
            {
                var keyBytes = password.GetBytes(Options.Keysize / 8);
                using (var symmetricKey = Aes.Create())
                {
                    symmetricKey.Mode = CipherMode.CBC;
                    using (var encryptor = symmetricKey.CreateEncryptor(keyBytes, Options.InitVectorBytes))
                    {
                        using (var memoryStream = new MemoryStream())
                        {
                            using (var cryptoStream = new CryptoStream(memoryStream, encryptor, CryptoStreamMode.Write))
                            {
                                cryptoStream.Write(plainTextBytes, 0, plainTextBytes.Length);
                                cryptoStream.FlushFinalBlock();
                                var cipherTextBytes = memoryStream.ToArray();
                                return Convert.ToBase64String(cipherTextBytes);
                            }
                        }
                    }
                }
            }
        }

        public virtual string Decrypt(string cipherText, string passPhrase = null, byte[] salt = null)
        {
            if (string.IsNullOrEmpty(cipherText))
            {
                return null;
            }

            if (passPhrase == null)
            {
                passPhrase = Options.DefaultPassPhrase;
            }

            if (salt == null)
            {
                salt = Options.DefaultSalt;
            }

            var cipherTextBytes = Convert.FromBase64String(cipherText);
            using (var password = new Rfc2898DeriveBytes(passPhrase, salt))
            {
                var keyBytes = password.GetBytes(Options.Keysize / 8);
                using (var symmetricKey = Aes.Create())
                {
                    symmetricKey.Mode = CipherMode.CBC;
                    using (var decryptor = symmetricKey.CreateDecryptor(keyBytes, Options.InitVectorBytes))
                    {
                        using (var memoryStream = new MemoryStream(cipherTextBytes))
                        {
                            using (var cryptoStream = new CryptoStream(memoryStream, decryptor, CryptoStreamMode.Read))
                            {
                                var plainTextBytes = new byte[cipherTextBytes.Length];
                                var totalReadCount = 0;
                                while (totalReadCount < cipherTextBytes.Length)
                                {
                                    var buffer = new byte[cipherTextBytes.Length];
                                    var readCount = cryptoStream.Read(buffer, 0, buffer.Length);
                                    if (readCount == 0)
                                    {
                                        break;
                                    }

                                    for (var i = 0; i < readCount; i++)
                                    {
                                        plainTextBytes[i + totalReadCount] = buffer[i];
                                    }

                                    totalReadCount += readCount;
                                }

                                return Encoding.UTF8.GetString(plainTextBytes, 0, totalReadCount);
                            }
                        }
                    }
                }
            }
        }
ghost commented 1 year ago

Tagging subscribers to this area: @dotnet/area-system-security, @vcsjones See info in area-owners.md if you want to be subscribed.

Issue Details
I have seen the following related issues and can't seem resolve mine given the solutions: 1. https://github.com/dotnet/runtime/issues/76389 2. https://github.com/dotnet/runtime/issues/46672 **Background** 1. I upgraded a production application from .NET Core 3.2 to .NET 7.0.3 3. It is published on a server running **IIS 10.0.17763.1,** **Windows Server 2019**, running[ latest hosting bundle](https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-aspnetcore-7.0.3-windows-x64-installer) 4. This issue **does not happen** when tested on a developer machine running **Visual Studio 2022 Version 17.4.5** on **Window 10 Pro v 21H2** **Old/Expected behaviour** 1. Encrypting and decrypting data symmetrically 2. Worked both on developer machine and IIS Server when it was running .NET Core 3.2 3. Encryted without error 5. Decrypted without error **New/Current behaviour** 1. Encrypts without error on developer machine 2. Decrypts without error on developer machine 3. Encrypts without error on IIS 4. Decrypts with following error on IIS: ``` System.Security.Cryptography.CryptographicException: Padding is invalid and cannot be removed. at System.Security.Cryptography.SymmetricPadding.GetPaddingLength(ReadOnlySpan`1 block, PaddingMode paddingMode, Int32 blockSize) at System.Security.Cryptography.UniversalCryptoDecryptor.UncheckedTransformFinalBlock(ReadOnlySpan`1 inputBuffer, Span`1 outputBuffer) at System.Security.Cryptography.UniversalCryptoDecryptor.UncheckedTransformFinalBlock(Byte[] inputBuffer, Int32 inputOffset, Int32 inputCount) at System.Security.Cryptography.UniversalCryptoTransform.TransformFinalBlock(Byte[] inputBuffer, Int32 inputOffset, Int32 inputCount) at System.Security.Cryptography.CryptoStream.ReadAsyncCore(Memory`1 buffer, CancellationToken cancellationToken, Boolean useAsync) at System.Security.Cryptography.CryptoStream.Read(Byte[] buffer, Int32 offset, Int32 count) at Volo.Abp.Security.Encryption.StringEncryptionService.Decrypt(String cipherText, String passPhrase, Byte[] salt) ``` **The failing code** ``` c# public virtual string Encrypt(string plainText, string passPhrase = null, byte[] salt = null) { if (plainText == null) { return null; } if (passPhrase == null) { passPhrase = Options.DefaultPassPhrase; } if (salt == null) { salt = Options.DefaultSalt; } var plainTextBytes = Encoding.UTF8.GetBytes(plainText); using (var password = new Rfc2898DeriveBytes(passPhrase, salt)) { var keyBytes = password.GetBytes(Options.Keysize / 8); using (var symmetricKey = Aes.Create()) { symmetricKey.Mode = CipherMode.CBC; using (var encryptor = symmetricKey.CreateEncryptor(keyBytes, Options.InitVectorBytes)) { using (var memoryStream = new MemoryStream()) { using (var cryptoStream = new CryptoStream(memoryStream, encryptor, CryptoStreamMode.Write)) { cryptoStream.Write(plainTextBytes, 0, plainTextBytes.Length); cryptoStream.FlushFinalBlock(); var cipherTextBytes = memoryStream.ToArray(); return Convert.ToBase64String(cipherTextBytes); } } } } } } public virtual string Decrypt(string cipherText, string passPhrase = null, byte[] salt = null) { if (string.IsNullOrEmpty(cipherText)) { return null; } if (passPhrase == null) { passPhrase = Options.DefaultPassPhrase; } if (salt == null) { salt = Options.DefaultSalt; } var cipherTextBytes = Convert.FromBase64String(cipherText); using (var password = new Rfc2898DeriveBytes(passPhrase, salt)) { var keyBytes = password.GetBytes(Options.Keysize / 8); using (var symmetricKey = Aes.Create()) { symmetricKey.Mode = CipherMode.CBC; using (var decryptor = symmetricKey.CreateDecryptor(keyBytes, Options.InitVectorBytes)) { using (var memoryStream = new MemoryStream(cipherTextBytes)) { using (var cryptoStream = new CryptoStream(memoryStream, decryptor, CryptoStreamMode.Read)) { var plainTextBytes = new byte[cipherTextBytes.Length]; var totalReadCount = 0; while (totalReadCount < cipherTextBytes.Length) { var buffer = new byte[cipherTextBytes.Length]; var readCount = cryptoStream.Read(buffer, 0, buffer.Length); if (readCount == 0) { break; } for (var i = 0; i < readCount; i++) { plainTextBytes[i + totalReadCount] = buffer[i]; } totalReadCount += readCount; } return Encoding.UTF8.GetString(plainTextBytes, 0, totalReadCount); } } } } } } ```
Author: tinonetic
Assignees: -
Labels: `area-System.Security`, `untriaged`
Milestone: -
vcsjones commented 1 year ago

Hello, thanks for taking the time to file an issue.

Using your code, I can't reproduce it. The exact code I used to do so is below.

Can you please create a runnable example that demonstrates the issue? In particular, I had to fill in some gaps with the Options for default sizes, etc. It would be helpful to know the lengths of the Options used.

using System.Security.Cryptography;
using System.Text;

var Options = new {
    Keysize = 128,
    InitVectorBytes = new byte[16],
    DefaultPassPhrase = "test",
    DefaultSalt = new byte[16]
};

var encrypt = Encrypt("potato", "hotdog");
var decrypt = Decrypt(encrypt, "hotdog");

Console.WriteLine(decrypt);

string Encrypt(string plainText, string passPhrase = null, byte[] salt = null)
{
    if (plainText == null)
    {
        return null;
    }

    if (passPhrase == null)
    {
        passPhrase = Options.DefaultPassPhrase;
    }

    if (salt == null)
    {
        salt = Options.DefaultSalt;
    }

    var plainTextBytes = Encoding.UTF8.GetBytes(plainText);
    using (var password = new Rfc2898DeriveBytes(passPhrase, salt))
    {
        var keyBytes = password.GetBytes(Options.Keysize / 8);
        using (var symmetricKey = Aes.Create())
        {
            symmetricKey.Mode = CipherMode.CBC;
            using (var encryptor = symmetricKey.CreateEncryptor(keyBytes, Options.InitVectorBytes))
            {
                using (var memoryStream = new MemoryStream())
                {
                    using (var cryptoStream = new CryptoStream(memoryStream, encryptor, CryptoStreamMode.Write))
                    {
                        cryptoStream.Write(plainTextBytes, 0, plainTextBytes.Length);
                        cryptoStream.FlushFinalBlock();
                        var cipherTextBytes = memoryStream.ToArray();
                        return Convert.ToBase64String(cipherTextBytes);
                    }
                }
            }
        }
    }
}

string Decrypt(string cipherText, string passPhrase = null, byte[] salt = null)
{
    if (string.IsNullOrEmpty(cipherText))
    {
        return null;
    }

    if (passPhrase == null)
    {
        passPhrase = Options.DefaultPassPhrase;
    }

    if (salt == null)
    {
        salt = Options.DefaultSalt;
    }

    var cipherTextBytes = Convert.FromBase64String(cipherText);
    using (var password = new Rfc2898DeriveBytes(passPhrase, salt))
    {
        var keyBytes = password.GetBytes(Options.Keysize / 8);
        using (var symmetricKey = Aes.Create())
        {
            symmetricKey.Mode = CipherMode.CBC;
            using (var decryptor = symmetricKey.CreateDecryptor(keyBytes, Options.InitVectorBytes))
            {
                using (var memoryStream = new MemoryStream(cipherTextBytes))
                {
                    using (var cryptoStream = new CryptoStream(memoryStream, decryptor, CryptoStreamMode.Read))
                    {
                        var plainTextBytes = new byte[cipherTextBytes.Length];
                        var totalReadCount = 0;
                        while (totalReadCount < cipherTextBytes.Length)
                        {
                            var buffer = new byte[cipherTextBytes.Length];
                            var readCount = cryptoStream.Read(buffer, 0, buffer.Length);
                            if (readCount == 0)
                            {
                                break;
                            }

                            for (var i = 0; i < readCount; i++)
                            {
                                plainTextBytes[i + totalReadCount] = buffer[i];
                            }

                            totalReadCount += readCount;
                        }

                        return Encoding.UTF8.GetString(plainTextBytes, 0, totalReadCount);
                    }
                }
            }
        }
    }
}
ghost commented 1 year ago

This issue has been marked needs-author-action and may be missing some important information.

ghost commented 1 year ago

This issue has been automatically marked no-recent-activity because it has not had any activity for 14 days. It will be closed if no further activity occurs within 14 more days. Any new comment (by anyone, not necessarily the author) will remove no-recent-activity.

ghost commented 1 year ago

This issue will now be closed since it had been marked no-recent-activity but received no further activity in the past 14 days. It is still possible to reopen or comment on the issue, but please note that the issue will be locked if it remains inactive for another 30 days.