2
\$\begingroup\$

My co-worker and I have been tasked with encrypting and decrypting text data using AES256. However both of us feel quite uncomfortable with this as this sits squarely outside our normal problem solving domain.

So first, the code we've come up with:

public class AesManagedTextEncryptionService
{
    public byte[] GenerateKey()
    {
        using (var rng = new RNGCryptoServiceProvider())
        {
            var key = new byte[32];
            rng.GetBytes(key);

            return key;
        }
    }

    public string Encrypt(byte[] key, string plainText)
    {
        using (var rng = new RNGCryptoServiceProvider())
        {
            //Generate random IV
            var iv = new byte[16];
            rng.GetBytes(iv);

            //Use CBC and PKCS7 Padding
            using (var aesAlg = new AesManaged {Key = key, IV = iv, Mode = CipherMode.CBC, Padding = PaddingMode.PKCS7})
            {
                using (var encryptor = aesAlg.CreateEncryptor(aesAlg.Key, aesAlg.IV))
                {
                    using (var msEncrypt = new MemoryStream())
                    {
                        using (var csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write))
                        {
                            using (var swEncrypt = new StreamWriter(csEncrypt, Encoding.UTF8))
                            {
                                swEncrypt.Write(plainText);
                            }

                            //The final "format" is that we prefix te01:: to indicate that this library generated this data, and 01 as a version.
                            //Then we prefix the 16 IV bytes onto the encrypted payload and base 64 encode this.
                            var encrypted = msEncrypt.ToArray();
                            var finalData = iv.Concat(encrypted).ToArray();

                            return $"te01::{Convert.ToBase64String(finalData)}";
                        }
                    }
                }
            }
        }
    }

    public string Decrypt(byte[] key, string encryptedBase64)
    {
        if (!IsCompatible(encryptedBase64))
        {
            throw new Exception("Unsupported encryption format");
        }

        var trimmed = encryptedBase64.Substring(6);
        var bytes = Convert.FromBase64String(trimmed);

        //Obtain the IV from the first 16 bytes
        var iv = bytes.Take(16).ToArray();

        //The remaining bytes in the array is the encrypted payload
        var encryptedData = bytes.Skip(16).ToArray();

        //Use provided key and the embedded IV to decrypt
        using (var aes = new AesManaged { Key = key, IV = iv, Mode = CipherMode.CBC, Padding = PaddingMode.PKCS7 })
        {
            using (var decryptor = aes.CreateDecryptor(key, iv))
            {
                using (var ms = new MemoryStream(encryptedData))
                {
                    using (var cs = new CryptoStream(ms, decryptor, CryptoStreamMode.Read))
                    {
                        using (var reader = new StreamReader(cs, Encoding.UTF8))
                        {
                            var plainText = reader.ReadToEnd();
                            return plainText;
                        }
                    }
                }
            }
        }
    }

    public bool IsCompatible(string encrypted)
    {
        return encrypted.StartsWith("te01::");
    }
}

The way this is meant to be used:

  1. we generate a key that is used to encrypt a bunch of text data
    1.1) that is shipped off to a bunch of third party systems
  2. this key is then encrypted again using a master key in our possession
    2.1) there is a master key generated for each batch (or logical grouping) of data the 3rd processes
  3. we destroy that particular master key whenever that data has lived it's life time
    3.1) Just so we can sort of "guarantee" that we'll never decrypt that data again

The 3rd party systems can the provide us with their encrypted version of the key, and the encrypted payload and we can decrypt the parts and see what's going on.

We store a hash of the data we provided the 3rd party on our end to ensure that data is as we gave it to them.

Now the reason we're doing this, is simply to be sure that any of the metadata we provide to the 3rd parties isn't immediately at risk in case they have an intrusion.

We're currently using .NET Framework 4.7.2.

Now, as always when researching something as complicated as encryption and how to use it, always raises a tons of questions. It seems that CBC is considered an unsafe cipher mode, at least in some use cases. I however don't feel like I'm qualified to know if this is one of them or not?

We could actually also go with .NET Core 3 for this, meaning we can use GCM which seems to be recommended in many cases, as long as you ensure that the IV is never reused(?).

I hope someone can chime in with some feedback. We'd like to not start using something to later find out we missed something obvious or trivial - plus it seems like a great way to learn something new in the process!

\$\endgroup\$
1
\$\begingroup\$

It is a minor thing but I think it's worth mentioning. You can combine several using blocks into a single one, which will reduce the nesting (indentation) level.

So in case of .NET Framework 4.8 you can do this:

public string Encrypt(byte[] key, string plainText)
{
    using (var rng = new RNGCryptoServiceProvider())
    {
        //Generate random IV
        var iv = new byte[16];
        rng.GetBytes(iv);

        //Use CBC and PKCS7 Padding
        using (var aesAlg = new AesManaged { Key = key, IV = iv, Mode = CipherMode.CBC, Padding = PaddingMode.PKCS7 })
        using (var encryptor = aesAlg.CreateEncryptor(aesAlg.Key, aesAlg.IV))
        using (var msEncrypt = new MemoryStream())
        using (var csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write))
        {
            using (var swEncrypt = new StreamWriter(csEncrypt, Encoding.UTF8))
                swEncrypt.Write(plainText);

            //The final "format" is that we prefix te01:: to indicate that this library generated this data, and 01 as a version.
            //Then we prefix the 16 IV bytes onto the encrypted payload and base 64 encode this.
            var encrypted = msEncrypt.ToArray();
            var finalData = iv.Concat(encrypted).ToArray();

            return $"te01::{Convert.ToBase64String(finalData)}";
        }
    }
}

In case of .NET Core 3, where C# 8 is available, you can take advantage of using statements, which can further streamline your code:

public string Encrypt(byte[] key, string plainText)
{
    using var rng = new RNGCryptoServiceProvider();
    //Generate random IV
    var iv = new byte[16];
    rng.GetBytes(iv);

    //Use CBC and PKCS7 Padding
    using var aesAlg = new AesManaged { Key = key, IV = iv, Mode = CipherMode.CBC, Padding = PaddingMode.PKCS7 };
    using var encryptor = aesAlg.CreateEncryptor(aesAlg.Key, aesAlg.IV);
    using var msEncrypt = new MemoryStream();
    using var csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write);
    using (var swEncrypt = new StreamWriter(csEncrypt, Encoding.UTF8))
        swEncrypt.Write(plainText);

    //The final "format" is that we prefix te01:: to indicate that this library generated this data, and 01 as a version.
    //Then we prefix the 16 IV bytes onto the encrypted payload and base 64 encode this.
    var encrypted = msEncrypt.ToArray();
    var finalData = iv.Concat(encrypted).ToArray();

    return $"te01::{Convert.ToBase64String(finalData)}";
}

If memory usage is a concern for you then I would recommend to consider:

| improve this answer | |
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.