WireGuard Key Generator — Ruby 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 Ruby implementation — the same logic the interactive tool runs, in a shareable, citable form.
# WireGuard Key Generator — Curve25519 key generation, pure logic.
#
# Language: Ruby (3.1+, standard library only)
# Source: CosmoDev polyglot showcase port of the WireGuard Key Generator
# tool, ported from src/lib/wireguard-keygen.ts (the canonical
# TypeScript implementation).
# License: 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-Integer 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 Integer math (Ruby integers are arbitrary precision), so the
# whole module runs anywhere with zero dependencies.
require 'securerandom'
module WireGuardKeygen
WireGuardKeys = Struct.new(:private_key, :public_key, keyword_init: true)
# Length of every WireGuard key, in bytes.
KEY_LENGTH = 32
# Curve25519 domain parameters: y^2 = x^3 + 486662x^2 + x over GF(2^255 - 19).
P = (1 << 255) - 19
A24 = 121_665 # (486662 - 2) / 4
# u = 9, little-endian.
BASE_POINT = (Array.new(KEY_LENGTH - 1, 0) + [9]).pack('C*').freeze
B64_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
class << self
# Encode bytes as standard Base64 with `=` padding (32 bytes -> 44 chars).
def bytes_to_base64(bytes)
[bytes].pack('m0')
end
# Decode standard Base64 (with padding). Raises on invalid input.
# Accepts a binary String or anything responding to #to_s.
def base64_to_bytes(b64)
s = b64.strip
if s.empty? || s.size % 4 != 0
raise ArgumentError, 'Invalid Base64: length must be a non-zero multiple of 4'
end
pad = s.end_with?('==') ? 2 : (s.end_with?('=') ? 1 : 0)
data = s[0, s.size - pad]
out = +(''.b)
buffer = 0
bits = 0
data.each_char do |ch|
v = B64_ALPHABET.index(ch)
raise ArgumentError, %(Invalid Base64 character: "#{ch}") if v.nil?
buffer = (buffer << 6) | v
bits += 6
if bits >= 8
bits -= 8
out << ((buffer >> bits) & 0xff).chr
end
end
out
end
# Clamp 32 bytes into a valid Curve25519 scalar (RFC 7748 §5). Returns a
# copy; the input is never mutated.
def clamp_private_key(key)
unless key.bytesize == KEY_LENGTH
raise ArgumentError,
"Private key must be #{KEY_LENGTH} bytes, got #{key.bytesize}"
end
out = key.dup.b
out.setbyte(0, out.getbyte(0) & 248) # clear the low 3 bits -> multiple of the cofactor
out.setbyte(31, out.getbyte(31) & 127) # clear the high bit
out.setbyte(31, out.getbyte(31) | 64) # force bit 254 -> the ladder always sees a 255-bit scalar
out
end
# X25519 scalar multiplication `scalar * u` — the RFC 7748 Montgomery
# ladder in plain Integer 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).
def curve25519(scalar, u)
unless scalar.bytesize == KEY_LENGTH
raise ArgumentError, "Scalar must be #{KEY_LENGTH} bytes, got #{scalar.bytesize}"
end
unless u.bytesize == KEY_LENGTH
raise ArgumentError, "u-coordinate must be #{KEY_LENGTH} bytes, got #{u.bytesize}"
end
k = decode_little_endian(clamp_private_key(scalar))
# Mask the most significant bit of the u-coordinate per RFC 7748 §5.
x1 = decode_little_endian(u) & ((1 << 255) - 1)
x2 = 1
z2 = 0
x3 = x1
z3 = 1
swap = 0
254.downto(0) do |t|
bit = (k >> t) & 1
swap ^= bit
if swap == 1
x2, x3 = x3, x2
z2, z3 = z3, z2
end
swap = bit
a = mod(x2 + z2)
aa = mod(a * a)
b = mod(x2 - z2)
bb = mod(b * b)
e = mod(aa - bb)
c = mod(x3 + z3)
d = mod(x3 - z3)
da = mod(d * a)
cb = mod(c * b)
sum = mod(da + cb)
diff = mod(da - cb)
x3 = mod(sum * sum)
z3 = mod(x1 * mod(diff * diff))
x2 = mod(aa * bb)
z2 = mod(e * mod(aa + A24 * e))
end
# 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.
encode_little_endian(mod(x2 * pow_mod(z2, P - 2)))
end
# A fresh private key: 32 CSPRNG bytes, clamped, Base64.
def generate_private_key
bytes_to_base64(clamp_private_key(random_bytes(KEY_LENGTH)))
end
# A fresh pre-shared key: 32 CSPRNG bytes, Base64 — used as-is, never
# clamped.
def generate_preshared_key
bytes_to_base64(random_bytes(KEY_LENGTH))
end
# Derive the WireGuard public key that pairs with a Base64 private key
# (Curve25519 scalar multiplication of the base point).
def private_key_to_public(private_key_base64)
priv = base64_to_bytes(private_key_base64)
unless priv.bytesize == KEY_LENGTH
raise ArgumentError,
"Invalid private key: expected #{KEY_LENGTH} bytes, got #{priv.bytesize}"
end
bytes_to_base64(curve25519(priv, BASE_POINT))
end
# A fresh WireGuard key pair (private + matching public key, both Base64).
def generate_wireguard_keys
private_key = generate_private_key
WireGuardKeys.new(private_key: private_key,
public_key: private_key_to_public(private_key))
end
# 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).
def format_config(keys, psk = nil)
lines = [
'[Interface]',
'# Your side — keep PrivateKey secret, share PublicKey with the peer',
"PrivateKey = #{keys.private_key}",
"PublicKey = #{keys.public_key}",
'# 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 && !psk.empty?
lines.push(
'# Optional pre-shared key — the same value must be set on BOTH sides',
"PresharedKey = #{psk}"
)
end
lines.push(
'# 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'
)
lines.join("\n")
end
private
def random_bytes(length)
SecureRandom.random_bytes(length)
end
# Reduce `a` into the canonical range [0, P).
def mod(a)
r = a % P
r.negative? ? r + P : r
end
# `base^exponent mod P` via square-and-multiply (used for field inversion).
def pow_mod(base, exponent)
result = 1
b = mod(base)
e = exponent
until e.zero?
result = mod(result * b) if e.odd?
b = mod(b * b)
e >>= 1
end
result
end
def decode_little_endian(bytes)
n = 0
(bytes.bytesize - 1).downto(0) do |i|
n = (n << 8) | bytes.getbyte(i)
end
n
end
def encode_little_endian(n, length = KEY_LENGTH)
Array.new(length) { |i| (n >> (8 * i)) & 0xff }.pack('C*')
end
end
end
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 →