Skip to content

Text Encryptor — C# source

Encrypt or decrypt text with a password using AES-256-GCM. Share the ciphertext safely - only someone with the password can read it.

This is the C# implementation — the same logic the interactive tool runs, in a shareable, citable form.

// Text Encryptor — password-based text encryption (AES-256-GCM + PBKDF2).
//
// Language: C# (.NET 8+, standard library only)
// Source:   CosmoDev polyglot showcase port of the Text Encryptor tool, ported
//           from src/lib/text-encryptor.ts (the canonical TypeScript
//           implementation).
// License:  display source — part of CosmoDev's polyglot tool pages.
//
// Format: PBKDF2 (SHA-256, 100k iterations, 16-byte random salt) derives an
// AES-256 key from the password; AES-GCM encrypts with a 12-byte random IV.
// The Base64 output packs salt + IV + ciphertext (+ GCM tag), so every
// encryption is unique and self-contained - decrypt needs only the string and
// the password.

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

public static partial class TextEncryptor
{
    private const int Pbkdf2Iterations = 100_000;
    private const int SaltBytes = 16;
    private const int IvBytes = 12;

    /// <summary>Smallest valid packed payload: salt + IV + one AES-GCM block (tag).</summary>
    private const int MinBytes = SaltBytes + IvBytes + 16;

    private static readonly Regex Base64Shape =
        new(@"^[A-Za-z0-9+/]+={0,2}$", RegexOptions.Compiled);

    /// <summary>
    /// Base64 → bytes. Accepts surrounding whitespace; throws on any other
    /// non-alphabet character or a wrong length.
    /// </summary>
    private static byte[] Base64ToBytes(string base64)
    {
        var clean = Regex.Replace(base64, @"\s+", "");
        if (clean.Length == 0)
        {
            throw new FormatException("Invalid Base64: input is empty");
        }
        if (clean.Length % 4 != 0)
        {
            throw new FormatException("Invalid Base64: length must be a multiple of 4");
        }
        if (!Base64Shape.IsMatch(clean))
        {
            throw new FormatException("Invalid Base64: unexpected character");
        }
        return Convert.FromBase64String(clean);
    }

    /// <summary>PBKDF2-SHA256 (100k iterations) → a 256-bit AES key.</summary>
    private static byte[] DeriveKey(string password, byte[] salt) =>
        Rfc2898DeriveBytes.Pbkdf2(
            Encoding.UTF8.GetBytes(password), salt, Pbkdf2Iterations, HashAlgorithmName.SHA256, 32);

    /// <summary>
    /// Encrypt UTF-8 <paramref name="plaintext"/> under
    /// <paramref name="password"/> and return the Base64 string packing
    /// salt + IV + AES-256-GCM ciphertext. Throws if either input is empty.
    /// </summary>
    public static string EncryptText(string plaintext, string password)
    {
        if (plaintext == string.Empty) throw new ArgumentException("Nothing to encrypt: input is empty");
        if (password == string.Empty) throw new ArgumentException("Password must not be empty");

        var salt = new byte[SaltBytes];
        var iv = new byte[IvBytes];
        RandomNumberGenerator.Fill(salt);
        RandomNumberGenerator.Fill(iv);

        var plain = Encoding.UTF8.GetBytes(plaintext);
        var key = DeriveKey(password, salt);
        var ciphertext = new byte[plain.Length + 16];
        try
        {
            using var aes = new AesGcm(key, 16);
            aes.Encrypt(iv, plain, ciphertext[..plain.Length], ciphertext[plain.Length..]);
        }
        finally
        {
            CryptographicOperations.ZeroMemory(key);
        }

        var packed = new byte[SaltBytes + IvBytes + ciphertext.Length];
        Buffer.BlockCopy(salt, 0, packed, 0, SaltBytes);
        Buffer.BlockCopy(iv, 0, packed, SaltBytes, IvBytes);
        Buffer.BlockCopy(ciphertext, 0, packed, SaltBytes + IvBytes, ciphertext.Length);
        return Convert.ToBase64String(packed);
    }

    /// <summary>
    /// Decrypt a Base64 string produced by <see cref="EncryptText"/> back to
    /// plaintext. Throws on empty input/password, invalid Base64, a payload
    /// too short to be one of ours, or a GCM authentication failure (wrong
    /// password / tampered data).
    /// </summary>
    public static string DecryptText(string ciphertext, string password)
    {
        if (string.IsNullOrWhiteSpace(ciphertext)) throw new ArgumentException("Nothing to decrypt: input is empty");
        if (password == string.Empty) throw new ArgumentException("Password must not be empty");

        var packed = Base64ToBytes(ciphertext.Trim());
        if (packed.Length < MinBytes)
        {
            throw new FormatException("Ciphertext too short - not a valid salt + IV + AES-GCM payload");
        }

        var salt = packed[..SaltBytes];
        var iv = packed[SaltBytes..(SaltBytes + IvBytes)];
        var data = packed[(SaltBytes + IvBytes)..];
        var plaintext = new byte[data.Length - 16];

        var key = DeriveKey(password, salt);
        try
        {
            using var aes = new AesGcm(key, 16);
            aes.Decrypt(iv, data[..plaintext.Length], data[plaintext.Length..], plaintext);
        }
        catch (CryptographicException)
        {
            throw new CryptographicException("Decryption failed - wrong password or corrupted ciphertext");
        }
        finally
        {
            CryptographicOperations.ZeroMemory(key);
        }
        return Encoding.UTF8.GetString(plaintext);
    }
}

Also available in 8 other languages

Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →