Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
Roneri75 opened this issue Oct 9, 2020 · 17 comments · Fixed by #43259
Closed

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

Roneri75 opened this issue Oct 9, 2020 · 17 comments · Fixed by #43259
Labels
area-System.Security breaking-change Issue or PR that represents a breaking API or functional change over a prerelease. tenet-compatibility Incompatibility with previous versions or .NET Framework
Milestone

Comments

@Roneri75
Copy link

Roneri75 commented Oct 9, 2020

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;

        }
    }
}
@bartonjs bartonjs transferred this issue from dotnet/core Oct 9, 2020
@Dotnet-GitSync-Bot Dotnet-GitSync-Bot added area-System.Security untriaged New issue has not been triaged by the area owner labels Oct 9, 2020
@ghost
Copy link

ghost commented Oct 9, 2020

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

@vcsjones
Copy link
Member

vcsjones commented Oct 9, 2020

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
Copy link
Member

vcsjones commented Oct 9, 2020

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

@bartonjs
Copy link
Member

bartonjs commented Oct 9, 2020

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
Copy link
Member

vcsjones commented Oct 9, 2020

@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
Copy link
Member

bartonjs commented Oct 9, 2020

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
Copy link
Member

vcsjones commented Oct 9, 2020

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

@bartonjs
Copy link
Member

bartonjs commented Oct 9, 2020

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

@danmoseley
Copy link
Member

@bartonjs so if I understand right

  • one value is much as good as the other, ie., we don't lose anything
  • it would make us meaningfully more (ie., code didn't prevoiusly work) compatible with .NET Framework
  • it would not break any customers coming from 3.1 because the mode didn't exist then
  • we have only one customer report but it's also the case that we haven't actually shipped yet
  • you believe the risk of the change is low or very low?

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 😃

@danmoseley danmoseley added this to the 5.0.0 milestone Oct 9, 2020
@danmoseley danmoseley added the tenet-compatibility Incompatibility with previous versions or .NET Framework label Oct 9, 2020
@bartonjs
Copy link
Member

bartonjs commented Oct 9, 2020

@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

private const int BitsPerByte = 8;
public override ICryptoTransform CreateDecryptor()

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.

@bartonjs bartonjs removed the untriaged New issue has not been triaged by the area owner label Oct 9, 2020
@danmoseley
Copy link
Member

I will guess 80% chance they take it..

@vcsjones
Copy link
Member

vcsjones commented Oct 9, 2020

I so owe you a cup of coffee

Sold!

@vcsjones
Copy link
Member

vcsjones commented Oct 10, 2020

@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
Copy link
Member

vcsjones commented Oct 10, 2020

@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
Copy link
Member

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
Copy link
Member

cc @jeffhandley as it's 5.0.

@danmoseley danmoseley added breaking-change Issue or PR that represents a breaking API or functional change over a prerelease. needs-breaking-change-doc-created Breaking changes need an issue opened with https://github.com/dotnet/docs/issues/new?template=dotnet labels Oct 13, 2020
@danmoseley
Copy link
Member

dotnet/docs#21103

@danmoseley danmoseley removed the needs-breaking-change-doc-created Breaking changes need an issue opened with https://github.com/dotnet/docs/issues/new?template=dotnet label Oct 19, 2020
@ghost ghost locked as resolved and limited conversation to collaborators Dec 7, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-System.Security breaking-change Issue or PR that represents a breaking API or functional change over a prerelease. tenet-compatibility Incompatibility with previous versions or .NET Framework
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants