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 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 →