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 →