Skip to content

WireGuard Key Generator — C# source

Generate Curve25519 key pairs for WireGuard VPN configuration. Derives the public key from a clamped private key with a pure-BigInt RFC 7748 Montgomery ladder, optionally generates a pre-shared key, and renders a ready-to-edit wg-quick config template. Everything runs 100% client-side - keys never leave your browser.

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

// WireGuard Key Generator — Curve25519 (RFC 7748) key generation.
// C# 12 / .NET 8 — ported from src/lib/wireguard-keygen.ts (the canonical
// TypeScript implementation). Display source for CosmoDev's polyglot pages.
//
// WireGuard uses Curve25519 (RFC 7748 X25519) for its key exchange:
//   - a private key is 32 random bytes, clamped per the Curve25519 rules
//     (key[0] &= 248; key[31] &= 127; key[31] |= 64)
//   - the public key is that scalar multiplied by the curve's base point 9,
//     computed with a pure BigInteger Montgomery ladder over GF(2^255 - 19)
//   - an optional pre-shared key is 32 random bytes, used as-is (no clamping)
//
// Every key is serialized as standard Base64 with padding — 44 characters for
// 32 bytes — which is exactly the format WireGuard config files expect.
//
// Randomness comes from RandomNumberGenerator and the curve arithmetic is
// deterministic BigInteger math (System.Numerics.BigInteger never wraps, so
// the TS reference's BigInt reasoning maps one-for-one).

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

/// <summary>A WireGuard key pair, both keys standard-padded Base64.</summary>
public sealed record WireGuardKeys(string PrivateKey, string PublicKey);

public static class WireGuardKeygen
{
    /// <summary>Length of every WireGuard key, in bytes.</summary>
    public const int KeyLength = 32;

    // Curve25519 domain parameters: y^2 = x^3 + 486662x^2 + x over GF(2^255 - 19).
    private static readonly BigInteger P = BigInteger.Pow(2, 255) - 19;
    private static readonly BigInteger A24 = 121665; // (486662 - 2) / 4

    private static readonly byte[] BasePoint = CreateBasePoint();

    private static byte[] CreateBasePoint()
    {
        var u = new byte[KeyLength]; // u = 9, little-endian
        u[0] = 9;
        return u;
    }

    private const string B64Alphabet =
        "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

    // ---------------------------------------------------------------------------
    // Base64 codec (standard alphabet, always padded — the WireGuard format)
    // ---------------------------------------------------------------------------

    /// <summary>Encode bytes as standard Base64 with '=' padding (32 bytes → 44 chars).</summary>
    public static string BytesToBase64(byte[] bytes)
    {
        var sb = new StringBuilder();
        for (var i = 0; i < bytes.Length; i += 3)
        {
            var b0 = bytes[i];
            var b1 = i + 1 < bytes.Length ? bytes[i + 1] : 0;
            var b2 = i + 2 < bytes.Length ? bytes[i + 2] : 0;
            sb.Append(B64Alphabet[b0 >> 2]);
            sb.Append(B64Alphabet[((b0 & 0x03) << 4) | (b1 >> 4)]);
            sb.Append(i + 1 < bytes.Length ? B64Alphabet[((b1 & 0x0f) << 2) | (b2 >> 6)] : '=');
            sb.Append(i + 2 < bytes.Length ? B64Alphabet[b2 & 0x3f] : '=');
        }
        return sb.ToString();
    }

    /// <summary>Decode standard Base64 (with padding). Throws on invalid input.</summary>
    public static byte[] Base64ToBytes(string b64)
    {
        var s = b64.Trim();
        if (s.Length == 0 || s.Length % 4 != 0)
        {
            throw new FormatException("Invalid Base64: length must be a non-zero multiple of 4");
        }
        var pad = s.EndsWith("==") ? 2 : s.EndsWith('=') ? 1 : 0;
        var data = s[..(s.Length - pad)];
        var output = new byte[s.Length / 4 * 3 - pad];
        var buffer = 0;
        var bits = 0;
        var o = 0;
        foreach (var ch in data)
        {
            var v = B64Alphabet.IndexOf(ch);
            if (v == -1) throw new FormatException($"Invalid Base64 character: \"{ch}\"");
            buffer = (buffer << 6) | v;
            bits += 6;
            if (bits >= 8)
            {
                bits -= 8;
                output[o++] = (byte)((buffer >> bits) & 0xff);
            }
        }
        return output;
    }

    // ---------------------------------------------------------------------------
    // Curve25519 scalar multiplication (RFC 7748 Montgomery ladder)
    // ---------------------------------------------------------------------------

    /// <summary>Reduce <paramref name="a"/> into the canonical range [0, P).</summary>
    private static BigInteger Mod(BigInteger a)
    {
        var r = a % P;
        return r.Sign >= 0 ? r : r + P;
    }

    /// <summary>base^exponent mod P via square-and-multiply (used for field inversion).</summary>
    private static BigInteger PowMod(BigInteger @base, BigInteger exponent)
    {
        var result = BigInteger.One;
        var b = Mod(@base);
        var e = exponent;
        while (e.Sign > 0)
        {
            if (!e.IsEven) result = Mod(result * b);
            b = Mod(b * b);
            e >>= 1;
        }
        return result;
    }

    /// <summary>Clamp 32 bytes into a valid Curve25519 scalar (RFC 7748 §5). Returns a copy.</summary>
    public static byte[] ClampPrivateKey(byte[] key)
    {
        if (key.Length != KeyLength)
        {
            throw new ArgumentException($"Private key must be {KeyLength} bytes, got {key.Length}");
        }
        var output = (byte[])key.Clone();
        output[0] &= 248; // clear the low 3 bits → multiple of the cofactor
        output[31] &= 127; // clear the high bit
        output[31] |= 64; // force bit 254 → the ladder always sees a 255-bit scalar
        return output;
    }

    private static BigInteger DecodeLittleEndian(byte[] bytes)
    {
        BigInteger n = 0;
        for (var i = bytes.Length - 1; i >= 0; i--)
        {
            n = (n << 8) | bytes[i];
        }
        return n;
    }

    private static byte[] EncodeLittleEndian(BigInteger n, int length = KeyLength)
    {
        var output = new byte[length];
        for (var i = 0; i < length; i++)
        {
            output[i] = (byte)(n & 0xff);
            n >>= 8;
        }
        return output;
    }

    /// <summary>
    /// X25519 scalar multiplication scalar · u — the RFC 7748 Montgomery ladder
    /// in plain BigInteger arithmetic. Deterministic and dependency-free. The
    /// scalar is clamped internally (an unclamped input yields the same result
    /// as its clamped form, exactly like every X25519 implementation).
    /// </summary>
    public static byte[] Curve25519(byte[] scalar, byte[] u)
    {
        if (scalar.Length != KeyLength)
        {
            throw new ArgumentException($"Scalar must be {KeyLength} bytes, got {scalar.Length}");
        }
        if (u.Length != KeyLength)
        {
            throw new ArgumentException($"u-coordinate must be {KeyLength} bytes, got {u.Length}");
        }
        var k = DecodeLittleEndian(ClampPrivateKey(scalar));
        // Mask the most significant bit of the u-coordinate per RFC 7748 §5.
        var x1 = DecodeLittleEndian(u) & ((BigInteger.One << 255) - 1);

        var x2 = BigInteger.One;
        var z2 = BigInteger.Zero;
        var x3 = x1;
        var z3 = BigInteger.One;
        var swap = 0;
        for (var t = 254; t >= 0; t--)
        {
            var bit = (int)((k >> t) & 1);
            swap ^= bit;
            if (swap == 1)
            {
                (x2, x3) = (x3, x2);
                (z2, z3) = (z3, z2);
            }
            swap = bit;

            var a = Mod(x2 + z2);
            var aa = Mod(a * a);
            var b = Mod(x2 - z2);
            var bb = Mod(b * b);
            var e = Mod(aa - bb);
            var c = Mod(x3 + z3);
            var d = Mod(x3 - z3);
            var da = Mod(d * a);
            var cb = Mod(c * b);
            var sum = Mod(da + cb);
            var diff = Mod(da - cb);
            x3 = Mod(sum * sum);
            z3 = Mod(x1 * Mod(diff * diff));
            x2 = Mod(aa * bb);
            z2 = Mod(e * Mod(aa + A24 * e));
        }
        // No final cswap: the loop leaves swap = k_0, and clamping clears bit 0,
        // so swap is provably 0 here for every input this function accepts.
        // x2 / z2 via z2^(P-2) (Fermat): the affine u-coordinate result.
        return EncodeLittleEndian(Mod(x2 * PowMod(z2, P - 2)));
    }

    // ---------------------------------------------------------------------------
    // Key generation
    // ---------------------------------------------------------------------------

    private static byte[] RandomBytes(int length) => RandomNumberGenerator.GetBytes(length);

    /// <summary>A fresh private key: 32 CSPRNG bytes, clamped, Base64.</summary>
    public static string GeneratePrivateKey() => BytesToBase64(ClampPrivateKey(RandomBytes(KeyLength)));

    /// <summary>A fresh pre-shared key: 32 CSPRNG bytes, Base64 — used as-is, never clamped.</summary>
    public static string GeneratePresharedKey() => BytesToBase64(RandomBytes(KeyLength));

    /// <summary>
    /// Derive the WireGuard public key that pairs with a Base64 private key
    /// (Curve25519 scalar multiplication of the base point).
    /// </summary>
    public static string PrivateKeyToPublic(string privateKeyBase64)
    {
        var priv = Base64ToBytes(privateKeyBase64);
        if (priv.Length != KeyLength)
        {
            throw new FormatException($"Invalid private key: expected {KeyLength} bytes, got {priv.Length}");
        }
        return BytesToBase64(Curve25519(priv, BasePoint));
    }

    /// <summary>A fresh WireGuard key pair (private + matching public key, both Base64).</summary>
    public static WireGuardKeys GenerateWireGuardKeys()
    {
        var privateKey = GeneratePrivateKey();
        var publicKey = PrivateKeyToPublic(privateKey);
        return new WireGuardKeys(privateKey, publicKey);
    }

    // ---------------------------------------------------------------------------
    // Config template
    // ---------------------------------------------------------------------------

    /// <summary>
    /// Render a wg-quick config template around a key pair. The peer's public
    /// key, endpoint, and your tunnel address depend on the other side, so they
    /// stay as placeholders. A PresharedKey line is included only when
    /// <paramref name="psk"/> is given (it must be present on BOTH sides of the
    /// tunnel).
    /// </summary>
    public static string FormatConfig(WireGuardKeys keys, string? psk = null)
    {
        var lines = new List<string>
        {
            "[Interface]",
            "# Your side — keep PrivateKey secret, share PublicKey with the peer",
            $"PrivateKey = {keys.PrivateKey}",
            $"PublicKey = {keys.PublicKey}",
            "# Tunnel address assigned by your server (plus optional tunnel DNS)",
            "Address = 10.0.0.2/32",
            "# DNS = 1.1.1.1",
            "",
            "[Peer]",
            "# The other side's public key",
            "PublicKey = <PEER_PUBLIC_KEY>",
        };
        if (!string.IsNullOrEmpty(psk))
        {
            lines.Add("# Optional pre-shared key — the same value must be set on BOTH sides");
            lines.Add($"PresharedKey = {psk}");
        }
        lines.Add("# Route everything through the tunnel (or scope it, e.g. 10.0.0.0/24)");
        lines.Add("AllowedIPs = 0.0.0.0/0, ::/0");
        lines.Add("# The peer's public address and port");
        lines.Add("Endpoint = vpn.example.com:51820");
        lines.Add("PersistentKeepalive = 25");
        return string.Join('\n', lines);
    }
}

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 →