21

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?

janw
  • 8,758
  • 11
  • 40
  • 62

1 Answers1

33

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.

janw
  • 8,758
  • 11
  • 40
  • 62
  • 2
    Be careful of stackallocing an arbitrary amount of data -- at some point, depending on platform, etc, it will fail. The general advice is to keep it below 1024 bytes or so, and allocate an array if necessary. Also, if you're using Span, use `MemoryMarshal` rather than `BitConverter` -- this lets you work directly with the span without allocating intermediate arrays. – canton7 Mar 30 '20 at 08:13
  • 2
    @canton7 Thanks for the comments! I only send small packets, so I did not think of the stack space limitation - which might clearly become an issue for others using this code. Also, thanks for the `MemoryMarshal` hint, I did not know this class yet. I have adjusted the code accordingly. – janw Mar 30 '20 at 10:00
  • 2
    Argh sorry, I meant `BinaryPrimitives`, not `MemoryMarshal`. Most people write the stackalloc stuff as `Span plainBytes = cipherSize <= 1024 ? stackalloc[cipherSize] : new byte[cipherSize]` – canton7 Mar 30 '20 at 10:03
  • 1
    Is there any way to provide AAD(Additional Authentication Data) for encryption and decryption? – jiten Jul 12 '20 at 07:07
  • 2
    @vikky Yes, [there is](https://learn.microsoft.com/en-us/dotnet/api/system.security.cryptography.aesgcm.encrypt?view=netcore-3.1#System_Security_Cryptography_AesGcm_Encrypt_System_Byte___System_Byte___System_Byte___System_Byte___System_Byte___)! It is the optional fifth parameter of `Encrypt` and `Decrypt` (`associatedData`). – janw Jul 12 '20 at 07:41
  • 2
    Thank you, this works beautifully for me! Note that `AesGcm` is `IDisposable` and until we have any documentation about that class, it's probably better to properly release the created instances again. You might also want to reuse the instance on the same scope where `_key` is defined. – ygoe Sep 08 '20 at 06:27
  • 1
    @ygoe Thanks for pointing that out - [this may result in a memory leak](https://github.com/dotnet/runtime/blob/master/src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/AesGcm.Windows.cs#L43). I have adjusted the answer to include a `using` statement. – janw Sep 08 '20 at 16:47
  • 1
    Reusing the existing instance makes sense; I left it out of the answer for compactness, but I have updated the fiddle accordingly. – janw Sep 08 '20 at 16:54