Skip to content

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

// WireGuard Key Generator — Curve25519 key generation, pure logic.
//
// Language: Java (17+, standard library only)
// Ported from src/lib/wireguard-keygen.ts
// display source — part of CosmoDev's polyglot tool 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 SecureRandom and the curve arithmetic is deterministic
// BigInteger math, so the whole class runs anywhere Java does, zero deps.

import java.math.BigInteger;
import java.security.SecureRandom;
import java.util.Base64;
import java.util.List;

public final class WireGuardKeygen {

    /** A WireGuard key pair: Base64 private key + matching Base64 public key. */
    public record WireGuardKeys(String privateKey, String publicKey) {
    }

    /** Length of every WireGuard key, in bytes. */
    public static final int KEY_LENGTH = 32;

    // Curve25519 domain parameters: y^2 = x^3 + 486662x^2 + x over GF(2^255 - 19).
    private static final BigInteger P = BigInteger.TWO.pow(255).subtract(BigInteger.valueOf(19));
    private static final BigInteger A24 = BigInteger.valueOf(121665); // (486662 - 2) / 4

    private static final byte[] BASE_POINT = new byte[KEY_LENGTH]; // u = 9, little-endian

    static {
        BASE_POINT[0] = 9;
    }

    private static final SecureRandom RANDOM = new SecureRandom();

    private WireGuardKeygen() {
    }

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

    /** Encode bytes as standard Base64 with `=` padding (32 bytes -> 44 chars). */
    public static String bytesToBase64(byte[] bytes) {
        return Base64.getEncoder().encodeToString(bytes);
    }

    /** Decode standard Base64 (with padding). Throws IllegalArgumentException on invalid input. */
    public static byte[] base64ToBytes(String b64) {
        String s = b64.trim();
        if (s.isEmpty() || s.length() % 4 != 0) {
            throw new IllegalArgumentException("Invalid Base64: length must be a non-zero multiple of 4");
        }
        try {
            return Base64.getDecoder().decode(s);
        } catch (IllegalArgumentException e) {
            throw new IllegalArgumentException("Invalid Base64 character", e);
        }
    }

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

    /** Reduce `a` into the canonical range [0, P). BigInteger.mod always lands there. */
    private static BigInteger mod(BigInteger a) {
        return a.mod(P);
    }

    /** `base^exponent mod P` (used for field inversion via Fermat's little theorem). */
    private static BigInteger powMod(BigInteger base, BigInteger exponent) {
        return base.modPow(exponent, P);
    }

    /** Clamp 32 bytes into a valid Curve25519 scalar (RFC 7748 §5). Returns a copy. */
    public static byte[] clampPrivateKey(byte[] key) {
        if (key.length != KEY_LENGTH) {
            throw new IllegalArgumentException(
                    "Private key must be " + KEY_LENGTH + " bytes, got " + key.length);
        }
        byte[] out = key.clone();
        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;
    }

    private static BigInteger decodeLittleEndian(byte[] bytes) {
        BigInteger n = BigInteger.ZERO;
        for (int i = bytes.length - 1; i >= 0; i--) {
            n = n.shiftLeft(8).or(BigInteger.valueOf(bytes[i] & 0xff));
        }
        return n;
    }

    private static byte[] encodeLittleEndian(BigInteger n) {
        byte[] out = new byte[KEY_LENGTH];
        for (int i = 0; i < KEY_LENGTH; i++) {
            out[i] = n.and(BigInteger.valueOf(0xff)).byteValue();
            n = n.shiftRight(8);
        }
        return out;
    }

    /**
     * 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).
     */
    public static byte[] curve25519(byte[] scalar, byte[] u) {
        if (scalar.length != KEY_LENGTH) {
            throw new IllegalArgumentException("Scalar must be " + KEY_LENGTH + " bytes, got " + scalar.length);
        }
        if (u.length != KEY_LENGTH) {
            throw new IllegalArgumentException("u-coordinate must be " + KEY_LENGTH + " bytes, got " + u.length);
        }
        BigInteger k = decodeLittleEndian(clampPrivateKey(scalar));
        // Mask the most significant bit of the u-coordinate per RFC 7748 §5.
        BigInteger x1 = decodeLittleEndian(u).and(BigInteger.TWO.pow(255).subtract(BigInteger.ONE));

        BigInteger x2 = BigInteger.ONE;
        BigInteger z2 = BigInteger.ZERO;
        BigInteger x3 = x1;
        BigInteger z3 = BigInteger.ONE;
        int swap = 0;
        for (int t = 254; t >= 0; t--) {
            int bit = k.testBit(t) ? 1 : 0;
            swap ^= bit;
            if (swap == 1) {
                BigInteger tmpX = x2; x2 = x3; x3 = tmpX;
                BigInteger tmpZ = z2; z2 = z3; z3 = tmpZ;
            }
            swap = bit;

            BigInteger a = mod(x2.add(z2));
            BigInteger aa = mod(a.multiply(a));
            BigInteger b = mod(x2.subtract(z2));
            BigInteger bb = mod(b.multiply(b));
            BigInteger e = mod(aa.subtract(bb));
            BigInteger c = mod(x3.add(z3));
            BigInteger d = mod(x3.subtract(z3));
            BigInteger da = mod(d.multiply(a));
            BigInteger cb = mod(c.multiply(b));
            BigInteger sum = mod(da.add(cb));
            BigInteger diff = mod(da.subtract(cb));
            x3 = mod(sum.multiply(sum));
            z3 = mod(x1.multiply(mod(diff.multiply(diff))));
            x2 = mod(aa.multiply(bb));
            z2 = mod(e.multiply(mod(aa.add(A24.multiply(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 method accepts.
        // x2 / z2 via z2^(P-2) (Fermat): the affine u-coordinate result.
        return encodeLittleEndian(mod(x2.multiply(powMod(z2, P.subtract(BigInteger.TWO)))));
    }

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

    private static byte[] randomBytes(int length) {
        byte[] bytes = new byte[length];
        RANDOM.nextBytes(bytes);
        return bytes;
    }

    /** A fresh private key: 32 CSPRNG bytes, clamped, Base64. */
    public static String generatePrivateKey() {
        return bytesToBase64(clampPrivateKey(randomBytes(KEY_LENGTH)));
    }

    /** A fresh pre-shared key: 32 CSPRNG bytes, Base64 — used as-is, never clamped. */
    public static 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).
     */
    public static String privateKeyToPublic(String privateKeyBase64) {
        byte[] priv = base64ToBytes(privateKeyBase64);
        if (priv.length != KEY_LENGTH) {
            throw new IllegalArgumentException(
                    "Invalid private key: expected " + KEY_LENGTH + " bytes, got " + priv.length);
        }
        return bytesToBase64(curve25519(priv, BASE_POINT));
    }

    /** A fresh WireGuard key pair (private + matching public key, both Base64). */
    public static WireGuardKeys generateWireGuardKeys() {
        String privateKey = generatePrivateKey();
        return new WireGuardKeys(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).
     */
    public static String formatConfig(WireGuardKeys keys, String psk) {
        List<String> lines = new java.util.ArrayList<>(List.of(
                "[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 (psk != null && !psk.isEmpty()) {
            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 →