AES GCM in .NET 8 (.csx script)

Well, sooner or later, I would need a way to encrypt things, a safe way, therefore here I leave the code for AES GCM string encryption with .NET 8.

I assume you already have .NET 8 installed as well as dotnet-script.

#!/usr/bin/env dotnet-script
/*
======================================================================
+ AES-GCM (Galois/Counter Mode)
  - Encrypts and authenticates the message to prevent data tampering.
  - Eliminates padding oracle attacks present in AES-CBC.

+ PBKDF2 (Password-Based Key Derivation Function 2)
  - Derives a secure 256-bit AES key from a user password.
  - Uses SHA-256 and 100,000 iterations to increase security.

+ Randomized Salt and IV
  - Ensures that even identical plaintexts will encrypt differently.

+ Authentication Tag (Message Integrity)
  - Prevents tampering and ensures ciphertext is valid.
======================================================================
*/

using System;
using System.Security.Cryptography;
using System.Text;

public static class AesGcmHelper
{
    // Constants for cryptographic sizes
    private const int KeySize = 32;  // AES-256 requires a 32-byte (256-bit) key
    private const int IvSize = 12;   // AES-GCM standard IV size is 12 bytes
    private const int TagSize = 16;  // Authentication tag size for AES-GCM is 16 bytes
    private const int SaltSize = 16; // Salt size for key derivation using PBKDF2

    /// <summary>
    /// Encrypts a plaintext string using AES-GCM encryption.
    /// </summary>
    /// <param name="plaintext">The text to be encrypted.</param>
    /// <param name="password">A password/key used to derive the encryption key.</param>
    /// <returns>A Base64-encoded string containing the encrypted data.</returns>
    public static string Encrypt(string plaintext, string password)
    {
        // Generate a random salt for key derivation
        byte[] salt = RandomNumberGenerator.GetBytes(SaltSize);

        // Derive a cryptographic key from the password and salt using PBKDF2
        byte[] key = DeriveKey(password, salt);

        // Generate a random IV (Nonce) for AES-GCM encryption
        byte[] iv = RandomNumberGenerator.GetBytes(IvSize);

        // Convert plaintext into a byte array
        byte[] plaintextBytes = Encoding.UTF8.GetBytes(plaintext);

        // Allocate arrays for ciphertext and authentication tag
        byte[] ciphertext = new byte[plaintextBytes.Length];
        byte[] tag = new byte[TagSize];

        // Perform AES-GCM encryption using the constructor that accepts the tag size
        using (var aes = new AesGcm(key, TagSize))
        {
            aes.Encrypt(iv, plaintextBytes, ciphertext, tag);
        }

        // Combine salt, IV, ciphertext, and tag into a single byte array for storage
        byte[] encryptedData = new byte[SaltSize + IvSize + ciphertext.Length + TagSize];
        Buffer.BlockCopy(salt, 0, encryptedData, 0, SaltSize);
        Buffer.BlockCopy(iv, 0, encryptedData, SaltSize, IvSize);
        Buffer.BlockCopy(ciphertext, 0, encryptedData, SaltSize + IvSize, ciphertext.Length);
        Buffer.BlockCopy(tag, 0, encryptedData, SaltSize + IvSize + ciphertext.Length, TagSize);

        // Convert to Base64 for easy storage and transport
        return Convert.ToBase64String(encryptedData);
    }

    /// <summary>
    /// Decrypts an AES-GCM encrypted string back to plaintext.
    /// </summary>
    /// <param name="encryptedText">The Base64-encoded encrypted string.</param>
    /// <param name="password">The same password used for encryption.</param>
    /// <returns>The decrypted plaintext string.</returns>
    public static string Decrypt(string encryptedText, string password)
    {
        // Convert the Base64-encoded input string back to a byte array
        byte[] encryptedData = Convert.FromBase64String(encryptedText);

        // Extract the salt, IV, ciphertext, and tag from the encrypted data
        byte[] salt = new byte[SaltSize];
        byte[] iv = new byte[IvSize];
        byte[] tag = new byte[TagSize];
        byte[] ciphertext = new byte[encryptedData.Length - SaltSize - IvSize - TagSize];

        Buffer.BlockCopy(encryptedData, 0, salt, 0, SaltSize);
        Buffer.BlockCopy(encryptedData, SaltSize, iv, 0, IvSize);
        Buffer.BlockCopy(encryptedData, SaltSize + IvSize, ciphertext, 0, ciphertext.Length);
        Buffer.BlockCopy(encryptedData, SaltSize + IvSize + ciphertext.Length, tag, 0, TagSize);

        // Derive the key using the same PBKDF2 function
        byte[] key = DeriveKey(password, salt);

        // Allocate an array to store the decrypted plaintext
        byte[] plaintextBytes = new byte[ciphertext.Length];

        // Perform AES-GCM decryption using the constructor that accepts the tag size
        using (var aes = new AesGcm(key, TagSize))
        {
            aes.Decrypt(iv, ciphertext, tag, plaintextBytes);
        }

        // Convert the decrypted byte array back to a UTF-8 string
        return Encoding.UTF8.GetString(plaintextBytes);
    }

    /// <summary>
    /// Derives a cryptographic key from a password using PBKDF2 (Password-Based Key Derivation Function 2).
    /// </summary>
    /// <param name="password">The user-provided password.</param>
    /// <param name="salt">A randomly generated salt to add randomness.</param>
    /// <returns>A derived 256-bit key suitable for AES encryption.</returns>
    private static byte[] DeriveKey(string password, byte[] salt)
    {
        // Use PBKDF2 with SHA-256, 100,000 iterations for key strengthening
        using (var pbkdf2 = new Rfc2898DeriveBytes(password, salt, 100000, HashAlgorithmName.SHA256))
        {
            return pbkdf2.GetBytes(KeySize); // Generate a 32-byte (256-bit) key
        }
    }
}

// Sample usage:
string secretMessage = "Hello, AES-GCM!";
string password = "blog.nodejslab.com";

string encrypted = AesGcmHelper.Encrypt(secretMessage, password);
Console.WriteLine($"Encrypted: {encrypted}");

string decrypted = AesGcmHelper.Decrypt(encrypted, password);
Console.WriteLine($"Decrypted: {decrypted}");

Here is an exmaple output:

~/Development/dotNET/AES GCM > dotnet-script AES-GCM.csx
Encrypted: JpCCYa60vXz0LE2rUQq+xp6oVddm9WQoJIpA9bZlkK1aCSLSOk9HXOofu3coL2U/wIEU7pe8o9/swWo=
Decrypted: Hello, AES-GCM!