Reputation: 9606
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
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
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
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