dotnet / runtime

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

System.Security.Cryptography.AesCryptoServiceProvider produces different results depending on the runtime version #71694

Closed JKamsker closed 2 years ago

JKamsker commented 2 years ago

Description

A program running in .net48 will produce a different output compared to running in net6.

Reproduction Steps

Program.cs:

using System;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;

namespace DotnetTestDecrypt
{
    internal class Program
    {
        static void Main(string[] args)
        {
            var (key, iv) = ("a8131038eaa84f1b9b494a3a0d1838ce", "e7e60f3dbd5c4ea98e7411662fcc3b58");
            var source = @"UEL7bGGkvPN0obYF6PEu-HwPPPbi61690VPpC73Zv-FTITKaoSlCiGdyI7ol5C-MBFCJD7zp7prHX5wS6AZ-PHBbF5KcNiTs1ho7B6FP-Wgvvntny3XQJfqTIh7u9-9gg2nN8MxHCrSaogMbVD-oPxKN9uXj2oNpjKOrtxB-6eYrqARdyXbfWfWHanD3-81jEcfBHoCKsWypGuRGH-Rd1QtPHdHe90enNKzTPw-OlIBSA3ztQmfRIhAOQDY-UIb675aoXADz3MH8g7wR-BncoZCVX7lE2FFoGpnj8-MUcLwx64zXtHxMgn9AGE-EKFWhmdXSBnaSz1jTkqk-n2wIH8xUPxMuNYKUbmBx-5NOow6sLr0er0H2GlckI-X0JrGGOawjysjdorN";
            var encrypted = Encrypt(source, key, iv);

            var dec = Decrypt(encrypted, key, iv);
            var ok = dec == source;
            Console.WriteLine($"Ok: {ok}");
        }

        private static string Decrypt(byte[] content, string key, string iv)
        {
            byte[] keyBytes = Encoding.ASCII.GetBytes(key);
            byte[] ivBytes = Encoding.UTF8.GetBytes(iv);
            byte[] decrypted = Decrypt(content, keyBytes, ivBytes);
            return Encoding.UTF8.GetString(decrypted).Trim(new char[1]);
        }

        private static byte[] Decrypt(byte[] textBytes, byte[] key, byte[] iv)
        {
            byte[] result;
            using (AesCryptoServiceProvider provider = new AesCryptoServiceProvider())
            {
                provider.Key = key.Take(provider.KeySize / 8).ToArray<byte>();
                provider.IV = iv.Take(provider.BlockSize / 8).ToArray<byte>();
                provider.Mode = CipherMode.CBC;
                provider.Padding = PaddingMode.PKCS7;
                using (MemoryStream ms = new MemoryStream(textBytes))
                {
                    using (ICryptoTransform decryptor = provider.CreateDecryptor(provider.Key, provider.IV))
                    {
                        using (CryptoStream cs = new CryptoStream(ms, decryptor, CryptoStreamMode.Read))
                        {
                            byte[] decrypted = new byte[textBytes.Length];
                            cs.Read(decrypted, 0, textBytes.Length);
                            result = decrypted;
                        }
                    }
                }
            }
            return result;
        }

        private static byte[] Encrypt(string content, string key, string iv)
        {
            byte[] keyBytes = Encoding.UTF8.GetBytes(key);
            byte[] ivBytes = Encoding.UTF8.GetBytes(iv);
            return Encrypt(Encoding.UTF8.GetBytes(content), keyBytes, ivBytes);
        }

        private static byte[] Encrypt(byte[] textBytes, byte[] key, byte[] iv)
        {
            byte[] result;
            using (AesCryptoServiceProvider provider = new AesCryptoServiceProvider())
            {
                provider.Key = key.Take(provider.KeySize / 8).ToArray<byte>();
                provider.IV = iv.Take(provider.BlockSize / 8).ToArray<byte>();
                provider.Mode = CipherMode.CBC;
                provider.Padding = PaddingMode.PKCS7;
                using (ICryptoTransform encryptor = provider.CreateEncryptor(provider.Key, provider.IV))
                {
                    using (MemoryStream ms = new MemoryStream())
                    {
                        using (CryptoStream cs = new CryptoStream(ms, encryptor, CryptoStreamMode.Write))
                        {
                            cs.Write(textBytes, 0, textBytes.Length);
                            cs.FlushFinalBlock();
                            result = ms.ToArray();
                        }
                    }
                }
            }
            return result;
        }
    }
}

csproj:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net48</TargetFramework>
    <!--<TargetFramework>net60</TargetFramework>-->
  </PropertyGroup>

</Project>

Switch between net48 and net60 and the program will output either "Ok: True" or "Ok: False"

Expected behavior

Both runtimes producing the exact same result.

Actual behavior

While net48 produces the correct value, the value from net6 will be missing exactly 12 characters ("HGXxizO3cOtI")

Regression?

No response

Known Workarounds

Use net4.8

or use a MemoryStream + cs.CopyTo in the Decryption method.

Configuration

No response

Other information

No response

ghost commented 2 years 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
### Description A program running in .net48 will produce a different output compared to running in net6. ### Reproduction Steps Program.cs: ```csharp using System; using System.IO; using System.Linq; using System.Security.Cryptography; using System.Text; namespace DotnetTestDecrypt { internal class Program { static void Main(string[] args) { var (key, iv) = ("a8131038eaa84f1b9b494a3a0d1838ce", "e7e60f3dbd5c4ea98e7411662fcc3b58"); var source = @"UEL7bGGkvPN0obYF6PEu-HwPPPbi61690VPpC73Zv-FTITKaoSlCiGdyI7ol5C-MBFCJD7zp7prHX5wS6AZ-PHBbF5KcNiTs1ho7B6FP-Wgvvntny3XQJfqTIh7u9-9gg2nN8MxHCrSaogMbVD-oPxKN9uXj2oNpjKOrtxB-6eYrqARdyXbfWfWHanD3-81jEcfBHoCKsWypGuRGH-Rd1QtPHdHe90enNKzTPw-OlIBSA3ztQmfRIhAOQDY-UIb675aoXADz3MH8g7wR-BncoZCVX7lE2FFoGpnj8-MUcLwx64zXtHxMgn9AGE-EKFWhmdXSBnaSz1jTkqk-n2wIH8xUPxMuNYKUbmBx-5NOow6sLr0er0H2GlckI-X0JrGGOawjysjdorN"; var encrypted = Encrypt(source, key, iv); var dec = Decrypt(encrypted, key, iv); var ok = dec == source; Console.WriteLine($"Ok: {ok}"); } private static string Decrypt(byte[] content, string key, string iv) { byte[] keyBytes = Encoding.ASCII.GetBytes(key); byte[] ivBytes = Encoding.UTF8.GetBytes(iv); byte[] decrypted = Decrypt(content, keyBytes, ivBytes); return Encoding.UTF8.GetString(decrypted).Trim(new char[1]); } private static byte[] Decrypt(byte[] textBytes, byte[] key, byte[] iv) { byte[] result; using (AesCryptoServiceProvider provider = new AesCryptoServiceProvider()) { provider.Key = key.Take(provider.KeySize / 8).ToArray(); provider.IV = iv.Take(provider.BlockSize / 8).ToArray(); provider.Mode = CipherMode.CBC; provider.Padding = PaddingMode.PKCS7; using (MemoryStream ms = new MemoryStream(textBytes)) { using (ICryptoTransform decryptor = provider.CreateDecryptor(provider.Key, provider.IV)) { using (CryptoStream cs = new CryptoStream(ms, decryptor, CryptoStreamMode.Read)) { byte[] decrypted = new byte[textBytes.Length]; cs.Read(decrypted, 0, textBytes.Length); result = decrypted; } } } } return result; } private static byte[] Encrypt(string content, string key, string iv) { byte[] keyBytes = Encoding.UTF8.GetBytes(key); byte[] ivBytes = Encoding.UTF8.GetBytes(iv); return Encrypt(Encoding.UTF8.GetBytes(content), keyBytes, ivBytes); } private static byte[] Encrypt(byte[] textBytes, byte[] key, byte[] iv) { byte[] result; using (AesCryptoServiceProvider provider = new AesCryptoServiceProvider()) { provider.Key = key.Take(provider.KeySize / 8).ToArray(); provider.IV = iv.Take(provider.BlockSize / 8).ToArray(); provider.Mode = CipherMode.CBC; provider.Padding = PaddingMode.PKCS7; using (ICryptoTransform encryptor = provider.CreateEncryptor(provider.Key, provider.IV)) { using (MemoryStream ms = new MemoryStream()) { using (CryptoStream cs = new CryptoStream(ms, encryptor, CryptoStreamMode.Write)) { cs.Write(textBytes, 0, textBytes.Length); cs.FlushFinalBlock(); result = ms.ToArray(); } } } } return result; } } } ``` csproj: ```xml Exe net48 ``` Switch between net48 and net60 and the program will output either "Ok: True" or "Ok: False" ### Expected behavior Both runtimes producing the exact same result. ### Actual behavior While net48 produces the correct value, the value from net6 will be missing exactly 12 characters ("HGXxizO3cOtI") ### Regression? _No response_ ### Known Workarounds Use net4.8 ### Configuration _No response_ ### Other information _No response_
Author: JKamsker
Assignees: -
Labels: `area-System.Security`
Milestone: -
vcsjones commented 2 years ago

This is the documented change in https://docs.microsoft.com/en-us/dotnet/core/compatibility/core-libraries/6.0/partial-byte-reads-in-streams.

byte[] decrypted = new byte[textBytes.Length];
cs.Read(decrypted, 0, textBytes.Length);
result = decrypted;

As documented in the article, Read is not guaranteed to fill the entire buffer, so Read should be called in a loop until it indicates there is no more content. The article includes sample code on how to do so.

Alternatively, for .NET 6, you can use the new [EncryptCbc](https://docs.microsoft.com/en-us/dotnet/api/system.security.cryptography.symmetricalgorithm.encryptcbc?view=net-6.0#system-security-cryptography-symmetricalgorithm-encryptcbc(system-byte()-system-byte()-system-security-cryptography-paddingmode)) and DecryptCbc one shots to avoid using CryptoStream entirely.

Closing as a duplicate of https://github.com/dotnet/runtime/issues/55527.