janw
janw

Reputation: 9606

Using the AesGcm class

I just noticed that .NET Standard 2.1/.NET Core 3.0 finally added a class for AES-GCM encryption.

However, its API seems to be slightly different from the usual .NET crypto classes: Its Encrypt function asks for pre-allocated byte arrays for the cipher text and the tag, instead of providing them itself. Unfortunately there is no example in the docs showing proper usage of that class.

I know how to calculate the expected cipher text size for an AES encryption in theory, but I wonder whether it is really the intended approach to kind of "guess" a buffer size for the cipher text there. Usually crypto libraries provide functions that take care of those calculations.

Does someone have an example on how to properly encrypt a byte array using AesGcm?

Upvotes: 25

Views: 25824

Answers (3)

Ogglas
Ogglas

Reputation: 69968

Example from Scott Brady using the AES-GCM implementation found in System.Security.Cryptography:

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

using var aes = new AesGcm(key);

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

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

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

Gives output like this:

Plaintext: Got more soul than a sock with a hole

Key: L4rzbn7Vuvrw3CJ21FyUqRO2nhOYRuzZ9r2dKVCZPKA=

Nonce (IV): x+tpmCnO8FYW2Hop

Ciphertext: eQclaNYmXXRB3ZG1ZWp0NxS7ZAuJ57Y8OZWaqB/C1UmNgZbT4w==

Tag: wZA1+zIIWlsKABEuJhfn2A==

Remember, to decrypt the ciphertext, you will need to remember not only the ciphertext but also the nonce and tag.

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:

https://www.scottbrady91.com/c-sharp/aes-gcm-dotnet

https://github.com/scottbrady91/samples/blob/master/AesGcmEncryption/Program.cs

Upvotes: 2

ebvtrnog
ebvtrnog

Reputation: 4377

To use AesGcm in .NET via streams, without having to manage the nonce and authentication tags yourself, there's a library for .NET, StreamingAead:

dotnet add package StreamingAead

From the description:

A library for streaming encryption and decryption of large datasets, based on Google's Tink protocol for Streaming AEAD. It features AES256_GCM_HKDF_1MB for segment-wise AES-256 GCM encryption, ensuring data security with a focus on efficiency and interoperability with Tink.

Here's an example:

byte[] key = File.ReadAllBytes(args.KeyPath);
byte[] associatedDataBytes = Encoding.UTF8.GetBytes(args.AssociatedData);

using (var inputStream = File.OpenRead(args.InputPath))
using (var outputStream = File.OpenWrite(args.OutputPath))
{
    if (args.Mode == "encrypt")
    {
        AES256_GCM_HKDF_1MB.Encrypt(key, inputStream, outputStream, associatedDataBytes);
    }
    else if (args.Mode == "decrypt")
    {
        AES256_GCM_HKDF_1MB.Decrypt(key, inputStream, outputStream, associatedDataBytes);
    }
    else
    {
        Console.WriteLine($"Invalid mode: {args.Mode}");
        return;
    }
}

Upvotes: 0

janw
janw

Reputation: 9606

I figured it out now.

I forgot that in GCM, the cipher text has the same length as the plain text; contrary to other encryption modes like CBC, no padding is required. The nonce and tag lengths are determined by the NonceByteSizes and TagByteSizes properties of AesGcm, respectively.

Using this, encryption can be done in the following way:

public string Encrypt(string plain)
{
    // Get bytes of plaintext string
    byte[] plainBytes = Encoding.UTF8.GetBytes(plain);
    
    // Get parameter sizes
    int nonceSize = AesGcm.NonceByteSizes.MaxSize;
    int tagSize = AesGcm.TagByteSizes.MaxSize;
    int cipherSize = plainBytes.Length;
    
    // We write everything into one big array for easier encoding
    int encryptedDataLength = 4 + nonceSize + 4 + tagSize + cipherSize;
    Span<byte> encryptedData = encryptedDataLength < 1024
                             ? stackalloc byte[encryptedDataLength]
                             : new byte[encryptedDataLength].AsSpan();
    
    // Copy parameters
    BinaryPrimitives.WriteInt32LittleEndian(encryptedData.Slice(0, 4), nonceSize);
    BinaryPrimitives.WriteInt32LittleEndian(encryptedData.Slice(4 + nonceSize, 4), tagSize);
    var nonce = encryptedData.Slice(4, nonceSize);
    var tag = encryptedData.Slice(4 + nonceSize + 4, tagSize);
    var cipherBytes = encryptedData.Slice(4 + nonceSize + 4 + tagSize, cipherSize);
    
    // Generate secure nonce
    RandomNumberGenerator.Fill(nonce);
    
    // Encrypt
    using var aes = new AesGcm(_key);
    aes.Encrypt(nonce, plainBytes.AsSpan(), cipherBytes, tag);
    
    // Encode for transmission
    return Convert.ToBase64String(encryptedData);
}

Correspondingly, the decryption is done as follows:

public string Decrypt(string cipher)
{
    // Decode
    Span<byte> encryptedData = Convert.FromBase64String(cipher).AsSpan();
    
    // Extract parameter sizes
    int nonceSize = BinaryPrimitives.ReadInt32LittleEndian(encryptedData.Slice(0, 4));
    int tagSize = BinaryPrimitives.ReadInt32LittleEndian(encryptedData.Slice(4 + nonceSize, 4));
    int cipherSize = encryptedData.Length - 4 - nonceSize - 4 - tagSize;
    
    // Extract parameters
    var nonce = encryptedData.Slice(4, nonceSize);
    var tag = encryptedData.Slice(4 + nonceSize + 4, tagSize);
    var cipherBytes = encryptedData.Slice(4 + nonceSize + 4 + tagSize, cipherSize);
    
    // Decrypt
    Span<byte> plainBytes = cipherSize < 1024
                          ? stackalloc byte[cipherSize]
                          : new byte[cipherSize];
    using var aes = new AesGcm(_key);
    aes.Decrypt(nonce, cipherBytes, tag, plainBytes);
    
    // Convert plain bytes back into string
    return Encoding.UTF8.GetString(plainBytes);
}

See dotnetfiddle for the full implementation and an example.

Note that I wrote this for network transmission, so everything is encoded into one, big base-64 string; alternatively, you can return nonce, tag and cipherBytes separately via out parameters.

The network setting is also the reason why I send the nonce and tag sizes: The class might be used by different applications with different runtime environments, which might have different supported parameter sizes.

Upvotes: 39

Related Questions