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 →