Search code examples
c#encryptionaes-gcm

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?


Solution

  • 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.