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!