Authenticated Encryption in .NET with AES-GCM

Scott Brady
Scott Brady
C#

When using symmetric encryption, you should be favoring authenticated encryption, such as AES-GCM (Galois/Counter Mode), rather than unauthenticated encryption, such as AES-CBC (Cipher Block Chaining).

Authenticated encryption provides you with confidentiality and an additional integrity check, allowing you to defend against various attacks based on the chosen-ciphertext attack.

In this article, you’re going to see how to use the AES-GCM implementation found in System.Security.Cryptography, available as of .NET Core 3. If you’re not there yet, I’m also going to show you how to do the same with Bouncy Castle.

Skip to the code.

Why Authenticated Encryption?

Authenticated encryption provides the usual confidentiality that you expect from encryption, but it also enables you to verify its integrity.

By including an in-built integrity check, you can check if the ciphertext is valid before attempting to decrypt it. This defends against chosen-ciphertext attacks, where an attacker will send your system invalid ciphertexts in an attempt to analyze the result. By refusing to decrypt this ciphertext, you stop this style of attack and won’t send the attacker any useful information.

Ideally, when choosing an encryption algorithm, you should be looking for one that supports authenticated encryption. To learn more about authenticated encryption, check out Matthew Green’s article on the topic.

There are definitely better alternatives than AES-GCM for authenticated encryption, such as AES-GCM-SIV and XChaCha20-Poly1305. However, if you want widespread interoperability, out-of-the-box support in .NET, and the speed provided by OS implementations, then AES-GCM is likely going to be the best option available to you.

AES-GCM vs. AES-CBC and AES-CCM

You should prefer AES-GCM over AES-CBC. AES-CBC is not authenticated encryption, so it is vulnerable to the various chosen-ciphertext attacks I mentioned earlier. As of v1.3, TLS no longer supports AES-CBC.

You should also prefer AES-GCM over AES-CCM (Counter with CBC-MAC). AES-CCM will authenticate the plaintext message rather than authenticate the ciphertext (we prefer authenticating the ciphertext) and is otherwise vulnerable to attacks that GCM is not. If you need it, AES-CCM is also available in .NET Core 3 onwards.

AES-GCM Encryption in .NET

If you are using .NET Core 3 onwards, you can use the AES-GCM implementation found in System.Security.Cryptography. On Windows and Linux, this API will call into the OS implementations of AES, while macOS will require you to have OpenSSL installed. This example will use AES-256-GCM.

AesGcm

To encrypt or decrypt, you will first need to new up an instance of AesGcm.

To create this object, you will need a key. Since this is symmetric encryption, you’ll need the same key for both encryption and decryption.

This key must be the same length as your chosen block size; otherwise, it will throw a CryptographicException with a message of “Specified key is not a valid size for this algorithm”. So that means if you are using AES-256-GCM, you’ll need a key 256-bits (32-bytes) in length.

The AesGcm class implements IDisposable, so be sure to include a using statement.

// generate a key
var key = new byte[32];
RandomNumberGenerator.Fill(key);

using var aes = new AesGcm(key);

Encryption

To encrypt, you’ll first need to generate a nonce: a number used once. This must be a cryptographically random value unique to this operation. You must never re-use a nonce for the same key; otherwise, you will destroy the security of this encryption algorithm. You’ll also see “nonce” referred to as the Initialization Vector (IV).

For AES-GCM, the nonce must be 96-bits (12-bytes) in length.

var nonce = new byte[AesGcm.NonceByteSizes.MaxSize]; // MaxSize = 12
RandomNumberGenerator.Fill(nonce);

In this case, you are using a random nonce. This means that you can only use this key 232 times before you’ll start repeating nonces for this key. Depending on your use case, another approach is to use deterministic nonces, such as a counter, which could give you anywhere up to 296 uses per key. If you want to learn more, NIST has a whole document on choosing the correct nonce for AES-GCM; just be aware that it should always be 96-bits long.

Once you have your nonce value, you’ll need to get some byte arrays ready. You’ll need to convert your plaintext (the value you want to encrypt) into bytes and create destinations for the resulting ciphertext and tag (the authentication tag, also known as the MAC).

var plaintextBytes = Encoding.UTF8.GetBytes("got more soul than a sock with a hole");
var ciphertext = new byte[plaintextBytes.Length];
var tag = new byte[AesGcm.TagByteSizes.MaxSize]; // MaxSize = 16

The ciphertext is always the same length as the plaintext bytes. The tag is typically 128-bits (16-bytes). It can be less, but the calculated tag itself is always 128-bits; taking less means you only use the leftmost bytes and simply lose data.

Now that you have everything ready, you can call the Encrypt method:

aes.Encrypt(nonce, plaintextBytes, ciphertext, tag);

You can optionally pass in some associated data. This is data that will not be encrypted but will be necessary to authenticate the data. This would be data that needs to be kept as plaintext but still protected from tampering.

Put it all together in a console app, base64 encode it and you’ll end up with something like this:

Plaintext: Got more soul than a sock with a hole
Key: L4rzbn7Vuvrw3CJ21FyUqRO2nhOYRuzZ9r2dKVCZPKA=
Nonce (IV): x+tpmCnO8FYW2Hop
Ciphertext: eQclaNYmXXRB3ZG1ZWp0NxS7ZAuJ57Y8OZWaqB/C1UmNgZbT4w==
Tag: wZA1+zIIWlsKABEuJhfn2A==

To decrypt the ciphertext, you will need to remember not only the ciphertext but also the nonce and tag. How you handle that is up to your use case For example, JWE keeps them separate; otherwise, a common approach is to concatenate them as:

Nonce (12B) | Ciphertext (*B) | Tag (16B)

Decryption

Decryption follows a similar process. With your encryption key, ciphertext, nonce, and tag, you can retrieve the plaintext.

This time, all you need to prepare is the byte array for the plaintext value.

private string Decrypt(byte[] ciphertext, byte[] nonce, byte[] tag, byte[] key)
{
    using (var aes = new AesGcm(key))
    {
        var plaintextBytes = new byte[ciphertext.Length];

        aes.Decrypt(nonce, ciphertext, tag, plaintextBytes);

        return Encoding.UTF8.GetString(plaintextBytes);
    }
}

Source Code

You can find a small test runner for AES-GCM in my usual samples repository on GitHub. This includes encryption & decryption using both the .NET libraries and Bouncy Castle.

Bonus: AES-GCM Encryption using Bouncy Castle

If you need to use a software implementation of Bouncy Castle, or if your version of .NET does not support AesGcm, then the below code demonstrates how to use Bouncy Castle for AES-GCM. Also, there are so few working C# examples of Bouncy Castle out there, so I figure every little helps.

This will use the Portable.BouncyCastle library, available on nuget.

dotnet add package Portable.BouncyCastle

To encrypt, you’ll be using the GcmBlockCipher class:

private static (byte[] ciphertext, byte[] nonce, byte[] tag)EncryptWithBouncyCastle(string plaintext, byte[] key)
{
    const int nonceLength = 12; // in bytes
    const int tagLenth = 16; // in bytes
    
    var nonce = new byte[nonceLength];
    RandomNumberGenerator.Fill(nonce);
    
    var plaintextBytes = Encoding.UTF8.GetBytes(plaintext);
    var bcCiphertext = new byte[plaintextBytes.Length + tagLenth];
    
    var cipher = new GcmBlockCipher(new AesEngine());
    var parameters = new AeadParameters(new KeyParameter(key), tagLenth * 8, nonce);
    cipher.Init(true, parameters);
    
    var offset = cipher.ProcessBytes(plaintextBytes, 0, plaintextBytes.Length, bcCiphertext, 0);
    cipher.DoFinal(bcCiphertext, offset);

    // Bouncy Castle includes the authentication tag in the ciphertext
    var ciphertext = new byte[plaintextBytes.Length];
    var tag = new byte[tagLenth];
    Buffer.BlockCopy(bcCiphertext, 0, ciphertext, 0, plaintextBytes.Length);
    Buffer.BlockCopy(bcCiphertext, plaintextBytes.Length, tag, 0, tagLenth);

    return (ciphertext, nonce, tag);
}

The main difference here (other than the low-level API) is that the resulting ciphertext comes pre-concatenated with the authentication tag. As a result, you’ll need to make sure you create the correct size byte array and split them at the end if you want the ciphertext and tag separate.

Decryption is much the same and you’ll have to make sure that the tag is appended to the end of the ciphertext.

private static string DecryptWithBouncyCastle(byte[] ciphertext, byte[] nonce, byte[] tag, byte[] key)
{
    var plaintextBytes = new byte[ciphertext.Length];

    var cipher = new GcmBlockCipher(new AesEngine());
    var parameters = new AeadParameters(new KeyParameter(key), tag.Length * 8, nonce);
    cipher.Init(false, parameters);

    var bcCiphertext = ciphertext.Concat(tag).ToArray();

    var offset = cipher.ProcessBytes(bcCiphertext, 0, bcCiphertext.Length, plaintextBytes, 0);
    cipher.DoFinal(plaintextBytes, offset);

    return Encoding.UTF8.GetString(plaintextBytes);
}