Age File Encryption — C# source
Encrypt and decrypt files with age — a modern, simple alternative to PGP. Password-based encryption runs entirely in your browser.
This is the C# implementation — the same logic the interactive tool runs, in a shareable, citable form.
// Age File Encryption — passphrase-based file encryption in the spirit of the
// age format (age-encryption.org/v1).
//
// Language: C# (.NET 8+, standard library only)
// Source: CosmoDev polyglot showcase port of the Age Encryption tool, ported
// from src/lib/age-encryption.ts (the canonical TypeScript
// implementation).
// License: display source — part of CosmoDev's polyglot tool pages.
//
// age's passphrase mode wraps a file key with an scrypt-derived key; this
// implementation delivers the same security properties with .NET primitives:
// PBKDF2-SHA256 (100k iterations) key stretching, a fresh random salt per
// encryption, and AES-256-GCM authenticated encryption.
//
// Wire format (age-style header + body):
// "cosmodev-age-v1" (15 B ASCII magic) || salt (16 B) || IV (12 B)
// || AES-256-GCM ciphertext + tag (16 B)
using System;
using System.Security.Cryptography;
using System.Text;
public static class AgeEncryption
{
public const string AgeHeader = "cosmodev-age-v1";
public const int SaltLength = 16;
public const int IvLength = 12;
public const int Iterations = 100_000;
private static readonly byte[] HeaderBytes = Encoding.ASCII.GetBytes(AgeHeader);
private static readonly int HeaderLength = HeaderBytes.Length; // 15
// GCM appends a 16-byte auth tag; the smallest possible encrypted payload
// is therefore header + salt + IV + tag = 59 bytes.
private const int TagLength = 16;
private static readonly int Overhead = HeaderLength + SaltLength + IvLength + TagLength;
/// <summary>True when <paramref name="data"/> starts with the cosmodev-age-v1 magic header.</summary>
public static bool IsAgeEncrypted(byte[] data)
{
if (data.Length < HeaderLength) return false;
for (var i = 0; i < HeaderLength; i++)
{
if (data[i] != HeaderBytes[i]) return false;
}
return true;
}
/// <summary>PBKDF2-SHA256 (100k iterations) → a 256-bit AES key.</summary>
private static byte[] DeriveKey(string passphrase, byte[] salt) =>
Rfc2898DeriveBytes.Pbkdf2(
Encoding.UTF8.GetBytes(passphrase), salt, Iterations, HashAlgorithmName.SHA256, 32);
/// <summary>
/// Encrypt <paramref name="data"/> under <paramref name="passphrase"/>.
/// Returns header || salt || IV || ciphertext+tag.
/// </summary>
public static byte[] AgeEncrypt(byte[] data, string passphrase)
{
if (string.IsNullOrEmpty(passphrase))
{
throw new ArgumentException("Passphrase must not be empty.");
}
if (data.Length == 0)
{
throw new ArgumentException("Input data is empty - nothing to encrypt.");
}
var salt = new byte[SaltLength];
var iv = new byte[IvLength];
RandomNumberGenerator.Fill(salt);
RandomNumberGenerator.Fill(iv);
var key = DeriveKey(passphrase, salt);
var ciphertext = new byte[data.Length + TagLength];
try
{
using var aes = new AesGcm(key, TagLength);
aes.Encrypt(iv, data, ciphertext[..data.Length], ciphertext[data.Length..]);
}
finally
{
CryptographicOperations.ZeroMemory(key);
}
var output = new byte[HeaderLength + SaltLength + IvLength + ciphertext.Length];
Buffer.BlockCopy(HeaderBytes, 0, output, 0, HeaderLength);
Buffer.BlockCopy(salt, 0, output, HeaderLength, SaltLength);
Buffer.BlockCopy(iv, 0, output, HeaderLength + SaltLength, IvLength);
Buffer.BlockCopy(ciphertext, 0, output, HeaderLength + SaltLength + IvLength, ciphertext.Length);
return output;
}
/// <summary>
/// Decrypt a payload produced by <see cref="AgeEncrypt"/>. Throws when the
/// data lacks the cosmodev-age-v1 header, the passphrase is wrong, or the
/// payload was corrupted/tampered (GCM auth-tag failure).
/// </summary>
public static byte[] AgeDecrypt(byte[] data, string passphrase)
{
if (string.IsNullOrEmpty(passphrase))
{
throw new ArgumentException("Passphrase must not be empty.");
}
if (!IsAgeEncrypted(data))
{
throw new FormatException("Not an age-encrypted file (missing cosmodev-age-v1 header).");
}
if (data.Length < Overhead)
{
throw new FormatException(
$"Input is too short to be an age-encrypted file (needs at least {Overhead} bytes: header + salt + IV + auth tag).");
}
var salt = data[HeaderLength..(HeaderLength + SaltLength)];
var iv = data[(HeaderLength + SaltLength)..(HeaderLength + SaltLength + IvLength)];
var ciphertext = data[(HeaderLength + SaltLength + IvLength)..];
var plaintext = new byte[ciphertext.Length - TagLength];
var key = DeriveKey(passphrase, salt);
try
{
using var aes = new AesGcm(key, TagLength);
aes.Decrypt(iv, ciphertext[..plaintext.Length], ciphertext[plaintext.Length..], plaintext);
}
catch (CryptographicException)
{
throw new CryptographicException("Decryption failed: wrong passphrase or corrupted file.");
}
finally
{
CryptographicOperations.ZeroMemory(key);
}
return 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 →