Skip to content

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

// wireguard-keygen — WireGuard key generation (Curve25519, RFC 7748).
//
// Language: Swift 5.9+ (Foundation + CryptoKit)
// 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
//   - an optional pre-shared key is 32 random bytes, used as-is (no clamping)
//
// The TS reference implements the X25519 Montgomery ladder with plain BigInt
// arithmetic because JS ships no curve primitive. CryptoKit ships X25519
// natively (Curve25519.KeyAgreement), so this port computes the same scalar
// product through it. The clamp function is kept verbatim: CryptoKit clamps
// internally too, but clamping before serialization makes the generated
// private key bytes identical to the TS reference's.
//
// Every key is standard padded Base64 — 44 characters for 32 bytes — exactly
// what WireGuard config files expect. Randomness comes from
// SystemRandomNumberGenerator, the system CSPRNG.

import Foundation
import CryptoKit

// MARK: - Types

/// A WireGuard key pair, both keys in standard padded Base64.
struct WireGuardKeys: Equatable {
    let privateKey: String
    let publicKey: String
}

enum WireGuardError: Error, LocalizedError {
    case invalidBase64Length
    case invalidBase64Character(Character)
    case wrongKeyLength(expected: Int, got: Int)

    var errorDescription: String? {
        switch self {
        case .invalidBase64Length:
            return "Invalid Base64: length must be a non-zero multiple of 4"
        case .invalidBase64Character(let ch):
            return "Invalid Base64 character: \"\(ch)\""
        case .wrongKeyLength(let expected, let got):
            let what = expected == KEY_LENGTH ? "Private key" : "Key"
            return "Invalid private key: expected \(expected) bytes, got \(got) (\(what))"
        }
    }
}

/// Length of every WireGuard key, in bytes.
let KEY_LENGTH = 32

/// The curve's base point u = 9, little-endian — what every private key is
/// multiplied by to derive its public key.
let BASE_POINT: [UInt8] = {
    var u = [UInt8](repeating: 0, count: KEY_LENGTH)
    u[0] = 9
    return u
}()

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

let B64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"

let B64_VALUES: [Character: Int] = {
    var map: [Character: Int] = [:]
    for (i, c) in B64_ALPHABET.enumerated() { map[c] = i }
    return map
}()

/// Encode bytes as standard Base64 with `=` padding (32 bytes → 44 chars).
/// (Foundation's Data base64 encoding is exactly this format.)
func bytesToBase64(_ bytes: [UInt8]) -> String {
    Data(bytes).base64EncodedString()
}

/// Decode standard Base64 (with padding). Throws on invalid input.
func base64ToBytes(_ b64: String) throws -> [UInt8] {
    let s = b64.trimmingCharacters(in: .whitespacesAndNewlines)
    guard !s.isEmpty, s.count % 4 == 0 else { throw WireGuardError.invalidBase64Length }
    let pad = s.hasSuffix("==") ? 2 : (s.hasSuffix("=") ? 1 : 0)
    var out = [UInt8]()
    out.reserveCapacity(s.count / 4 * 3 - pad)
    var buffer = 0
    var bits = 0
    for ch in s.dropLast(pad) {
        guard let v = B64_VALUES[ch] else { throw WireGuardError.invalidBase64Character(ch) }
        buffer = (buffer << 6) | v
        bits += 6
        if bits >= 8 {
            bits -= 8
            out.append(UInt8((buffer >> bits) & 0xff))
        }
    }
    return out
}

// MARK: - Curve25519

/// Clamp 32 bytes into a valid Curve25519 scalar (RFC 7748 §5). Returns a copy.
func clampPrivateKey(_ key: [UInt8]) throws -> [UInt8] {
    guard key.count == KEY_LENGTH else {
        throw WireGuardError.wrongKeyLength(expected: KEY_LENGTH, got: key.count)
    }
    var 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
}

/// X25519 scalar multiplication `scalar · u` — the same operation as the TS
/// reference's BigInt Montgomery ladder, computed by CryptoKit's native
/// implementation. The scalar is clamped internally (an unclamped input
/// yields the same result as its clamped form, exactly like every X25519
/// implementation).
func curve25519(scalar: [UInt8], u: [UInt8]) throws -> [UInt8] {
    guard scalar.count == KEY_LENGTH else {
        throw WireGuardError.wrongKeyLength(expected: KEY_LENGTH, got: scalar.count)
    }
    guard u.count == KEY_LENGTH else {
        throw WireGuardError.wrongKeyLength(expected: KEY_LENGTH, got: u.count)
    }
    let secret = try Curve25519.KeyAgreement.PrivateKey(rawRepresentation: Data(scalar))
    let base = try Curve25519.KeyAgreement.PublicKey(rawRepresentation: Data(u))
    let shared = try secret.sharedSecretFromKeyAgreement(with: base)
    return Array(shared.withUnsafeBytes { Data($0) })
}

// MARK: - Key generation

/// 32 CSPRNG bytes — SystemRandomNumberGenerator is the system CSPRNG
/// (SecRandom on Apple platforms), the counterpart of crypto.getRandomValues.
func randomBytes(_ length: Int) -> [UInt8] {
    var csprng = SystemRandomNumberGenerator()
    return (0..<length).map { _ in UInt8.random(in: .min ... .max, using: &csprng) }
}

/// A fresh private key: 32 CSPRNG bytes, clamped, Base64.
func generatePrivateKey() -> String {
    (try? bytesToBase64(clampPrivateKey(randomBytes(KEY_LENGTH)))) ?? ""
}

/// A fresh pre-shared key: 32 CSPRNG bytes, Base64 — used as-is, never clamped.
func generatePresharedKey() -> String {
    bytesToBase64(randomBytes(KEY_LENGTH))
}

/// Derive the WireGuard public key that pairs with a Base64 private key
/// (Curve25519 scalar multiplication of the base point).
func privateKeyToPublic(_ privateKeyBase64: String) throws -> String {
    let priv = try base64ToBytes(privateKeyBase64)
    guard priv.count == KEY_LENGTH else {
        throw WireGuardError.wrongKeyLength(expected: KEY_LENGTH, got: priv.count)
    }
    return bytesToBase64(try curve25519(scalar: priv, u: BASE_POINT))
}

/// A fresh WireGuard key pair (private + matching public key, both Base64).
func generateWireGuardKeys() throws -> WireGuardKeys {
    let privateKey = generatePrivateKey()
    let publicKey = try privateKeyToPublic(privateKey)
    return WireGuardKeys(privateKey: privateKey, publicKey: publicKey)
}

// MARK: - 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).
func formatConfig(_ keys: WireGuardKeys, psk: String? = nil) -> String {
    var lines = [
        "[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 let psk = psk, !psk.isEmpty {
        lines.append("# Optional pre-shared key — the same value must be set on BOTH sides")
        lines.append("PresharedKey = \(psk)")
    }
    lines += [
        "# 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 lines.joined(separator: "\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 →