As programmers, we strive to make our system as impenetrable as possible from a security perspective. But we often forget to ask the question: “What happens if a hacker manages to gain root access to the database?”
Privacy goes straight out the window if sensitive data is not encrypted.
We are used to hashing passwords to prevent recovery of the plain text, but for other types of sensitive data we need to use symmetric encryption.
In this article, we are going to look into AES and how to properly code it in C#.
AES
The Advanced Encryption Standard, a variant of the Rijndael block cipher, is a block cipher that supports 128-bit, 192-bit and 256-bit symmetric encryption.
While the block size for AES is always 128-bit, the key size can be 16, 24 or 32 bytes. Obviously, 32-byte keys provide maximum security. That’s because 2^256 is an impossibly large number, and it is simply not possible to calculate all possible combinations. At this point, we are getting close to the number of particles in the universe.
Because AES is a block cipher, it also uses an Initialization Vector (IV), which is basically a starting point for the encryption process. The IV length is the same length as the block size, e.g. 16 bytes. An IV should be randomly generated, and should never be used twice for the same plain text.
Using AES in .NET
Utilising AES encryption in .NET is pretty straightforward, but there are a few things to pay attention to.
Firstly, everything we need is in this namespace:
using System.Security.Cryptography;
Creating a new Aes instance confirms to us the valid key sizes:
using var aes = Aes.Create();
foreach (KeySizes sizes in aes.LegalKeySizes)
{
int i = sizes.MinSize;
do Console.WriteLine("Valid key size: " + i);
while ((i += sizes.SkipSize) <= sizes.MaxSize);
}
Prints the output:
Valid key size: 128
Valid key size: 192
Valid key size: 256
Think bytes, not strings
In the context of data encryption, you should always think in terms of byte arrays - byte[]
Strings have no meaning here, so you should always use something like the following to get a byte array from a string:
var bytes = System.Text.Encoding.UTF8.GetBytes("My private info!");
Generating an initialization vector
This is done automatically when creating a new Aes instance, but to be explicit, or to generate a new IV, we can do:
using var aes = Aes.Create();
aes.GenerateIV();
Remember to get the newly generated IV from the Aes instance, accessed on the property called .IV, because you will need it to decrypt the data!
Setting the 256-bit key
Setting the key is also very straightforward:
aes.Key = aesKey;
Just make sure that your aesKey variable is of type byte[] with Length 32.
Tip: You can use HMACSHA256 to easily create a 256-bit byte array from a password and secret.
Encrypting the data by using the Crypto Stream
You may be tempted to use methods on the Aes instance itself, such as aes.EncryptCbc(…); or others. Do not use these methods directly. Instead, create an encryptor and pass this to a new CryptoStream object together with a MemoryStream, in the following way:
using var stream = new MemoryStream();
var enc = aes.CreateEncryptor();
using (var cryptoStream = new CryptoStream(stream, enc, CryptoStreamMode.Write))
{
cryptoStream.Write(plainText, 0, plainText.Length);
}
byte[] encrypted = stream.ToArray();
The ICryptoTransform implementation is created with the current key and IV when calling .CreateEncryptor();
We need to create a MemoryStream (or any other type of Stream) as a destination for writing the encrypted data to.
The CryptoStream object allows you to write bytes to it directly (here, plainText is a byte array). The CryptoStream should also be closed after writing to it, by calling
.Dispose()
or preferably by scoping a using-statement as I’ve done here.Finally, pull the byte array from the MemoryStream.
You will need to save the IV, and save the encrypted byte array, but other than that, you now have everything you need for decryption.
Decrypting data
Decryption is done in virtually the same way, with a few small changes. Instead of generating a new IV, we set the IV with our saved IV. Instead of creating an encryptor, we create a decryptor.
using (var aes = Aes.Create())
aes.Key = aesKey;
aes.IV = iv;
using var stream = new MemoryStream();
var enc = aes.CreateDecryptor();
using (var cryptoStream = new CryptoStream(stream, enc, CryptoStreamMode.Write))
{
cryptoStream.Write(cypherText, 0, cypherText.Length);
}
byte[] decrypted = stream.ToArray();
Improving security with a packet signature
It is known that some symmetric encryption algorithms are susceptible to “timing attacks”. These type of attacks aim to measure minute differences in computation time of the decryption process when the cipher text is known, thus getting the target system to leak information about the plain text or the key.
When trying to decrypt AES-encrypted data with the wrong IV or wrong key, you will usually get an “Padding is invalid and cannot be removed” CryptographicException. There is no concept of the whether the key is correct or not when the decryption process starts, there is only either output of decrypted data, or a failure in the process of decryption. This also occurs if you change just one byte of the encrypted data, even if you use the correct key and IV.
Adding a signature (for example, a 256-bit HMAC hash) to end of the encrypted byte array means that you will be able to check it before attempting the decryption process. This effectively stops timing attacks dead, since the hashing process is linear according to the quantity of input data.
How to handle the IV
The initialization vector is not a secret piece of information, because it is merely a jumping-off point for the encryption and decryption process. It can therefore be appended (or prepended) to the data packet after encryption. However, if the key is compromised, decryption is trivial, since the starting point is known.
The IV for AES is 128-bit (16 bytes). Therefore, there are 16 bytes of potential entropy that you can secure further by “encrypting” the IV which is prepended to the data packet.
This can be done in a simple fashion by running an XOR on those 128 bits with a different 128-bit secret.
XOR is a very simple way to encrypt fixed-length strings of information when the clear text is not known to the attacker.
Putting all the code together
Here is the final code, which creates a signed packet, optionally with an XORed IV, and decrypts it.
using System.Security.Cryptography;
namespace ACA.Security.Cryptography;
public static class AES256
{
private const ushort AES_BITS = 256;
private const byte AES_KEY_LENGTH = AES_BITS / 8;
private const byte IV_BITS = 128;
private const byte IV_KEY_LENGTH = IV_BITS / 8;
private const byte SIGNATURE_BYTE_LENGTH = 32;
private const byte MIN_PACKET_LENGTH = 2 * IV_KEY_LENGTH + SIGNATURE_BYTE_LENGTH;
///
/// Encrypt data into a packet
///
/// The clear text byte array
/// The 256-bit (32 byte) AES key
/// A signing key, preferably 64-byte
/// An optional 16-byte key for extra security
///
///
public static byte[] EncryptAndSign(byte[] clearText, byte[] aesKey,
byte[] hmacSigningKey, byte[]? ivXorKey = null)
{
if (clearText is null || clearText.Length == 0)
throw new ArgumentNullException(nameof(clearText));
if (aesKey is null)
throw new ArgumentNullException(nameof(aesKey));
if (hmacSigningKey is null || hmacSigningKey.Length == 0)
throw new ArgumentNullException(nameof(hmacSigningKey));
if (aesKey.Length != AES_KEY_LENGTH)
throw new ArgumentOutOfRangeException(nameof(aesKey), "AES key must be " + AES_BITS + "-bit!");
if (ivXorKey != null && ivXorKey.Length != IV_KEY_LENGTH)
throw new ArgumentOutOfRangeException(nameof(ivXorKey), "IV-XOR-key must be " + IV_BITS + "-bit!");
byte[] encrypted, iv, signature, packet;
using (var aes = Aes.Create())
{
aes.Key = aesKey;
aes.GenerateIV();
iv = aes.IV;
using var stream = new MemoryStream();
var enc = aes.CreateEncryptor();
using (var cryptoStream = new CryptoStream(stream, enc, CryptoStreamMode.Write))
{
cryptoStream.Write(clearText, 0, clearText.Length);
}
encrypted = stream.ToArray();
}
if (ivXorKey != null)
iv = Xor(iv, ivXorKey);
packet = new byte[iv.Length + encrypted.Length];
iv.CopyTo(packet, 0);
encrypted.CopyTo(packet, iv.Length);
using (var hmac = GetHmac(hmacSigningKey))
{
signature = hmac.ComputeHash(packet);
}
var originalPacketLength = packet.Length;
Array.Resize(ref packet, packet.Length + signature.Length);
signature.CopyTo(packet, originalPacketLength);
return packet;
}
private static HMAC GetHmac(byte[] key) => new HMACSHA256(key);
///
/// Encrypt a packet into data
///
/// The encrypted data packet byte array
/// The 256-bit (32 byte) AES key
/// A signing key, preferably 64-byte
/// An optional 16-byte key for extra security
///
///
///
public static byte[] DecryptSignedPacket(byte[] packet, byte[] aesKey,
byte[] hmacSigningKey, byte[]? ivXorKey = null)
{
if (packet is null)
throw new ArgumentNullException(nameof(packet));
if (aesKey is null)
throw new ArgumentNullException(nameof(aesKey));
if (hmacSigningKey is null)
throw new ArgumentNullException(nameof(hmacSigningKey));
if (packet.Length < MIN_PACKET_LENGTH)
throw new ArgumentOutOfRangeException(nameof(packet), "Packet length must be at least " + MIN_PACKET_LENGTH + " bytes");
if (aesKey.Length != AES_KEY_LENGTH)
throw new ArgumentOutOfRangeException(nameof(aesKey), "AES key must be " + AES_BITS + "-bit!");
byte[] iv, decrypted, hash;
byte[] signature = new byte[SIGNATURE_BYTE_LENGTH];
Array.Copy(packet, packet.Length - SIGNATURE_BYTE_LENGTH, signature, 0, SIGNATURE_BYTE_LENGTH);
Array.Resize(ref packet, packet.Length - SIGNATURE_BYTE_LENGTH);
using (var hmac = GetHmac(hmacSigningKey))
{
hash = hmac.ComputeHash(packet);
}
if (!signature.SequenceEqual(hash))
throw new ArgumentException("The packet signature is invalid.", nameof(packet));
byte[] cypherText = new byte[packet.Length - IV_KEY_LENGTH];
Array.Copy(packet, IV_KEY_LENGTH, cypherText, 0, packet.Length - IV_KEY_LENGTH);
Array.Resize(ref packet, IV_KEY_LENGTH);
iv = packet;
if (ivXorKey != null)
iv = Xor(iv, ivXorKey);
using (var aes = Aes.Create())
{
aes.Key = aesKey;
aes.IV = iv;
using var stream = new MemoryStream();
var enc = aes.CreateDecryptor();
using (var cryptoStream = new CryptoStream(stream, enc, CryptoStreamMode.Write))
{
cryptoStream.Write(cypherText, 0, cypherText.Length);
}
decrypted = stream.ToArray();
}
return decrypted;
}
private static byte[] Xor(byte[] array1, byte[] array2)
{
if (array1 is null)
throw new ArgumentNullException(nameof(array1));
if (array2 is null)
throw new ArgumentNullException(nameof(array2));
if (array1.Length != array2.Length)
throw new ArgumentException("Both arrays must be the same length!");
byte[] output = new byte[array1.Length];
for (int i = 0; i < array1.Length; i++)
{
output[i] = (byte)(array1[i] ^ array2[i]);
}
return output;
}
}
Happy coding! Please remember to share this page and to come back soon! 😃