Skip to content

WireGuard Key Generator — Zig 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 Zig implementation — the same logic the interactive tool runs, in a shareable, citable form.

//! wireguard-keygen — WireGuard key generation (Curve25519 / RFC 7748).
//!
//! Language: Zig 0.14 (standard library only)
//! Ported from: src/lib/wireguard-keygen.ts (the canonical TypeScript implementation).
//! display source — part of CosmoDev's polyglot tool pages.
//!
//! A WireGuard 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. The TS reference
//! implements the Montgomery ladder by hand in BigInt arithmetic to stay
//! dependency-free in the browser; Zig's standard library ships a constant-
//! time field-verified X25519 (`std.crypto.ecc.X25519`), so this port uses
//! it directly. Every key serializes as standard Base64 with padding —
//! 44 characters for 32 bytes — exactly the format WireGuard config files
//! expect.

const std = @import("std");

/// Length of every WireGuard key, in bytes.
pub const KEY_LENGTH = 32;

/// A WireGuard key pair, both keys in the standard Base64 wire format.
pub const WireGuardKeys = struct {
    private_key: [44]u8,
    public_key: [44]u8,
};

pub const Error = error{
    InvalidBase64,
    InvalidKeyLength,
    InvalidScalarLength,
    InvalidULength,
};

/// Base64 with the standard alphabet and `=` padding — what wg(8) expects.
const Base64 = std.base64.standard.Encoder;
const Base64Decoder = std.base64.standard.Decoder;

/// Clamp 32 bytes into a valid Curve25519 scalar (RFC 7748 §5). Returns a copy.
pub fn clampPrivateKey(key: []const u8) Error![KEY_LENGTH]u8 {
    if (key.len != KEY_LENGTH) return Error.InvalidKeyLength;
    var out = key[0..KEY_LENGTH].*;
    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 → always a 255-bit scalar
    return out;
}

/// Decode standard Base64 (with padding). Errors on invalid input.
pub fn base64ToBytes(out: *[KEY_LENGTH]u8, b64: []const u8) Error!void {
    const s = std.mem.trim(u8, b64, " \t\r\n");
    if (s.len != 44) return Error.InvalidBase64;
    Base64Decoder.decode(out, s) catch return Error.InvalidBase64;
}

/// Encode 32 bytes as the 44-char standard Base64 WireGuard uses.
pub fn bytesToBase64(out: *[44]u8, bytes: *const [KEY_LENGTH]u8) []const u8 {
    return Base64.encode(out, bytes);
}

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

const BASE_POINT: [KEY_LENGTH]u8 = blk: {
    var u = [_]u8{0} ** KEY_LENGTH;
    u[0] = 9; // the curve's base point, little-endian
    break :blk u;
};

/// The X25519 base point `9`, little-endian.
pub const base_point = BASE_POINT;

/// X25519 scalar multiplication `scalar · u`. The scalar is clamped
/// internally (an unclamped input yields the same result as its clamped
/// form, exactly like every X25519 implementation). The most significant
/// bit of the u-coordinate is masked per RFC 7748 §5.
pub fn curve25519(scalar: []const u8, u: []const u8) Error![KEY_LENGTH]u8 {
    if (scalar.len != KEY_LENGTH) return Error.InvalidScalarLength;
    if (u.len != KEY_LENGTH) return Error.InvalidULength;
    var k = try clampPrivateKey(scalar);
    var point = u[0..KEY_LENGTH].*;
    point[31] &= 0x7f; // mask the top bit of the u-coordinate
    var shared: [KEY_LENGTH]u8 = undefined;
    std.crypto.ecc.X25519.scalarmult(&shared, k, point) catch
        return Error.InvalidULength; // small-order input point → all-zero output
    return shared;
}

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

/// A fresh private key: 32 CSPRNG bytes, clamped, Base64.
pub fn generatePrivateKey(out: *[44]u8) []const u8 {
    var raw: [KEY_LENGTH]u8 = undefined;
    std.crypto.random.bytes(&raw);
    const clamped = clampPrivateKey(&raw) catch unreachable;
    return bytesToBase64(out, &clamped);
}

/// A fresh pre-shared key: 32 CSPRNG bytes, Base64 — used as-is, never clamped.
pub fn generatePresharedKey(out: *[44]u8) []const u8 {
    var raw: [KEY_LENGTH]u8 = undefined;
    std.crypto.random.bytes(&raw);
    return bytesToBase64(out, &raw);
}

/// Derive the WireGuard public key that pairs with a Base64 private key
/// (Curve25519 scalar multiplication of the base point).
pub fn privateKeyToPublic(out: *[44]u8, private_key_b64: []const u8) Error![]const u8 {
    var priv: [KEY_LENGTH]u8 = undefined;
    try base64ToBytes(&priv, private_key_b64);
    const public = try curve25519(&priv, &BASE_POINT);
    return bytesToBase64(out, &public);
}

/// A fresh WireGuard key pair (private + matching public key, both Base64).
pub fn generateWireGuardKeys() Error!WireGuardKeys {
    var keys: WireGuardKeys = undefined;
    _ = generatePrivateKey(&keys.private_key);
    const pub_b64 = try privateKeyToPublic(&keys.public_key, &keys.private_key);
    _ = pub_b64;
    return keys;
}

// ---------------------------------------------------------------------------
// 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).
pub fn formatConfig(allocator: std.mem.Allocator, keys: WireGuardKeys, psk: ?[]const u8) ![]u8 {
    var out = std.ArrayList(u8).init(allocator);
    errdefer out.deinit();

    const w = out.writer();
    try w.writeAll(
        \\[Interface]
        \\# Your side — keep PrivateKey secret, share PublicKey with the peer
    ++ "\n");
    try w.print("PrivateKey = {s}\n", .{keys.private_key});
    try w.print("PublicKey = {s}\n", .{keys.public_key});
    try w.writeAll(
        \\# 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>
    ++ "\n");
    if (psk != null and psk.?.len != 0) {
        try w.writeAll("# Optional pre-shared key — the same value must be set on BOTH sides\n");
        try w.print("PresharedKey = {s}\n", .{psk.?});
    }
    try w.writeAll(
        \\# Route everything through the tunnel (or scope it, e.g. 10.0.0.0/24)
        \\AllowedIPs = 0.0.0.0/0, ::/0
        \\# The peer's public address and port
        \\Endpoint = vpn.example.com:51820
        \\PersistentKeepalive = 25
    );
    return out.toOwnedSlice();
}

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 →