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 X25519) key generation.
//
// Language: C++17 (standard library + OpenSSL BN / RAND)
// Ported from src/lib/wireguard-keygen.ts (the canonical TypeScript
// implementation, which computes the ladder in BigInt arithmetic).
// display source — part of CosmoDev's polyglot tool pages.
//
// WireGuard uses Curve25519 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 the RFC 7748 Montgomery ladder over GF(2^255 - 19) — here
// with OpenSSL BIGNUMs standing in for the TS reference's BigInts
// - 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 — exactly the format WireGuard config files expect. Randomness
// comes from the OpenSSL CSPRNG, so the whole module runs locally with no
// network and no dependencies beyond libcrypto.
//
// Build: c++ -std=c++17 wireguard-keygen.cpp -lcrypto
#include <cstdint>
#include <memory>
#include <stdexcept>
#include <string>
#include <vector>
#include <openssl/bn.h>
#include <openssl/rand.h>
namespace wireguard {
using Bytes = std::vector<uint8_t>;
/** Length of every WireGuard key, in bytes. */
constexpr size_t KEY_LENGTH = 32;
const char* BASE64_ALPHABET =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
// ── Base64 codec (standard alphabet, always padded — the WireGuard format) ──
/** Encode bytes as standard Base64 with `=` padding (32 bytes → 44 chars). */
std::string bytesToBase64(const Bytes& bytes) {
std::string out;
out.reserve((bytes.size() + 2) / 3 * 4);
for (size_t i = 0; i < bytes.size(); i += 3) {
const uint8_t b0 = bytes[i];
const uint8_t b1 = i + 1 < bytes.size() ? bytes[i + 1] : 0;
const uint8_t b2 = i + 2 < bytes.size() ? bytes[i + 2] : 0;
out += BASE64_ALPHABET[b0 >> 2];
out += BASE64_ALPHABET[((b0 & 0x03) << 4) | (b1 >> 4)];
out += i + 1 < bytes.size() ? BASE64_ALPHABET[((b1 & 0x0f) << 2) | (b2 >> 6)] : '=';
out += i + 2 < bytes.size() ? BASE64_ALPHABET[b2 & 0x3f] : '=';
}
return out;
}
/** Decode standard Base64 (with padding). Throws on invalid input. */
Bytes base64ToBytes(const std::string& b64) {
auto valueOf = [&](char c) -> int {
for (int i = 0; BASE64_ALPHABET[i] != '\0'; i++) {
if (BASE64_ALPHABET[i] == c) return i;
}
return -1;
};
// Trim surrounding whitespace.
size_t begin = 0;
size_t end = b64.size();
while (begin < end && (b64[begin] == ' ' || b64[begin] == '\t' || b64[begin] == '\n' ||
b64[begin] == '\r')) {
begin++;
}
while (end > begin && (b64[end - 1] == ' ' || b64[end - 1] == '\t' || b64[end - 1] == '\n' ||
b64[end - 1] == '\r')) {
end--;
}
const std::string s = b64.substr(begin, end - begin);
if (s.empty() || s.size() % 4 != 0) {
throw std::runtime_error("Invalid Base64: length must be a non-zero multiple of 4");
}
const int pad = s.ends_with("==") ? 2 : s.ends_with("=") ? 1 : 0;
Bytes out((s.size() / 4) * 3 - pad);
uint32_t buffer = 0;
int bits = 0;
size_t o = 0;
for (size_t i = 0; i < s.size() - pad; i++) {
const int v = valueOf(s[i]);
if (v < 0) {
throw std::runtime_error(std::string("Invalid Base64 character: \"") + s[i] + "\"");
}
buffer = (buffer << 6) | static_cast<uint32_t>(v);
bits += 6;
if (bits >= 8) {
bits -= 8;
out[o++] = static_cast<uint8_t>((buffer >> bits) & 0xff);
}
}
return out;
}
// ── OpenSSL BIGNUM plumbing ────────────────────────────────────────────────
struct BNDeleter {
void operator()(BIGNUM* bn) const { BN_free(bn); }
void operator()(BN_CTX* ctx) const { BN_CTX_free(ctx); }
};
using BigNum = std::unique_ptr<BIGNUM, BNDeleter>;
using BigContext = std::unique_ptr<BN_CTX, BNDeleter>;
/** Shared BN_CTX for the field arithmetic below. */
static BN_CTX* bnCtx() {
static BigContext ctx(BN_CTX_new());
if (!ctx) throw std::runtime_error("BN_CTX_new failed.");
return ctx.get();
}
static BigNum fromHex(const char* hex) {
BIGNUM* bn = nullptr;
if (BN_hex2bn(&bn, hex) == 0) throw std::runtime_error("BN_hex2bn failed.");
return BigNum(bn);
}
/** Curve25519 prime: 2^255 - 19, and A24 = (486662 - 2) / 4 = 121665. */
static const BigNum& modulus() {
static const BigNum P = fromHex("7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffed");
return P;
}
static const BigNum& a24() {
static const BigNum A24 = fromHex("1db4"); // 121665
return A24;
}
/** r = a + b mod P */
static BigNum add(const BIGNUM* a, const BIGNUM* b) {
BigNum r(BN_new());
if (!r || BN_mod_add(r.get(), a, b, modulus().get(), bnCtx()) != 1) {
throw std::runtime_error("Curve25519 field addition failed.");
}
return r;
}
/** r = a - b mod P */
static BigNum sub(const BIGNUM* a, const BIGNUM* b) {
BigNum r(BN_new());
if (!r || BN_mod_sub(r.get(), a, b, modulus().get(), bnCtx()) != 1) {
throw std::runtime_error("Curve25519 field subtraction failed.");
}
return r;
}
/** r = a * b mod P */
static BigNum mul(const BIGNUM* a, const BIGNUM* b) {
BigNum r(BN_new());
if (!r || BN_mod_mul(r.get(), a, b, modulus().get(), bnCtx()) != 1) {
throw std::runtime_error("Curve25519 field multiplication failed.");
}
return r;
}
/** base^exponent mod P via square-and-multiply (used for field inversion). */
static BigNum powMod(const BIGNUM* base, const BIGNUM* exponent) {
BigContext ctx(BN_CTX_new());
BigNum result(BN_new());
if (!ctx || !result || BN_one(result.get()) != 1) {
throw std::runtime_error("Curve25519 field exponentiation failed.");
}
BigNum b(BN_new());
if (!b || BN_copy(b.get(), base) == nullptr ||
BN_mod(b.get(), b.get(), modulus().get(), ctx.get()) != 1) {
throw std::runtime_error("Curve25519 field exponentiation failed.");
}
const int bits = BN_num_bits(exponent);
for (int i = 0; i < bits; i++) {
if (BN_is_bit_set(exponent, i)) {
if (BN_mod_mul(result.get(), result.get(), b.get(), modulus().get(), ctx.get()) != 1) {
throw std::runtime_error("Curve25519 field exponentiation failed.");
}
}
if (BN_mod_mul(b.get(), b.get(), b.get(), modulus().get(), ctx.get()) != 1) {
throw std::runtime_error("Curve25519 field exponentiation failed.");
}
}
return result;
}
// ── Curve25519 scalar multiplication (RFC 7748 Montgomery ladder) ──────────
/** Clamp 32 bytes into a valid Curve25519 scalar (RFC 7748 §5). Returns a copy. */
Bytes clampPrivateKey(const Bytes& key) {
if (key.size() != KEY_LENGTH) {
throw std::runtime_error("Private key must be " + std::to_string(KEY_LENGTH) +
" bytes, got " + std::to_string(key.size()));
}
Bytes out = key;
out[0] &= 248; // clear the low 3 bits → multiple of the cofactor
out[31] &= 127; // clear the high bit
out[31] |= 64; // force bit 254 → the ladder always sees a 255-bit scalar
return out;
}
/** Little-endian bytes → BIGNUM. */
static BigNum decodeLittleEndian(const Bytes& bytes) {
Bytes reversed(bytes.rbegin(), bytes.rend());
BigNum n(BN_bin2bn(reversed.data(), static_cast<int>(reversed.size()), nullptr));
if (!n) throw std::runtime_error("BN_bin2bn failed.");
return n;
}
/** BIGNUM → `length` little-endian bytes. */
static Bytes encodeLittleEndian(const BIGNUM* n, size_t length = KEY_LENGTH) {
Bytes bigEndian(BN_num_bytes(n));
BN_bn2bin(n, bigEndian.data());
Bytes out(length, 0);
for (size_t i = 0; i < length && i < bigEndian.size(); i++) {
out[i] = bigEndian[bigEndian.size() - 1 - i];
}
return out;
}
/** true when bit `i` of `n` is set. */
static bool bitSet(const BIGNUM* n, int i) { return BN_is_bit_set(n, i) != 0; }
/** Conditional swap of two BigNums when `swap` is true. */
static void cswap(BigNum& a, BigNum& b, bool swap) {
if (!swap) return;
std::swap(a, b);
}
/**
* X25519 scalar multiplication `scalar · u` — the RFC 7748 Montgomery ladder
* in OpenSSL BIGNUM arithmetic, mirroring the TS reference line for line.
* Deterministic and constant-time in structure (the cswap pattern). The scalar
* is clamped internally (an unclamped input yields the same result as its
* clamped form, exactly like every X25519 implementation).
*/
Bytes curve25519(const Bytes& scalar, const Bytes& u) {
if (scalar.size() != KEY_LENGTH) {
throw std::runtime_error("Scalar must be " + std::to_string(KEY_LENGTH) + " bytes, got " +
std::to_string(scalar.size()));
}
if (u.size() != KEY_LENGTH) {
throw std::runtime_error("u-coordinate must be " + std::to_string(KEY_LENGTH) +
" bytes, got " + std::to_string(u.size()));
}
const BigNum k = decodeLittleEndian(clampPrivateKey(scalar));
// Mask the most significant bit of the u-coordinate per RFC 7748 §5.
const Bytes masked = [&] {
Bytes m = u;
m[31] &= 127;
return m;
}();
const BigNum x1 = decodeLittleEndian(masked);
BigNum x2(BN_new()), z2(BN_new()), x3(BN_new()), z3(BN_new());
if (!x2 || !z2 || !x3 || !z3 || BN_one(x2.get()) != 1 || BN_zero(z2.get()) != 1 ||
BN_copy(x3.get(), x1.get()) == nullptr || BN_one(z3.get()) != 1) {
throw std::runtime_error("Montgomery ladder state init failed.");
}
bool swap = false;
for (int t = 254; t >= 0; t--) {
const bool bit = bitSet(k.get(), t);
swap ^= bit;
cswap(x2, x3, swap);
cswap(z2, z3, swap);
swap = bit;
const BigNum a = add(x2.get(), z2.get());
const BigNum aa = mul(a.get(), a.get());
const BigNum b = sub(x2.get(), z2.get());
const BigNum bb = mul(b.get(), b.get());
const BigNum e = sub(aa.get(), bb.get());
const BigNum c = add(x3.get(), z3.get());
const BigNum d = sub(x3.get(), z3.get());
const BigNum da = mul(d.get(), a.get());
const BigNum cb = mul(c.get(), b.get());
const BigNum sum = add(da.get(), cb.get());
const BigNum diff = sub(da.get(), cb.get());
x3 = mul(sum.get(), sum.get());
z3 = mul(x1.get(), mul(diff.get(), diff.get()).get());
x2 = mul(aa.get(), bb.get());
z2 = mul(e.get(), add(aa.get(), mul(a24().get(), e.get()).get()).get());
}
// No final cswap: the loop leaves swap = k_0, and clamping clears bit 0,
// so swap is provably false here for every input this function accepts.
// x2 / z2 via z2^(P-2) (Fermat): the affine u-coordinate result.
const BigNum pMinus2 = [&] {
BigNum pm2(BN_new());
if (!pm2 || BN_copy(pm2.get(), modulus().get()) == nullptr || BN_sub_word(pm2.get(), 2) != 1) {
throw std::runtime_error("Montgomery ladder final inversion failed.");
}
return pm2;
}();
return encodeLittleEndian(mul(x2.get(), powMod(z2.get(), pMinus2.get()).get()));
}
// ── Key generation ─────────────────────────────────────────────────────────
/** `length` bytes from the OpenSSL CSPRNG. */
static Bytes randomBytes(size_t length) {
Bytes bytes(length);
if (RAND_bytes(bytes.data(), static_cast<int>(length)) != 1) {
throw std::runtime_error("CSPRNG failure.");
}
return bytes;
}
/** A fresh private key: 32 CSPRNG bytes, clamped, Base64. */
std::string generatePrivateKey() { return bytesToBase64(clampPrivateKey(randomBytes(KEY_LENGTH))); }
/** A fresh pre-shared key: 32 CSPRNG bytes, Base64 — used as-is, never clamped. */
std::string generatePresharedKey() { return bytesToBase64(randomBytes(KEY_LENGTH)); }
/** Derive the WireGuard public key that pairs with a Base64 private key
* (Curve25519 scalar multiplication of the base point). */
std::string privateKeyToPublic(const std::string& privateKeyBase64) {
const Bytes priv = base64ToBytes(privateKeyBase64);
if (priv.size() != KEY_LENGTH) {
throw std::runtime_error("Invalid private key: expected " + std::to_string(KEY_LENGTH) +
" bytes, got " + std::to_string(priv.size()));
}
Bytes u(KEY_LENGTH, 0);
u[0] = 9; // base point
return bytesToBase64(curve25519(priv, u));
}
struct WireGuardKeys {
std::string privateKey;
std::string publicKey;
};
/** A fresh WireGuard key pair (private + matching public key, both Base64). */
WireGuardKeys generateWireGuardKeys() {
const std::string privateKey = generatePrivateKey();
return {privateKey, privateKeyToPublic(privateKey)};
}
// ── Config template ────────────────────────────────────────────────────────
/**
* 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 `psk` is
* given (it must be present on BOTH sides of the tunnel).
*/
std::string formatConfig(const WireGuardKeys& keys, const std::string& psk = "") {
std::string out = "[Interface]\n";
out += "# Your side — keep PrivateKey secret, share PublicKey with the peer\n";
out += "PrivateKey = " + keys.privateKey + "\n";
out += "PublicKey = " + keys.publicKey + "\n";
out += "# Tunnel address assigned by your server (plus optional tunnel DNS)\n";
out += "Address = 10.0.0.2/32\n";
out += "# DNS = 1.1.1.1\n";
out += "\n";
out += "[Peer]\n";
out += "# The other side's public key\n";
out += "PublicKey = <PEER_PUBLIC_KEY>\n";
if (!psk.empty()) {
out += "# Optional pre-shared key — the same value must be set on BOTH sides\n";
out += "PresharedKey = " + psk + "\n";
}
out += "# Route everything through the tunnel (or scope it, e.g. 10.0.0.0/24)\n";
out += "AllowedIPs = 0.0.0.0/0, ::/0\n";
out += "# The peer's public address and port\n";
out += "Endpoint = vpn.example.com:51820\n";
out += "PersistentKeepalive = 25";
return out;
}
} // namespace wireguard
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 →