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.
Comments
Felix
Just used this to add encryption, thanks 🙂
March 2, 2023Jonathan Crozier
You’re most welcome, Felix. Glad you found it helpful! 😊
March 2, 2023Gianluca
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, 2023Jonathan 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