Symmetric Encryption with the .NET Framework
A basic introduction
All of the cryptographic algorithms deriving from the SymmetricAlgorithm class use cipher block chaining (CBC). Block ciphers work on fixed length chunks (or blocks) of bytes. If the length of plain text is not a multiple of the block size, then padding is introduced in order to increase the length such that the final length is a multiple of the block size.
These algorithms all rely on a key and an initialization vector (IV). The key should only be known by the party performing the cryptocraphic operation. The IV is not considered private. As we will see below, we can include the IV as the first bytes of the stream containing the cipher text. As such the only thing that the application will need to persist is the encryption key and the cipher text. Best practices dictate that we should store our encryption keys separate from our cipher text. In an ideal world we would store our keys in Azure KeyVault or some other hardware security module (HSM).
Encrypt
Each block of plain text is XOR’d against the prior block of cipher text in order to introduce randomness into the algorithm so that the same value re-encrypted with the same key will produce different cipher text. The first block of clear text to be processed won’t have a block of cipher text to XOR against. The purpose of the IV is to provide that first block of data to perform the XOR against.
A new initialization vector should be generated each time encryption is performed on a set of data. We accomplish this in the below EncryptAsync
method by calling the GenerateIV
method on the instance of the algorithm in use. GenerateIV
uses a cryptographically secure pseudo-random number generator (RNGCryptoServiceProvider) in order to generate an array of bytes that matches the length of the block size for the algorithm.
In order to minimize the amount of state the application needs to persist, we write the IV to the beginning of the stream that will contain the cipher text.
public static async Task EncryptAsync(Stream cleartext, Stream ciphertext, byte[] key) {
using (var algo = new T()) {
// generate a new IV for each encryption operation
algo.GenerateIV();
// write IV to the beginning of the stream in cleartext bytes
await ciphertext.WriteAsync(algo.IV, 0, algo.IV.Length);
var encryptor = algo.CreateEncryptor(key, algo.IV);
using (var crypto = new CryptoStream(ciphertext, encryptor, CryptoStreamMode.Write)) {
await cleartext.CopyToAsync(crypto);
}
algo.Clear();
}
}
Decrypt
When decrypting cipher text generated with one of the symmetric algorithms, both the key and initialization vector must match those used to encrypt the data. We retrieve our key from a persistence mechanism such as a database, Azure KeyVault, a HSM, or some other method. Our initialization vector is read from the beginning of the cipher text stream.
public static async Task DecryptAsync(Stream ciphertext, Stream cleartext, byte[] key) {
using (var algo = new T()) {
// read IV from the beginning of the stream
var iv = new byte[algo.BlockSize / 8];
await ciphertext.ReadAsync(iv, 0, iv.Length);
var decryptor = algo.CreateDecryptor(key, iv);
using (var crypto = new CryptoStream(ciphertext, decryptor, CryptoStreamMode.Read)) {
await crypto.CopyToAsync(cleartext);
}
algo.Clear();
}
}
Helper Class
The below SymmetricEncryption<T>
class is a helper class that takes care of the initialization vector generation on each call to EncryptAsync
as well as writing the IV at the beginning of the cipher text. The class provides helpers for encrypting, decrypting, and generating random cryptographic keys based on the algorithm provided as the generic parameter.
This class currently relies on the default key length, mode, and padding for the provided algorithm. It could easily be updated to allow the caller to pass in a pre-configured instance of the algorithm as well.
public static class SymmetricEncryption<T> where T : SymmetricAlgorithm, new() {
public static byte[] GenerateKey() {
using (var algo = new T()) {
algo.GenerateKey();
return algo.Key;
}
}
public static async Task EncryptAsync(Stream cleartext, Stream ciphertext, byte[] key) {
if (null == cleartext)
throw new ArgumentNullException(nameof(cleartext));
if (null == ciphertext)
throw new ArgumentNullException(nameof(ciphertext));
if (null == key)
throw new ArgumentNullException(nameof(key));
using (var algo = new T()) {
// generate a new IV for each encryption operation
algo.GenerateIV();
// write IV to the beginning of the stream in cleartext bytes
await ciphertext.WriteAsync(algo.IV, 0, algo.IV.Length);
var encryptor = algo.CreateEncryptor(key, algo.IV);
using (var crypto = new CryptoStream(ciphertext, encryptor, CryptoStreamMode.Write)) {
await cleartext.CopyToAsync(crypto);
}
algo.Clear();
}
}
public static async Task<byte[]> EncryptAsync(byte[] input, byte[] key) {
if (null == input)
throw new ArgumentNullException(nameof(input));
using (var cleartext = new MemoryStream(input)) {
using (var ciphertext = new MemoryStream()) {
await EncryptAsync(cleartext, ciphertext, key);
return ciphertext.ToArray();
}
}
}
public static async Task<string> EncryptAsync(string cleartext, byte[] key, Encoding encoding = null) {
if (string.IsNullOrEmpty(cleartext))
throw new ArgumentNullException(nameof(cleartext));
encoding = encoding ?? Encoding.UTF8;
var encryptedBytes = await EncryptAsync(encoding.GetBytes(cleartext), key);
return Convert.ToBase64String(encryptedBytes);
}
public static async Task DecryptAsync(Stream ciphertext, Stream cleartext, byte[] key) {
if (null == ciphertext)
throw new ArgumentNullException(nameof(ciphertext));
if (null == cleartext)
throw new ArgumentNullException(nameof(cleartext));
if (null == key)
throw new ArgumentNullException(nameof(key));
using (var algo = new T()) {
// read IV from the beginning of the stream
var iv = new byte[algo.BlockSize / 8];
await ciphertext.ReadAsync(iv, 0, iv.Length);
var decryptor = algo.CreateDecryptor(key, iv);
using (var crypto = new CryptoStream(ciphertext, decryptor, CryptoStreamMode.Read)) {
await crypto.CopyToAsync(cleartext);
}
algo.Clear();
}
}
public static async Task<byte[]> DecryptAsync(byte[] encrypted, byte[] key) {
if (null == encrypted)
throw new ArgumentNullException(nameof(encrypted));
using (var ciphertext = new MemoryStream(encrypted)) {
using (var cleartext = new MemoryStream()) {
await DecryptAsync(ciphertext, cleartext, key);
return cleartext.ToArray();
}
}
}
public static async Task<string> DecryptAsync(string encrypted, byte[] key, Encoding encoding = null) {
if (string.IsNullOrEmpty(encrypted))
throw new ArgumentNullException(nameof(encrypted));
var clearBytes = await DecryptAsync(Convert.FromBase64String(encrypted), key);
encoding = encoding ?? Encoding.UTF8;
return encoding.GetString(clearBytes);
}
}
Example Usage
This example uses the simplist overloads for encrypting/decrypting a string using the AesManaged
algorithm.
In this case the EncryptAsync
overload takes an input
parameter as a string and outputs the cipher text as a base64 encoded string. The DecryptAsync
overload expects a base64 encoded representation of the cipher text and returns a clear text string. Both methods support passing in a specific encoding
parameter. If an encoding
parameter is not passed, then Encoding.UTF8
is used.
var input = @"There are two types of encryption: one that will prevent your sister
from reading your diary and one that will prevent your government.";
var key = SymmetricEncryption<AesManaged>.GenerateKey();
// Returns a base64 encoded representation of the ciphertext
var ciphertext = await SymmetricEncryption<AesManaged>.EncryptAsync(input, key);
// Accepts a base64 encoded representatino of the ciphertext
var cleartext= await SymmetricEncryption<AesManaged>.DecryptAsync(ciphertext, key);
Share this post
Twitter
Facebook
LinkedIn