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

CipherMode.CFB does not Encrypt the same in .NET 5.0 RC1 as in .NET 4.7 #43234

Closed Roneri75 closed 4 years ago

Roneri75 commented 4 years ago

When using CipherMode.CFB as Mode in the System.Security.Cryptography.TripleDES to Encrypt e.g. a password you don't get the same encryption string back in .NET 5.0 RC1 as you get in .NET 4.7. We have used code like this since .net 4.5 and it's worked exactly the same since, i should work exactly the same in .NET 5.0 as in 4.x or all apps using this for e.g. saving password encrypted will stop working after upgrade to .NET 5.

I did a test console app to reproduce this, it's the exact the same code for both .NET 5.0 RC1 as for .NET 4.7. Both apps where created in VS 2019 Preview (16..8.0 Preview 3.2). And they do not generate the same result.

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

namespace TestConsole.net5
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello, Typ in you text to be Encrypted!");
            string text = Console.ReadLine();
            TripleDESHelper tripleDESHelper = new TripleDESHelper();
            string encryptedtext = tripleDESHelper.Encrypt(text);
            Console.WriteLine("Your encrypted text results in this: " + encryptedtext);
            Console.ReadLine();
        }
    }
    public class TripleDESHelper
    {

        private readonly byte[] KEY_BYTES = { 2, 24, 1, 114, 15, 74, 23, 43, 81, 23, 87, 34, 55, 72, 32, 34 };
        private readonly byte[] IV_BYTES = { 90, 12, 54, 27, 62, 3, 45, 23 };

        private TripleDES tripleDES;

        public TripleDESHelper()
        {
            tripleDES = TripleDESCryptoServiceProvider.Create();
            tripleDES.Key = KEY_BYTES;
            tripleDES.IV = IV_BYTES;

            // Used for small chunck of data. In this one, we use it for user's password.
            tripleDES.Mode = CipherMode.CFB;
        }

        /// <summary>
        /// Encrypts the given plain text string into an encrypted, base 64 encoded value; using the Triple Data Encryption Standard algorithms
        /// </summary>
        /// <param name="clearText">The plain text.</param>
        /// <returns>A string representing the encrypted text.</returns>
        public string Encrypt(string clearText)
        {
            if (tripleDES == null || String.IsNullOrEmpty(clearText))
                return String.Empty;

            String encryptedText = String.Empty;

            ICryptoTransform encryptor = tripleDES.CreateEncryptor();

            byte[] textBytes = Encoding.UTF8.GetBytes(clearText);

            encryptedText = System.Convert.ToBase64String(encryptor.TransformFinalBlock(textBytes, 0, textBytes.Length));

            return encryptedText;
        }

        /// <summary>
        /// Decrypts the given cipher text string into an plain text using the Triple Data Encryption Standard algorithms
        /// </summary>
        /// <param name="cipherText">The cipher text.</param>
        /// <returns>A string representing the plain text</returns>
        public string Decrypt(string cipherText)
        {
            if (tripleDES == null || String.IsNullOrEmpty(cipherText))
                return String.Empty;

            String decryptedText = String.Empty;

            byte[] encryptedBytes = System.Convert.FromBase64String(cipherText);

            ICryptoTransform decryptor = tripleDES.CreateDecryptor();

            decryptedText = Encoding.UTF8.GetString(decryptor.TransformFinalBlock(encryptedBytes, 0, encryptedBytes.Length));

            return decryptedText;

        }
    }
}
ghost commented 4 years ago

Tagging subscribers to this area: @bartonjs, @vcsjones, @krwq, @jeffhandley See info in area-owners.md if you want to be subscribed.

vcsjones commented 4 years ago

It looks like there are two things:

  1. net5.0 defaults to a FeedbackSize of 64, and net48 defaults to a feedback size of 8.
  2. net5.0 applies padding a little differently than net48.

If you explicitly set the FeedbackSize, then the results are the same, minus the padding differences.

vcsjones commented 4 years ago

@bartonjs assuming you want something fixed here, how likely is it that it could make it in to RTM?

bartonjs commented 4 years ago

Ah, I see. TripleDES sets FeedBackSize to the block size, but TripleDESCryptoServiceProvider sets it to 8. So the data would be inconsistent (CFB8 vs CFB64) in the .NET Framework application if someone registered to CryptoConfig that TripleDESCng should be the default 3DES provider. The app would also work if it used new TripleDESCryptoServiceProvider(). (It looks like it's creating a TripleDESCryptoServiceProvider directly, but there's no TripleDESCryptoServiceProvider.Create method, it's falling back to TripleDES.Create(), and thus getting whatever CryptoConfig says the right answer is for 3DES)

I think that this is just an unfortunate circumstance of the type hierarchy for the crypto types here. The only change to consider would be making the TripleDESImplementation class default to FeedbackSize = 8 so that TripleDES.Create().FeedbackSize produced the same value across a clean .NET Framework installation execution and a .NET 5 execution.

@danmosemsft How would you feel about taking a change to 5.0 that changed the default value of TripleDES.Create().FeedbackSize to match .NET Framework? It pretty much only affects .NET 5, since it only impacts CFB mode, which we didn't bring back to Core until 5. So it's a "no existing .NET Core applications should be depending on it, but it makes .NET 5 slightly more compatible with .NET Framework than it otherwise would have been" change. I think it's a reasonable concession to compatibility; but I'm way happier changing it for 5 than for 6, so I don't think we would change it in master unless it was already likely to get approved; because then we're deciding if we wanted to be more compatible with .NET 5 or .NET Framework.

vcsjones commented 4 years ago

@bartonjs another issue is padding. It looks like for CFB8, we are on padding to the feedback size (so 1 byte of padding is added). In .NET Framework we are padding to the whole block (nearest 8 for 3DES).

I don't think that will impact padding removal and the data will interop as long as normal padding removal is used.

bartonjs commented 4 years ago

Yeah, I guess my willingness to take the FeedbackSize default change is predicated on it meaning that a value encrypted in .NET Framework can be decrypted in .NET 5, and vice-versa (even though they compute different padding).

vcsjones commented 4 years ago

I'll work on this over the weekend and you can decide if you want to port to release/5.0.

bartonjs commented 4 years ago

Thanks, @vcsjones. I so owe you a cup of coffee for the help you've given while I've been juggling multiple projects.

danmoseley commented 4 years ago

@bartonjs so if I understand right

If so this sounds like worth taking to tactics for approval, but note @vcsjones we won't know their decision before Monday, in case that influences whether you want to spend time on it 😃

bartonjs commented 4 years ago

@danmosemsft Yeah, that's an accurate summary.

@vcsjones My psychic diagnostic powers believe the whole change should be to add

public TripleDESImplementation()
{
    // Default CFB to CFB8 to match .NET Framework's default for TripleDES.Create()
    FeedbackSizeValue = 8;
}

around hereish

https://github.com/dotnet/runtime/blob/d984ab77062e2aee681f31170db9ec809d29bd6f/src/libraries/System.Security.Cryptography.Algorithms/src/Internal/Cryptography/TripleDesImplementation.cs#L12-L14

and a "set the minimum number of properties and make sure we can read data from netfx, even though we produce different data in the 'ignore me' bytes" test.

Hopefully yours agrees. I'm eager to find out how close my predictive powers were to reality.

danmoseley commented 4 years ago

I will guess 80% chance they take it..

vcsjones commented 4 years ago

I so owe you a cup of coffee

Sold!

vcsjones commented 4 years ago

@bartonjs You were right about the feedback size. Padding remains an interesting case.

The .NET Framework expects the ciphertext to be padded to the block size, even with CFB8. Otherwise it will blow up. As the way things are today, .NET 5 pads CFB8 data in a way that .NET Framework cannot de-pad (unless you get lucky).

This will work in .NET 5 and fail in .NET Framework:

byte[] ciphertext = Convert.FromBase64String("Vx8GlhTi");

using var tripleDes = TripleDES.Create();
tripleDes.FeedbackSize = 8;
tripleDes.Mode = CipherMode.CFB;
tripleDes.Key = Convert.FromBase64String("aaHRNTLQh++TK3lhShiicCPLQ6IO0YVH");
tripleDes.IV = Convert.FromBase64String("OQHTwYDPlMo=");

using var transform = tripleDes.CreateDecryptor();
Console.WriteLine(Encoding.UTF8.GetString(transform.TransformFinalBlock(ciphertext, 0, ciphertext.Length)));

The reverse is not a problem: we can de-pad .NET Framework encrypted data in .NET 5.

vcsjones commented 4 years ago

@bartonjs The padding size thing is a bit thorny if we want to start encrypting with full blocks of padding. We might break something for someone going from RC to RTM.

Here are the options, as I figure.

  1. We do nothing. This means that CFB8 encrypted data from .NET 5 will not decrypt in .NET Framework if padding is being used.
  2. We go back to .NET Framework behavior. This will break anything encrypted with CFB8 and padding from RC going to RTM.
  3. We try some kind of hybrid approach.

    1. Allow CFB8 decryption in .NET 5 to support arbitrary padding lengths, or "1" or "block size" padding lengths.
    2. Change CFB8 encryption in .NET 5 to always pad to a full block.

    This will permit .NET 5 ciphertexts to be read by .NET Framework, and will continue to allow decrypting 1-octet padding sizes as well. This raises the question, "what does this mean for PaddingMode.None?". This is also perhaps a large change for this late in the .NET 5 cycle.

@danmosemsft there are two potential .NET Framework compatibility breaks here. The first one that was originally discussed is fairly simple and aligns with your initial assessment. That one I still think is worth taking and discussing with "tactics" on Monday. That is fixed in #43259 and that is the only thing in that PR.

bartonjs commented 4 years ago

The reverse is not a problem: we can de-pad .NET Framework encrypted data in .NET 5.

Then I think at this point we stick with "We do nothing." (for padding, vs feedback size). Upgrading succeeds (old data can be read), but side-by-side is hampered (since old programs can't read new program data). Callers who need the padding to be there can do their encrypts with no padding and manually append the padding data. It's not great, but the workaround is required in the newer application, so it's at least not something that requires a time machine.

danmoseley commented 4 years ago

cc @jeffhandley as it's 5.0.

danmoseley commented 4 years ago

https://github.com/dotnet/docs/issues/21103