WireGuard Key Generator — Kotlin 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 Kotlin implementation — the same logic the interactive tool runs, in a shareable, citable form.
// WireGuard key generation — pure logic.
//
// Language: Kotlin 1.9+ (JVM), standard library only (java.math.BigInteger
// for the curve arithmetic, java.util.Base64 for the key format).
// Ported from src/lib/wireguard-keygen.ts — display source, part of CosmoDev's
// polyglot tool pages. Functionally equivalent to the TS reference: same
// clamping, same RFC 7748 Montgomery ladder (here over BigInteger instead of
// BigInt), same Base64 encoding, same config template.
//
// 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
// - 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; everything runs locally.
import java.math.BigInteger
import java.security.SecureRandom
import java.util.Base64
data class WireGuardKeys(
val privateKey: String,
val publicKey: String,
)
/** Length of every WireGuard key, in bytes. */
const val KEY_LENGTH = 32
// Curve25519 domain parameters: y² = x³ + 486662x² + x over GF(2^255 - 19).
private val P = BigInteger.ONE.shiftLeft(255).subtract(BigInteger.valueOf(19))
private val A24 = BigInteger.valueOf(121665) // (486662 - 2) / 4
private val BASE_POINT = ByteArray(KEY_LENGTH).also { it[0] = 9 } // u = 9, little-endian
private val RANDOM = SecureRandom()
// ---------------------------------------------------------------------------
// Base64 codec (standard alphabet, always padded — the WireGuard format)
// ---------------------------------------------------------------------------
/** Encode bytes as standard Base64 with `=` padding (32 bytes → 44 chars). */
fun bytesToBase64(bytes: ByteArray): String = Base64.getEncoder().encodeToString(bytes)
/** Decode standard Base64 (with padding). Throws on invalid input. */
fun base64ToBytes(b64: String): ByteArray {
val s = b64.trim()
if (s.isEmpty() || s.length % 4 != 0) {
throw IllegalArgumentException("Invalid Base64: length must be a non-zero multiple of 4")
}
return try {
Base64.getDecoder().decode(s)
} catch (_: IllegalArgumentException) {
throw IllegalArgumentException("Invalid Base64 character in input")
}
}
// ---------------------------------------------------------------------------
// Curve25519 scalar multiplication (RFC 7748 Montgomery ladder)
// ---------------------------------------------------------------------------
/** `base^exponent mod P` — BigInteger.modPow over the field. */
private fun powMod(base: BigInteger, exponent: BigInteger): BigInteger =
base.modPow(exponent, P)
/** Clamp 32 bytes into a valid Curve25519 scalar (RFC 7748 §5). Returns a copy. */
fun clampPrivateKey(key: ByteArray): ByteArray {
if (key.size != KEY_LENGTH) {
throw IllegalArgumentException("Private key must be $KEY_LENGTH bytes, got ${key.size}")
}
val out = key.copyOf()
out[0] = (out[0].toInt() and 248).toByte() // clear the low 3 bits → multiple of the cofactor
out[31] = (out[31].toInt() and 127).toByte() // clear the high bit
out[31] = (out[31].toInt() or 64).toByte() // force bit 254 → 255-bit scalar
return out
}
private fun decodeLittleEndian(bytes: ByteArray): BigInteger = BigInteger(1, bytes.reversedArray())
private fun encodeLittleEndian(n: BigInteger, length: Int = KEY_LENGTH): ByteArray {
// toByteArray() is minimal big-endian (possibly shorter than 32 bytes) —
// normalize to exactly `length` bytes, then flip to little-endian.
val be = n.toByteArray()
val padded = ByteArray(length)
val take = minOf(be.size, length)
System.arraycopy(be, be.size - take, padded, length - take, take)
return padded.reversedArray()
}
/**
* 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).
*/
fun curve25519(scalar: ByteArray, u: ByteArray): ByteArray {
if (scalar.size != KEY_LENGTH) {
throw IllegalArgumentException("Scalar must be $KEY_LENGTH bytes, got ${scalar.size}")
}
if (u.size != KEY_LENGTH) {
throw IllegalArgumentException("u-coordinate must be $KEY_LENGTH bytes, got ${u.size}")
}
val k = decodeLittleEndian(clampPrivateKey(scalar))
// Mask the most significant bit of the u-coordinate per RFC 7748 §5.
val x1 = decodeLittleEndian(u).and(BigInteger.ONE.shiftLeft(255).subtract(BigInteger.ONE))
var x2 = BigInteger.ONE
var z2 = BigInteger.ZERO
var x3 = x1
var z3 = BigInteger.ONE
var swap = BigInteger.ZERO
for (t in 254 downTo 0) {
val bit = k.shiftRight(t).and(BigInteger.ONE)
swap = swap.xor(bit)
if (swap.signum() == 1) {
var tmp = x2; x2 = x3; x3 = tmp
tmp = z2; z2 = z3; z3 = tmp
}
swap = bit
val a = x2.add(z2).mod(P)
val aa = a.multiply(a).mod(P)
val b = x2.subtract(z2).mod(P)
val bb = b.multiply(b).mod(P)
val e = aa.subtract(bb).mod(P)
val c = x3.add(z3).mod(P)
val d = x3.subtract(z3).mod(P)
val da = d.multiply(a).mod(P)
val cb = c.multiply(b).mod(P)
val sum = da.add(cb).mod(P)
val diff = da.subtract(cb).mod(P)
x3 = sum.multiply(sum).mod(P)
z3 = x1.multiply(diff.multiply(diff).mod(P)).mod(P)
x2 = aa.multiply(bb).mod(P)
z2 = e.multiply(aa.add(A24.multiply(e)).mod(P)).mod(P)
}
// No final cswap: the loop leaves swap = k_0, and clamping clears bit 0,
// so swap is provably 0 here for every input this function accepts.
// x2 / z2 via z2^(P-2) (Fermat): the affine u-coordinate result.
return encodeLittleEndian(x2.multiply(powMod(z2, P.subtract(BigInteger.TWO))).mod(P))
}
// ---------------------------------------------------------------------------
// Key generation
// ---------------------------------------------------------------------------
private fun randomBytes(length: Int): ByteArray = ByteArray(length).also(RANDOM::nextBytes)
/** A fresh private key: 32 CSPRNG bytes, clamped, Base64. */
fun generatePrivateKey(): String = bytesToBase64(clampPrivateKey(randomBytes(KEY_LENGTH)))
/** A fresh pre-shared key: 32 CSPRNG bytes, Base64 — used as-is, never clamped. */
fun generatePresharedKey(): String = bytesToBase64(randomBytes(KEY_LENGTH))
/** Derive the WireGuard public key that pairs with a Base64 private key. */
fun privateKeyToPublic(privateKeyBase64: String): String {
val priv = base64ToBytes(privateKeyBase64)
if (priv.size != KEY_LENGTH) {
throw IllegalArgumentException("Invalid private key: expected $KEY_LENGTH bytes, got ${priv.size}")
}
return bytesToBase64(curve25519(priv, BASE_POINT))
}
/** A fresh WireGuard key pair (private + matching public key, both Base64). */
fun generateWireGuardKeys(): WireGuardKeys {
val privateKey = generatePrivateKey()
return 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).
*/
fun formatConfig(keys: WireGuardKeys, psk: String? = null): String {
val lines = mutableListOf(
"[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.isNullOrEmpty()) {
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 lines.joinToString("\n")
}
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 →