Search code examples
c#bouncycastleaes-gcm

BouncyCastle C# GCM Ecrypt and GCM Decrypt


I was trying to learn AES GCM from (https://asecuritysite.com/bouncy/bc_gcm). Although the site does a nice job combining the code all in one. I was trying to break it out and make it a function (GCM Encrypt and GCM Decrypt). I seem to have figured out the encrypt part, but I am totally lost on the decryption part. Any assistance would be greatly appreciated.

Oh, I did do a search for the errors I'm finding (Key length not 128/192/256 bits and 'mac check in GCM failed'). Most of them were Java, Python or android which I'm not familiar with and didn't really cover for the most part BouncyCastle GCM Encrypt and GCM Decrypt or in a way for a newbie to understand.

Here is what I was able to construct.

    public static string Testing_GCM_Encrypt2(string msg)
    {
        //var msg = "Hello 123";
        var add = "";
        var iv = "00112233445566778899AABBCCDDEEFF00";
        //
        byte[] plainTextData = System.Text.Encoding.UTF8.GetBytes(msg);
        IBlockCipher cipher = new AesEngine();
        int macSize = 8 * cipher.GetBlockSize();
        byte[] nonce = new byte[8];
        Array.Copy(System.Text.Encoding.UTF8.GetBytes(iv), nonce, 8);
        //
        byte[] associatedText = System.Text.Encoding.UTF8.GetBytes(add);
        //
        CipherKeyGenerator keyGen = new CipherKeyGenerator();
        keyGen.Init(new KeyGenerationParameters(new SecureRandom(), 128));
        KeyParameter keyParam = keyGen.GenerateKeyParameter();

        AeadParameters keyParamAead = new AeadParameters(keyParam, macSize, nonce, associatedText);
        GcmBlockCipher cipherMode = new GcmBlockCipher(cipher);
        cipherMode.Init(true, keyParamAead);
        int outputSize = cipherMode.GetOutputSize(plainTextData.Length);
        byte[] cipherTextData = new byte[outputSize];
        int result = cipherMode.ProcessBytes(plainTextData, 0, plainTextData.Length, cipherTextData, 0);
        cipherMode.DoFinal(cipherTextData, result);
        var rtn = cipherTextData;
        //
        return Convert.ToBase64String(rtn);
    }
   // Decrypt:

    public static string Testing_GCM_Decrypt2(string Ciphertext)
    {
        String iv = "00112233445566778899AABBCCDDEEFF00";
        byte[] byteIV = System.Text.Encoding.UTF8.GetBytes(iv);
        //
        byte[] nonce = new byte[8];
        //
        byte[] TextData = System.Text.Encoding.UTF8.GetBytes(Ciphertext);
        var add = string.Empty;
        byte[] associatedText = System.Text.Encoding.UTF8.GetBytes(add);
        //
        GcmBlockCipher cipher = new GcmBlockCipher(new AesEngine());
        cipher.Init(false, new AeadParameters(new KeyParameter(byteIV), 128, nonce));
        //
        int outputSize = cipher.GetOutputSize(TextData.Length);
        TextData = new byte[outputSize];
        //
        int result = cipher.ProcessBytes(TextData, 0, TextData.Length, TextData, 0);
        cipher.DoFinal(TextData, result);
        var pln = TextData;
        return System.Text.Encoding.UTF8.GetString(pln );
    }

Solution

  • Decryption does not work for various reasons:

    1. The 128 bit key generated and used during encryption is not applied during decryption. Instead, byteIV is used as the key for decryption. This is 34 bytes in size and therefore does not correspond to any of the valid AES key lengths, which triggers a corresponding exception. The key generated during encryption can be determined with KeyParameter#GetKey() and must be stored securely in a suitable location so that it is available for decryption.
    2. The nonce used for encryption is not the same as that used for decryption. During encryption, an 8 bytes nonce is generated from the UTF-8 encoded iv by copying the first 8 bytes. For decryption, an empty 8 bytes nonce is used.
    3. For the encrypted data and decrypted data the same variable is used (TextData), so that the encrypted data is lost with TextData = new byte[outputSize].
    4. The encoding of the ciphertext differs for encryption and decryption. During encryption, the ciphertext is Base64 encoded, during decryption it is UTF-8 encoded.
    5. The AAD (4th parameter in AeadParameters) are not taken into account during decryption. However, this has no effect as empty AADs are used during encryption.

    Each of these points (regarding the 1st point an incorrect key of valid length) result in a failed authentication with a corresponding exception.

    The minimum changes required for decryption to work are:

    public static string Testing_GCM_Decrypt2(string Ciphertext)
    {
        String iv = "00112233445566778899AABBCCDDEEFF00";
        byte[] byteIV = System.Text.Encoding.UTF8.GetBytes(iv);
        //
        byte[] nonce = new byte[8];
        Array.Copy(System.Text.Encoding.UTF8.GetBytes(iv), nonce, 8); // Fix 2: apply nonce from encryption
        //
        byte[] encryptedData = Convert.FromBase64String(Ciphertext); // Fix 4: apply encoding from encryption, Fix 3: use different variables for encrypted and decrypted data
        var add = string.Empty;
        byte[] associatedText = System.Text.Encoding.UTF8.GetBytes(add);
        //
        GcmBlockCipher cipher = new GcmBlockCipher(new AesEngine());
        cipher.Init(false, new AeadParameters(new KeyParameter(key), 128, nonce, associatedText)); // Fix 1: apply key from encryption, Fix 5: consider AAD
        //
        int outputSize = cipher.GetOutputSize(encryptedData.Length); // Fix 3
        byte[] TextData = new byte[outputSize];
        //
        int result = cipher.ProcessBytes(encryptedData, 0, encryptedData.Length, TextData, 0); // Fix 3
        cipher.DoFinal(TextData, result);
        var pln = TextData;
        return System.Text.Encoding.UTF8.GetString(pln);
    }
    

    However, your implementation is inefficient and has serious security vulnerabilities (in the first place the reuse of key/nonce pairs). The following should be considered for AES/GCM:

    • AES is a symmetric cryptographic algorithm, i.e. the same key must be used for decryption as for encryption.
    • When decrypting, the same nonce must be used as for encryption. The recommended nonce length for GCM is 12 bytes. As the reuse of key/nonce pairs is a serious security vulnerability in AES-GCM, a random nonce is usually generated for each encryption (when the key is fixed). This is not secret and is passed to the decrypting side along with the ciphertext (usually concatenated: nonce|ciphertext).
    • The authentication tag is automatically appended to the ciphertext by C#/BouncyCastle. For security reasons, the maximum length of 128 bits should generally be used. With concatenation of the nonce, the data is therefore nonce|ciphertext|tag.
    • When decrypting, the same AAD must be used as for encryption.
    • All encodings during encryption and decryption must be consistent, e.g. if the ciphertext is Base64 encoded after encryption, it must be Base64 decoded before decryption.

    A possible implementation is:

    using Org.BouncyCastle.Crypto;
    using Org.BouncyCastle.Crypto.Engines;
    using Org.BouncyCastle.Crypto.Modes;
    using Org.BouncyCastle.Crypto.Parameters;
    using Org.BouncyCastle.Security;
    using System;
    using System.Text;
    
    ...
    
    public static string Encrypt(byte[] key, byte[] plaintext, byte[] aad)
    {
        // create random nonce
        byte[] nonce = new byte[12];
        SecureRandom secureRandom = new SecureRandom();
        secureRandom.NextBytes(nonce);
    
        // encrypt
        GcmBlockCipher gcmBlockCipher = new GcmBlockCipher(new AesEngine());
        gcmBlockCipher.Init(true, new AeadParameters(new KeyParameter(key), 128, nonce, aad));
        int outputSizeCiphertextTag = gcmBlockCipher.GetOutputSize(plaintext.Length);
        byte[] nonceCiphertextTag = new byte[nonce.Length + outputSizeCiphertextTag]; // buffer for nonce|ciphertext|tag
        Buffer.BlockCopy(nonce, 0, nonceCiphertextTag, 0, nonce.Length);
        int processedBytes = gcmBlockCipher.ProcessBytes(plaintext, 0, plaintext.Length, nonceCiphertextTag, nonce.Length);
        gcmBlockCipher.DoFinal(nonceCiphertextTag, nonce.Length + processedBytes);
        
        // Base64 encode encrypted data
        return Convert.ToBase64String(nonceCiphertextTag);
    }
    
    public static byte[] Decrypt(byte[] key, string nonceCiphertextTagB64, byte[] aad)
    {
        // Base64 decode encrypted data
        byte[] nonceCiphertextTag = Convert.FromBase64String(nonceCiphertextTagB64);
        
        // separate nonce and ciphertextTag
        int nonceSize = 12;
        byte[] nonce = nonceCiphertextTag[..nonceSize];
        byte[] ciphertextTag = nonceCiphertextTag[nonceSize..];
    
        // decrypt
        GcmBlockCipher gcmBlockCipher = new GcmBlockCipher(new AesEngine());
        gcmBlockCipher.Init(false, new AeadParameters(new KeyParameter(key), 128, nonce, aad));
        int outputSizeDecryptedData = gcmBlockCipher.GetOutputSize(ciphertextTag.Length);
        byte[] decryptedData = new byte[outputSizeDecryptedData];
        int processedBytes = gcmBlockCipher.ProcessBytes(ciphertextTag, 0, ciphertextTag.Length, decryptedData, 0);
        gcmBlockCipher.DoFinal(decryptedData, processedBytes);
    
        return decryptedData;
    }
    

    Use case:

    // create key
    CipherKeyGenerator cipherKeyGenerator = new CipherKeyGenerator();
    cipherKeyGenerator.Init(new KeyGenerationParameters(new SecureRandom(), 128));
    byte[] key = cipherKeyGenerator.GenerateKey();
    
    // encrypt/decrypt
    string plaintext = "The quick brown fox jumps over the lazy dog";
    string aad = "some additional authenticated data";
    string encryptedData = Encrypt(key, Encoding.UTF8.GetBytes(plaintext), Encoding.UTF8.GetBytes(aad));
    byte[] decryptedData = Decrypt(key, encryptedData, Encoding.UTF8.GetBytes(aad));
    Console.WriteLine(Encoding.UTF8.GetString(decryptedData)); // The quick brown fox jumps over the lazy dog
    

    Edit: .. is the range operator which is available as of C# 8. If the range operator is not available in your environment you can use Buffer.BlockCopy():

    byte[] nonceCiphertextTag = Encoding.UTF8.GetBytes("abcdefghijklmnopqrstuvwxyz");
    int nonceSize = 12;
    //byte[] nonce = nonceCiphertextTag[..nonceSize];
    //byte[] ciphertextTag = nonceCiphertextTag[nonceSize..];
    byte[] nonce = new byte[nonceSize];
    Buffer.BlockCopy(nonceCiphertextTag, 0, nonce, 0, nonce.Length);
    byte[] ciphertextTag = new byte[nonceCiphertextTag.Length - nonceSize];
    Buffer.BlockCopy(nonceCiphertextTag, nonce.Length, ciphertextTag, 0, ciphertextTag.Length);