Skip to content

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 →