How to implement symmetric encryption in a .NET app using AES

Many options are available when it comes to encrypting sensitive data via a .NET application. However, while the System.Security.Cryptography namespace provides a plethora of different encryption algorithms, many of these are obsolete and should be avoided if possible. It is therefore important to keep abreast of the latest recommendations, especially when it comes to security-critical encryption code.

Symmetric encryption represents a type of encryption whereby the same key that is used to encrypt data must also be used to decrypt the data. This article will look at the latest Microsoft recommendations relating to symmetric encryption algorithms using AES (Advanced Encryption Standard) and the relevant .NET APIs that should be used. We’ll also cover a practical example of using AES encryption within a .NET application to encrypt and decrypt text.

Symmetric algorithms

The Microsoft documentation for the System.Security.Cryptography namespace lists many different encryption and hashing algorithms. Among the symmetric encryption types, you will find the following.

When browsing the documentation you will find warnings that the older DES and TripleDES types should only be for compatibility with legacy applications. Additionally, you will see that the Rijndael and RijndaelManaged types have been marked as obsolete, in favour of the Aes class.

Even the AesManaged class is now deprecated, if you try to use it in your codebase you will be greeted with the following compiler warning.

‘AesManaged’ is obsolete: ‘Derived cryptographic types are obsolete. Use the Create method on the base type instead.’

In other words, if you need to use symmetric encryption in your application, the current official advice is to use the static Aes.Create method to create a cryptographic object that can be used to perform the encryption.

If you look at the documentation page for the Aes.Create method you will also notice a prominent cautionary message warning that only the parameterless Create method should be used, not the overloads that allow specific AES algorithm implementations to be specified by name.

Note that although the Rijndael and RijndaelManaged types are obsolete, it is still the Rijndael algorithm that is used under the hood when the Aes.Create method is called.

In the following section, we’ll look at the implementation of a class that abstracts away some of the encryption complexities, providing the caller with a simplified public interface for encrypting and decrypting strings of text.

AES encryption example

In this section, we’re going to dive straight into some code and see how the Aes class along with other types in the System.Security.Cryptography namespace can be used to encrypt and decrypt text.

The full source code for a class named EncryptionServices class has been included below for reference and can also be found in the accompanying GitHub repository.

using JC.Samples.SymmetricEncryption.Services.Interfaces;
using System.Security.Cryptography;
using System.Text;
 
namespace JC.Samples.SymmetricEncryption.Services;
 
/// <summary>
/// Provides Encryption services.
/// </summary>
public class EncryptionServices : IEncryptionServices
{
    #region Constants
 
    /// <summary>
    /// The prefix text added to the start of encrypted data, 
    /// to help identify that the data is encrypted.
    /// </summary>
    private const string EncryptedValuePrefix = "EncryptedValue:";
 
    #endregion
 
    #region Methods
 
    #region Public
 
    /// <summary>
    /// Decrypts the specified text.
    /// </summary>
    /// <param name="text">The text to decrypt</param>
    /// <param name="key">The encryption key</param>
    /// <returns>The decrypted text</returns>
    public string DecryptString(string text, byte[] key)
    {
        if (string.IsNullOrWhiteSpace(text) || !IsEncrypted(text))
        {
            // There is no need to decrypt null/empty or unencrypted text.
            return text;
        }
 
        // Parse the vector from the encrypted data.
        byte[] vector = Convert.FromBase64String(text.Split(';')[0].Split(':')[1]);
 
        // Decrypt and return the plain text.
        return Decrypt(Convert.FromBase64String(text.Split(';')[1]), key, vector);
    }
 
    /// <summary>
    /// Encrypts the specified text.
    /// </summary>
    /// <param name="text">The text to encrypt</param>
    /// <param name="key">The encryption key</param>
    /// <returns>The encrypted text</returns>
    public string EncryptString(string text, byte[] key)
    {
        if (string.IsNullOrWhiteSpace(text) || IsEncrypted(text))
        {
            // There is no need to encrypt null/empty or already encrypted text.
            return text;
        }
 
        // Create a new random vector.
        byte[] vector = GenerateInitializationVector();
 
        // Encrypt the text.
        string encryptedText = Convert.ToBase64String(Encrypt(text, key, vector));
 
        // Format and return the encrypted data.
        return EncryptedValuePrefix + Convert.ToBase64String(vector) + ";" + encryptedText;
    }
 
    /// <summary>
    /// Determines if a specified text is encrypted.
    /// </summary>
    /// <param name="text">The text to check</param>
    /// <returns>True if the text is encrypted, otherwise false</returns>
    public bool IsEncrypted(string text) => 
        text.StartsWith(EncryptedValuePrefix, StringComparison.OrdinalIgnoreCase);
 
    #endregion
 
    #region Private
 
    /// <summary>
    /// Decrypts the specified byte array to plain text.
    /// </summary>
    /// <param name="encryptedBytes">The encrypted byte array</param>
    /// <param name="key">The encryption key</param>
    /// <param name="vector">The initialization vector</param>
    /// <returns>The decrypted text as a string</returns>
    private string Decrypt(byte[] encryptedBytes, byte[] key, byte[] vector)
    {
        using (var aesAlgorithm = Aes.Create())
        using (var decryptor    = aesAlgorithm.CreateDecryptor(key, vector))
        using (var memoryStream = new MemoryStream(encryptedBytes))
        using (var cryptoStream = new CryptoStream(memoryStream, decryptor, CryptoStreamMode.Read))
        using (var streamReader = new StreamReader(cryptoStream, Encoding.UTF8))
        {
            return streamReader.ReadToEnd();
        }
    }
 
    /// <summary>
    /// Encrypts the specified text and returns an encrypted byte array.
    /// </summary>
    /// <param name="plainText">The text to encrypt</param>
    /// <param name="key">The encryption key</param>
    /// <param name="vector">The initialization vector</param>
    /// <returns>The encrypted text as a byte array</returns>
    private byte[] Encrypt(string plainText, byte[] key, byte[] vector)
    {
        using (var aesAlgorithm = Aes.Create())
        using (var encryptor    = aesAlgorithm.CreateEncryptor(key, vector))
        using (var memoryStream = new MemoryStream())
        using (var cryptoStream = new CryptoStream(memoryStream, encryptor, CryptoStreamMode.Write))
        {
            using (var streamWriter = new StreamWriter(cryptoStream, Encoding.UTF8))
            {
                streamWriter.Write(plainText);
            }
 
            return memoryStream.ToArray();
        }
    }
 
    /// <summary>
    /// Generates a random initialization vector.
    /// </summary>
    /// <returns>The initialization vector as a byte array</returns>
    private byte[] GenerateInitializationVector()
    {
        var aesAlgorithm = Aes.Create();
        aesAlgorithm.GenerateIV();
        
        return aesAlgorithm.IV;
    }
 
    #endregion
 
    #endregion
}

Note that you should adjust the namespaces used in the above code to make them suitable for your project.

In addition to the IsEncrypted method, the EncryptionServices class exposes two public methods named EncryptString and DecryptString. Both methods accept two parameters; the string of text to encrypt or decrypt and the encryption/decryption key.

In the following sub-sections, we’ll walk through the key parts of the above code.

EncryptString

The EncryptString method carries out a basic check to see if there is any text available to encrypt and another check to determine if the text is already encrypted. If the text starts with a specific prefix value, then the code simply returns the specified text, treating it as already encrypted. It’s important that we don’t encrypt data multiple times!

The private GenerateInitializationVector method is called to generate a unique initialization vector. An initialization vector is used to ensure that when the same data is encrypted on different occasions, we do not get the same encrypted value, thereby greatly enhancing security.

The text, encryption key, and initialization vector are then passed to the private Encrypt method which applies the encryption algorithm to the text. The byte array returned by the Encrypt method is converted to a Base64 string using the static Convert.ToBase64String method. This allows the encrypted text to be easily transported and/or stored as a string value.

The final string that is returned to the caller is a combination of the encrypted value prefix, the initialization vector, and the encrypted text. Below is an example of the final encrypted data.

EncryptedValue:NaMjfYKUZXIZYs7RIUlznA==;bRJHPgU+cAdmFbguVGJmQY0gR7x+EAYcWpdGk3ADEnw=

By storing the data in this format, we can not only tell that the text is encrypted via the encrypted value prefix, but we can also easily parse out the initialization vector by looking for the semi-colon in the string. This works well since Base64 strings cannot contain semi-colon characters.

Note it is normal that the initialization vector is stored ‘as is’ along with the encrypted text so that decryption can be successfully performed later.

DecryptString

The DecryptString method is similar in nature to the Encrypt method except that it is of course doing the opposite by decrypting the specified text. As per the Encrypt method, a basic check is carried out to see if there is any text available to decrypt and there is a second check to determine if the text is encrypted before attempting to decrypt it.

The initialization vector is parsed from the text by splitting the string on the colon and semi-colon characters. The static Convert.FromBase64String method is used to convert the vector string to a byte array.

The encrypted bytes, encryption key, and initialization vector are then passed to the private Decrypt method and the decrypted text is returned to the caller.

IsEncrypted

The IsEncrypted method is a simple method that determines if the specified text is encrypted by checking if it starts with a specific value that indicates encrypted text.

Encrypt

The private Encrypt method contains the lower-level encryption logic. Every object that is created is wrapped with a using statement to ensure that resources are properly disposed of and the usings are stacked where possible for conciseness and code readability.

As per Microsoft recommendations, the static Aes.Create method is used to create a cryptographic object that can be used to perform the encryption.

The CreateEncryptor method is used to create a symmetric encryptor object with the specified key and vector.

A MemoryStream and CryptoStream is created in preparation for writing the cryptographic transformations to memory.

A StreamWriter instance is then used to write out the encrypted bytes to memory using UTF-8 encoding and the contents of the MemoryStream are returned as a byte array to the caller.

Decrypt

In a similar fashion to the Encrypt method, the private Decrypt method uses the static Aes.Create method to create a cryptographic object that can be used to perform the decryption.

The CreateDecryptor method is used to create a symmetric decryptor object with the specified key and vector. The same key and vector values that were used to encrypt the data must be used when decrypting since we are using a symmetric encryption algorithm.

The MemoryStream and CryptoStream objects are then created and this time a StreamReader instance is used to read in the decrypted bytes using UTF-8 encoding and a decrypted string is returned to the caller.

Interface

The EncryptionServices class implements an interface which is defined as follows.

namespace JC.Samples.SymmetricEncryption.Services.Interfaces;
 
/// <summary>
/// Encryption services interface.
/// </summary>
public interface IEncryptionServices
{
    #region Methods
 
    string DecryptString(string text, byte[] key);
    string EncryptString(string text, byte[] key);
    bool IsEncrypted(string text);
 
    #endregion
}

Note that you should adjust the namespace to make it suitable for your project.

Demo

Now let’s see how we can use an instance of the EncryptionServices class to encrypt and decrypt some text.

Encryption key

First, we need to create a suitable encryption key before we can encrypt or decrypt anything.

You can use the following code which leverages the RandomNumberGenerator class to generate a cryptographically random sequence of bytes and writes them out as a string to the console so that they can be easily copied.

using System.Security.Cryptography;

var bytes = new byte[32];
RandomNumberGenerator.Create().GetBytes(bytes);
foreach (byte b in bytes) {     Console.Write("{0}, ", b); }

// Example output: 73, 84, 28, 39, 182, 122, 193, 73, 43, 71, 106, 142, 76, 16, 54, 19, 21, 115, 138, 75, 45, 114, 41, 79, 181, 196, 40, 148, 154, 81, 173, 56,

These bytes can then be transposed into a byte array for demo purposes and I’ve included a class called DemoKey in the accompanying GitHub repository containing a static read-only Value field that holds the random bytes.

namespace JC.Samples.SymmetricEncryption;
 
/// <summary>
/// Holds a demo encryption key.
/// </summary>
internal class DemoKey
{
    #region Static Readonlys
 
    /// <summary>
    /// The encryption key value.
    /// </summary>
    internal static readonly byte[] Value = new byte[32] // 32 bytes = 256-bit.
    {
        73, 84, 28, 39, 182, 122, 193, 73, 43, 71, 106, 142, 76, 16, 54, 19, 21, 115, 138, 75, 45, 114, 41, 79, 181, 196, 40, 148, 154, 81, 173, 56
    };
 
    #endregion
}

Note that it is of course very important that you generate and use your own encryption key when developing a real-world project; don’t use the above demo key. You’ll also need to consider best practices for encryption key management as opposed to storing the encryption key in code.

Demo code

Below are the contents of a simple .NET Console application that uses the Encrypt and Decrypt methods provided by the EncryptionServices class to encrypt and decrypt some sample text, using the demo key Value from the DemoKey class.

using JC.Samples.SymmetricEncryption;
using JC.Samples.SymmetricEncryption.Services;
using JC.Samples.SymmetricEncryption.Services.Interfaces;
 
string plainText = "https://jonathancrozier.com";
Console.WriteLine("Plain text: {0}", plainText);
 
IEncryptionServices encryptionServices = new EncryptionServices();
 
Console.WriteLine("Encrypting plain text...");
string encryptedText = encryptionServices.EncryptString(plainText, DemoKey.Value);
Console.WriteLine("Encrypted data: {0}", encryptedText);
 
Console.WriteLine("Decrypting encrypted data...");
string decryptedText = encryptionServices.DecryptString(encryptedText, DemoKey.Value);
Console.WriteLine("Decrypted text: {0}", decryptedText);

The output of the above program will be similar to the following.

Plain text: https://jonathancrozier.com
Encrypting plain text...
Encrypted data: EncryptedValue:lKbobY5YzZn0HMoKkGp8Sg==;CMkKr9jvhL2WQcCrZf1AnGJwULlK+BfGsbsLReea1TY=
Decrypting encrypted data...
Decrypted text: https://jonathancrozier.com

As you can see from the above code, the EncryptionServices class provides a simple abstraction that is easy to work with, while underneath it is implementing the recommended best practices to provide strong protection of sensitive data.

Summary

In this article, I have demonstrated how you can use symmetric encryption within a .NET application to protect sensitive data.

I started by briefly looking at some of the .NET options that are available for symmetric encryption and discussed the latest Microsoft recommendations in relation to the usage of AES and specifically the static Aes.Create method.

I then walked through the implementation of a custom EncryptionServices class that uses the Aes class along with other relevant types within the System.Security.Cryptography namespace to encrypt and decrypt text in a practical way.

In closing, I recommend that you check out the full GitHub project that contains all of the source code used in this article so that you can get up and running quickly and tweak the solution to suit your needs.


I hope you enjoyed this post! Comments are always welcome and I respond to all questions.

If you like my content and it helped you out, please check out the button below 🙂

Comments

Felix

Just used this to add encryption, thanks 🙂

March 2, 2023

Jonathan Crozier

You’re most welcome, Felix. Glad you found it helpful! 😊

March 2, 2023

Gianluca

Thanks for the provided article and code Jonathan! On web there a plent of solution like your but no one is so clear and well explained. What if I need to encrypt a number or a string in a safeurl value? (of course I need also to decrypt it)

June 27, 2023

Jonathan Crozier

Hi, thanks for your comment! If I understand your question correctly it sounds like it would be better for you to use Base64 URL Encoding. I covered this in another article here: https://jonathancrozier.com/blog/base64-url-encoding-using-c-sharp

June 28, 2023