Skip to content

PGP Encrypt & Decrypt — C# source

Encrypt or decrypt messages with PGP public/private keys. Powered by OpenPGP.js, runs entirely in your browser.

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

// PGP Encrypt & Decrypt — read OpenPGP key metadata, encrypt (optionally
// signing), decrypt.
// C# 12 / .NET 8 — ported from src/lib/pgp-encrypt.ts (the canonical
// TypeScript implementation). Display source for CosmoDev's polyglot pages.
//
// .NET has no OpenPGP implementation in the BCL, so this port drives the
// `gpg` binary (the same engine the C port reaches through GPGME and the
// native counterpart to the TS reference's openpgp.js). Three entry points,
// matching the TS public API one-for-one:
//   ReadKeyInfo — user ID, uppercase fingerprint, algorithm, creation date,
//                 expiry (null when the key never expires), and whether the
//                 armor holds a private key.
//   PgpEncrypt  — ASCII-armored PGP message for a recipient's public key,
//                 optionally signed with the sender's private key.
//   PgpDecrypt  — plaintext from an armored message + private key.
//
// Armored key material is imported into a fresh 0700 GNUPGHOME under a temp
// directory that is deleted on the way out, so nothing is written to the
// user's real keyring — the C# equivalent of the TS island keeping every
// operation client-side.
//
// gpg is always spawned with an argument ARRAY (ProcessStartInfo.ArgumentList,
// never a command string), so no key, message or passphrase is ever parsed by
// a shell. Messages and armored keys travel via stdin/stdout, passphrases via
// --passphrase with --pinentry-mode loopback --batch.

using System.Diagnostics;
using System.Text.RegularExpressions;

/// <summary>Metadata extracted from one armored PGP key.</summary>
public sealed record PGPKeyInfo(
    string UserId,
    string Fingerprint,
    string Algorithm,
    /// <summary>ISO yyyy-MM-dd.</summary>
    string CreationDate,
    /// <summary>ISO yyyy-MM-dd, null when the key never expires.</summary>
    string? Expiry,
    bool IsPrivate);

public static class PgpEncrypt
{
    private static readonly Regex HexEscapeRegex = new(@"\\x([0-9a-fA-F]{2})", RegexOptions.Compiled);

    /// <summary>gpg's numeric public-key algorithm ids -> openpgp.js-style names.</summary>
    private static readonly IReadOnlyDictionary<int, string> AlgoNames = new Dictionary<int, string>
    {
        [1] = "rsa",
        [2] = "rsa",
        [3] = "rsa",
        [16] = "elgamal",
        [17] = "dsa",
        [18] = "ecdh",
        [19] = "ecdsa",
        [22] = "ed25519",
        [27] = "x25519",
    };

    /// <summary>
    /// A fresh 0700 GNUPGHOME under a temp directory, deleted (best-effort)
    /// on dispose. gpg refuses a world-readable homedir, and this way no key
    /// ever touches the user's real keyring.
    /// </summary>
    private sealed class TempGnuHome : IDisposable
    {
        public string Path { get; } = Path.Combine(Path.GetTempPath(), $"gnupg-{Guid.NewGuid():N}");

        public TempGnuHome()
        {
            Directory.CreateDirectory(Path);
            if (OperatingSystem.IsUnix())
            {
                new DirectoryInfo(Path).SetUnixFileMode(
                    System.IO.FileMode doesn_not_exist_placeholder);
            }
        }

        public void Dispose()
        {
            try
            {
                Directory.Delete(Path, recursive: true);
            }
            catch (IOException)
            {
                // Lock/Sockets leftovers — the OS temp cleaner owns the directory now.
            }
        }
    }
}

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 →