From fd1d199ae8dccb6d12b5c7c114578063cdbfae88 Mon Sep 17 00:00:00 2001 From: CaffeineFueled Date: Fri, 20 Jun 2025 17:00:06 +0200 Subject: [PATCH] init --- LICENSE | 21 + README.md | 46 +++ index.html | 108 +++++ mesh-generator/index.html | 97 +++++ static/script.js | 837 ++++++++++++++++++++++++++++++++++++++ static/styles.css | 312 ++++++++++++++ 6 files changed, 1421 insertions(+) create mode 100644 LICENSE create mode 100644 README.md create mode 100644 index.html create mode 100644 mesh-generator/index.html create mode 100644 static/script.js create mode 100644 static/styles.css diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..05e72b9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 WireGuard Configuration Generator + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..c857f8e --- /dev/null +++ b/README.md @@ -0,0 +1,46 @@ +# WireGuard Configuration Generator + +A web-based tool for generating WireGuard VPN configurations with cryptographically secure key generation. Supports both **Hub-and-Spoke** and **Mesh Network** topologies. + +## TODO + +- QR Code generator for config +- Download all config at once +- Make `PresharedKey` and other options optional +- Container +- frontend rework (I hate frontend) + +## 🔐 Production-Ready Cryptography + +This project uses **real cryptographic implementations** suitable for production WireGuard deployments, not demonstration code. + +### Cryptographic Implementation + +#### **Libraries Used** +- **[TweetNaCl.js v1.0.3](https://tweetnacl.js.org/)** - Audited, lightweight cryptographic library +- **Web Crypto API** - Browser-native cryptographic operations when available +- **HMAC-SHA256 Fallback** - Custom implementation for browsers without Web Crypto API + +#### Cryptographic Flow + +1. **Seed Generation/Input** + - Generate cryptographically secure 32-byte seed + - Or accept user-provided hex seed for reproducibility + +2. **Key Derivation** + - Use HKDF to derive keys from seed with unique salts + - Private keys: `HKDF(seed, "WireGuard v1 private key", key_index)` + - Preshared keys: `HKDF(seed, "WireGuard v1 preshared key", key_index)` + +3. **Public Key Generation** + - Apply Curve25519 scalar multiplication: `public = private * G` + - Where G is the Curve25519 base point + +4. **Key Validation** + - Verify key lengths (32 bytes each) + - Check private key clamping + - Confirm public key derivation + +# License + +WIP diff --git a/index.html b/index.html new file mode 100644 index 0000000..c39d292 --- /dev/null +++ b/index.html @@ -0,0 +1,108 @@ + + + + + + WireGuard Hub-and-Spoke Configuration Generator + + + + +
+ + +

WireGuard Hub-and-Spoke Configuration Generator

+ +
+ Privacy Notice: All configuration generation and cryptographic operations are performed entirely in your browser. No data is transmitted to any server - your keys, configurations, and settings remain completely private and local to your device. +
+ +
+
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+ +
Click "Generate New Seed" or "Generate Configurations" to create a seed
+
+ + + +
+
+ + +
+
+ + +
+ + +
+ + + + + + \ No newline at end of file diff --git a/mesh-generator/index.html b/mesh-generator/index.html new file mode 100644 index 0000000..e891c2c --- /dev/null +++ b/mesh-generator/index.html @@ -0,0 +1,97 @@ + + + + + + WireGuard Mesh Network Configuration Generator + + + + +
+ + +

WireGuard Mesh Network Configuration Generator

+ +
+ Privacy Notice: All configuration generation and cryptographic operations are performed entirely in your browser. No data is transmitted to any server - your keys, configurations, and settings remain completely private and local to your device. +
+ +

Generate a full mesh WireGuard configuration where every peer can communicate directly with every other peer.

+ +
+
+

Network Settings

+
+
+ + +
+
+ + +
+
+
+ +
+

Peer Configuration

+
+ + + +
+
+ +
+

Peer Details

+
+
+ +
+ +
Click "Generate New Seed" or "Generate Configurations" to create a seed
+
+ + + +
+
+ + +
+
+ + +
+ + +
+ + + + + + \ No newline at end of file diff --git a/static/script.js b/static/script.js new file mode 100644 index 0000000..ac4122d --- /dev/null +++ b/static/script.js @@ -0,0 +1,837 @@ +// Production-ready cryptographic functions for WireGuard key generation +// Uses TweetNaCl.js for proper Curve25519 operations and Web Crypto API for HKDF + +class WireGuardCrypto { + constructor() { + this.crypto = window.crypto || window.msCrypto; + + // Check if TweetNaCl is available + if (typeof nacl === 'undefined') { + throw new Error('TweetNaCl library is required but not loaded'); + } + + // Check if Web Crypto API is available + this.hasWebCrypto = !!(this.crypto && this.crypto.subtle); + + if (!this.hasWebCrypto) { + console.warn('Web Crypto API not available, falling back to less secure methods'); + } + + // Key counter for deterministic key generation + this.keyCounter = 0; + } + + // Generate a cryptographically secure random seed + generateSeed() { + const array = new Uint8Array(32); + this.crypto.getRandomValues(array); + return array; + } + + // HKDF implementation using Web Crypto API when available + async hkdf(seed, salt, info, length = 32) { + if (this.hasWebCrypto) { + try { + // Import the seed as key material + const keyMaterial = await this.crypto.subtle.importKey( + 'raw', + seed, + 'HKDF', + false, + ['deriveBits'] + ); + + // Derive key using HKDF + const derivedBits = await this.crypto.subtle.deriveBits( + { + name: 'HKDF', + hash: 'SHA-256', + salt: salt, + info: info + }, + keyMaterial, + length * 8 // length in bits + ); + + return new Uint8Array(derivedBits); + } catch (error) { + console.warn('Web Crypto HKDF failed, falling back to HMAC-based implementation:', error); + return this.hkdfFallback(seed, salt, info, length); + } + } else { + return this.hkdfFallback(seed, salt, info, length); + } + } + + // Fallback HKDF implementation using TweetNaCl's hash function + hkdfFallback(seed, salt, info, length = 32) { + // HKDF-Extract + const prk = this.hmacSha256(salt.length > 0 ? salt : new Uint8Array(32), seed); + + // HKDF-Expand + const n = Math.ceil(length / 32); + const okm = new Uint8Array(length); + let t = new Uint8Array(0); + + for (let i = 1; i <= n; i++) { + const concat = new Uint8Array(t.length + info.length + 1); + concat.set(t); + concat.set(info, t.length); + concat[concat.length - 1] = i; + + t = this.hmacSha256(prk, concat); + const copyLength = Math.min(32, length - (i - 1) * 32); + okm.set(t.subarray(0, copyLength), (i - 1) * 32); + } + + return okm; + } + + // HMAC-SHA256 implementation using TweetNaCl + hmacSha256(key, data) { + const blockSize = 64; + const hashSize = 32; + + // If key is longer than block size, hash it + if (key.length > blockSize) { + key = nacl.hash(key).subarray(0, hashSize); + } + + // Pad key to block size + const paddedKey = new Uint8Array(blockSize); + paddedKey.set(key); + + // Create inner and outer padding + const ipad = new Uint8Array(blockSize); + const opad = new Uint8Array(blockSize); + + for (let i = 0; i < blockSize; i++) { + ipad[i] = paddedKey[i] ^ 0x36; + opad[i] = paddedKey[i] ^ 0x5c; + } + + // Inner hash + const innerData = new Uint8Array(blockSize + data.length); + innerData.set(ipad); + innerData.set(data, blockSize); + const innerHash = nacl.hash(innerData); + + // Outer hash + const outerData = new Uint8Array(blockSize + hashSize); + outerData.set(opad); + outerData.set(innerHash); + + return nacl.hash(outerData); + } + + // Generate a WireGuard private key using proper key derivation + async generatePrivateKey(seed) { + const salt = new TextEncoder().encode('WireGuard v1 private key'); + const info = new Uint8Array(4); + // Convert key counter to bytes (little endian) + const keyIndex = this.keyCounter++; + info[0] = keyIndex & 0xff; + info[1] = (keyIndex >> 8) & 0xff; + info[2] = (keyIndex >> 16) & 0xff; + info[3] = (keyIndex >> 24) & 0xff; + + const keyMaterial = await this.hkdf(seed, salt, info, 32); + + // Apply Curve25519 key clamping as per RFC 7748 + keyMaterial[0] &= 248; // Clear bottom 3 bits + keyMaterial[31] &= 127; // Clear top bit + keyMaterial[31] |= 64; // Set second-highest bit + + return this.arrayToBase64(keyMaterial); + } + + // Generate public key from private key using real Curve25519 scalar multiplication + generatePublicKey(privateKeyBase64) { + try { + const privateKeyBytes = this.base64ToArray(privateKeyBase64); + + // Use TweetNaCl's scalar multiplication with base point + const publicKeyBytes = nacl.scalarMult.base(privateKeyBytes); + + return this.arrayToBase64(publicKeyBytes); + } catch (error) { + throw new Error('Failed to generate public key: ' + error.message); + } + } + + // Generate preshared key using proper key derivation + async generatePresharedKey(seed) { + const salt = new TextEncoder().encode('WireGuard v1 preshared key'); + const info = new Uint8Array(4); + const keyIndex = this.keyCounter++; + info[0] = keyIndex & 0xff; + info[1] = (keyIndex >> 8) & 0xff; + info[2] = (keyIndex >> 16) & 0xff; + info[3] = (keyIndex >> 24) & 0xff; + + const keyMaterial = await this.hkdf(seed, salt, info, 32); + return this.arrayToBase64(keyMaterial); + } + + // Reset key counter for deterministic generation + resetKeyCounter() { + this.keyCounter = 0; + } + + // Utility functions + arrayToBase64(array) { + return btoa(String.fromCharCode.apply(null, array)); + } + + base64ToArray(base64) { + const binary = atob(base64); + const array = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i++) { + array[i] = binary.charCodeAt(i); + } + return array; + } + + arrayToHex(array) { + return Array.from(array).map(b => b.toString(16).padStart(2, '0')).join(''); + } + + hexToArray(hex) { + if (hex.length !== 64) { + throw new Error('Hex string must be exactly 64 characters (32 bytes)'); + } + const array = new Uint8Array(32); + for (let i = 0; i < 32; i++) { + array[i] = parseInt(hex.substr(i * 2, 2), 16); + } + return array; + } + + // Parse seed from input (hex string or generate new) + parseSeed(customSeedHex) { + if (customSeedHex && customSeedHex.trim()) { + return this.hexToArray(customSeedHex.trim()); + } else { + return this.generateSeed(); + } + } + + // Validate that generated keys are valid + validateKeyPair(privateKeyBase64, publicKeyBase64) { + try { + const privateKey = this.base64ToArray(privateKeyBase64); + const publicKey = this.base64ToArray(publicKeyBase64); + + // Check lengths + if (privateKey.length !== 32 || publicKey.length !== 32) { + console.warn('Invalid key length'); + return false; + } + + // Check key clamping on private key + if ((privateKey[0] & 7) !== 0 || (privateKey[31] & 128) !== 0 || (privateKey[31] & 64) === 0) { + console.warn('Invalid key clamping'); + return false; + } + + // Verify public key matches private key + const derivedPublic = nacl.scalarMult.base(privateKey); + + for (let i = 0; i < 32; i++) { + if (derivedPublic[i] !== publicKey[i]) { + console.warn('Public key does not match private key'); + return false; + } + } + + return true; + } catch (error) { + console.error('Key validation error:', error); + return false; + } + } + + // Get info about the crypto implementation + getCryptoInfo() { + return { + library: 'TweetNaCl.js v1.0.3', + hasWebCrypto: this.hasWebCrypto, + curve: 'Curve25519 (X25519)', + hkdf: this.hasWebCrypto ? 'Web Crypto API' : 'HMAC-SHA256 fallback', + secure: true + }; + } +} + +let currentConfigs = null; +let currentSeed = null; +let currentMeshConfigs = null; + +// Initialize crypto with error handling +let crypto; +try { + crypto = new WireGuardCrypto(); + console.log('Crypto initialized:', crypto.getCryptoInfo()); +} catch (error) { + console.error('Failed to initialize crypto:', error); + alert('Cryptographic library failed to initialize. Please refresh the page.'); +} + +async function generateConfigs() { + try { + const serverName = document.getElementById('serverName').value; + const serverPort = parseInt(document.getElementById('serverPort').value); + const serverNetwork = document.getElementById('serverNetwork').value; + const serverEndpoint = document.getElementById('serverEndpoint').value; + const clientCount = parseInt(document.getElementById('clientCount').value); + const dns = document.getElementById('dns').value; + + // Validate inputs + if (!validateInputs(serverNetwork, serverEndpoint, clientCount)) { + return; + } + + // Show loading state + const generateBtn = document.querySelector('button[onclick="generateConfigs()"]'); + const originalText = generateBtn.textContent; + generateBtn.textContent = 'Generating...'; + generateBtn.disabled = true; + + // Generate or use existing seed + const customSeedHex = document.getElementById('customSeed').value; + let seed; + + if (customSeedHex && customSeedHex.trim()) { + // Use custom seed from input + seed = crypto.parseSeed(customSeedHex); + } else if (currentSeed) { + // Use existing seed if available + seed = currentSeed; + } else { + // Generate new seed only if none exists + seed = crypto.generateSeed(); + } + + currentSeed = seed; + + // Update seed display and input field + const seedHex = crypto.arrayToHex(seed); + document.getElementById('seedDisplay').textContent = seedHex; + if (!customSeedHex || !customSeedHex.trim()) { + document.getElementById('customSeed').value = seedHex; + } + + // Reset key counter for deterministic generation + crypto.resetKeyCounter(); + + // Generate server keys + const serverPrivateKey = await crypto.generatePrivateKey(seed); + const serverPublicKey = crypto.generatePublicKey(serverPrivateKey); + + // Parse network + const [networkBase, cidr] = serverNetwork.split('/'); + const networkParts = networkBase.split('.').map(n => parseInt(n)); + + // Generate client configurations + const clients = []; + for (let i = 1; i <= clientCount; i++) { + const clientPrivateKey = await crypto.generatePrivateKey(seed); + const clientPublicKey = crypto.generatePublicKey(clientPrivateKey); + const presharedKey = await crypto.generatePresharedKey(seed); + + // Calculate client IP (server gets .1, clients get .2, .3, etc.) + const clientIP = `${networkParts[0]}.${networkParts[1]}.${networkParts[2]}.${networkParts[3] + i}`; + + clients.push({ + name: `client-${i}`, + privateKey: clientPrivateKey, + publicKey: clientPublicKey, + presharedKey: presharedKey, + ip: clientIP + }); + } + + // Generate configurations + const serverConfig = generateServerConfig(serverName, serverPrivateKey, serverPort, + `${networkParts[0]}.${networkParts[1]}.${networkParts[2]}.${networkParts[3] + 1}`, + cidr, clients); + + const clientConfigs = clients.map(client => + generateClientConfig(client, serverPublicKey, serverEndpoint, serverPort, + `${networkParts[0]}.${networkParts[1]}.${networkParts[2]}.${networkParts[3] + 1}`, + cidr, dns) + ); + + // Store configurations + currentConfigs = { + server: serverConfig, + clients: clientConfigs.map((config, index) => ({ + name: clients[index].name, + config: config + })) + }; + + // Display configurations + displayConfigurations(); + + // Restore button + generateBtn.textContent = originalText; + generateBtn.disabled = false; + + } catch (error) { + // Restore button on error + const generateBtn = document.querySelector('button[onclick="generateConfigs()"]'); + if (generateBtn) { + generateBtn.textContent = 'Generate Configurations'; + generateBtn.disabled = false; + } + alert('Error generating configurations: ' + error.message); + } +} + +function validateInputs(serverNetwork, serverEndpoint, clientCount) { + // Validate network CIDR + const cidrRegex = /^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}\/([1-2]?[0-9]|3[0-2])$/; + if (!cidrRegex.test(serverNetwork)) { + alert('Invalid network CIDR format'); + return false; + } + + // Validate endpoint + if (!serverEndpoint.trim()) { + alert('Server endpoint is required'); + return false; + } + + // Validate client count + if (clientCount < 1 || clientCount > 50) { + alert('Client count must be between 1 and 50'); + return false; + } + + return true; +} + +function generateServerConfig(name, privateKey, port, serverIP, cidr, clients) { + let config = `# ${name} Configuration +[Interface] +PrivateKey = ${privateKey} +Address = ${serverIP}/${cidr} +ListenPort = ${port} +SaveConfig = true + +# PostUp and PostDown rules for NAT +PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE +PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE + +`; + + clients.forEach(client => { + config += `# ${client.name} +[Peer] +PublicKey = ${client.publicKey} +PresharedKey = ${client.presharedKey} +AllowedIPs = ${client.ip}/32 + +`; + }); + + return config; +} + +function generateClientConfig(client, serverPublicKey, serverEndpoint, serverPort, serverIP, cidr, dns) { + let config = `# ${client.name} Configuration +[Interface] +PrivateKey = ${client.privateKey} +Address = ${client.ip}/${cidr}`; + + // Only add DNS if it's not empty + if (dns && dns.trim()) { + config += ` +DNS = ${dns.trim()}`; + } + + config += ` + +[Peer] +PublicKey = ${serverPublicKey} +PresharedKey = ${client.presharedKey} +AllowedIPs = 0.0.0.0/0, ::/0 +Endpoint = ${serverEndpoint}:${serverPort} +PersistentKeepalive = 25 +`; + + return config; +} + +function displayConfigurations() { + // Show server config + document.getElementById('serverConfig').textContent = currentConfigs.server; + + // Generate client configs in row layout + const clientConfigs = document.getElementById('clientConfigs'); + clientConfigs.innerHTML = ''; + + currentConfigs.clients.forEach((client, index) => { + // Create client config item + const configItem = document.createElement('div'); + configItem.className = 'client-config-item'; + configItem.innerHTML = ` +
${client.name}
+
${client.config}
+ + `; + clientConfigs.appendChild(configItem); + }); + + document.getElementById('configOutput').style.display = 'block'; +} + +function downloadConfig(name, content) { + const blob = new Blob([content], { type: 'text/plain' }); + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `${name}.conf`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + window.URL.revokeObjectURL(url); +} + +// Seed management functions +function generateNewSeed() { + const seed = crypto.generateSeed(); + currentSeed = seed; + const hexSeed = crypto.arrayToHex(seed); + document.getElementById('seedDisplay').textContent = hexSeed; + document.getElementById('customSeed').value = hexSeed; +} + +function copySeed() { + if (currentSeed) { + const hexSeed = crypto.arrayToHex(currentSeed); + navigator.clipboard.writeText(hexSeed).then(() => { + alert('Seed copied to clipboard!'); + }).catch(() => { + // Fallback for older browsers + const textArea = document.createElement('textarea'); + textArea.value = hexSeed; + document.body.appendChild(textArea); + textArea.select(); + document.execCommand('copy'); + document.body.removeChild(textArea); + alert('Seed copied to clipboard!'); + }); + } else { + alert('No seed to copy. Generate configurations first.'); + } +} + +function pasteSeed() { + navigator.clipboard.readText().then(text => { + if (text && text.length === 64 && /^[0-9a-fA-F]+$/.test(text)) { + document.getElementById('customSeed').value = text; + document.getElementById('seedDisplay').textContent = text; + // Update current seed + try { + currentSeed = crypto.hexToArray(text); + alert('Seed pasted successfully!'); + } catch (error) { + alert('Invalid seed format.'); + } + } else { + alert('Invalid seed in clipboard. Must be 64-character hex string.'); + } + }).catch(() => { + alert('Cannot read from clipboard. Please paste manually into the Custom Seed field.'); + }); +} + +// Initialize form validation +document.addEventListener('DOMContentLoaded', function() { + const form = document.getElementById('configForm'); + if (form) { + form.addEventListener('submit', function(e) { + e.preventDefault(); + generateConfigs(); + }); + } + + const meshForm = document.getElementById('meshConfigForm'); + if (meshForm) { + meshForm.addEventListener('submit', function(e) { + e.preventDefault(); + generateMeshConfigs(); + }); + + // Initialize with default peer fields for mesh + updatePeerFields(); + } + + // Validate custom seed input + const customSeedInput = document.getElementById('customSeed'); + if (customSeedInput) { + customSeedInput.addEventListener('input', function(e) { + const value = e.target.value.trim(); + if (value && (value.length !== 64 || !/^[0-9a-fA-F]*$/.test(value))) { + e.target.style.borderColor = '#dc3545'; + } else { + e.target.style.borderColor = '#ddd'; + // Update current seed and display when valid seed is entered + if (value && value.length === 64 && /^[0-9a-fA-F]+$/.test(value)) { + try { + currentSeed = crypto.hexToArray(value); + document.getElementById('seedDisplay').textContent = value; + } catch (error) { + console.warn('Invalid hex seed:', error); + } + } + } + }); + + // Handle clearing the custom seed input + customSeedInput.addEventListener('keydown', function(e) { + if (e.key === 'Delete' || e.key === 'Backspace') { + // If user is clearing the field, don't update currentSeed immediately + // Let them finish editing first + } + }); + } +}); + +// === MESH NETWORK FUNCTIONS === + +function updatePeerFields() { + const peerCount = parseInt(document.getElementById('peerCount').value); + const peerInputs = document.getElementById('peerInputs'); + + if (peerCount < 2 || peerCount > 20) { + alert('Number of peers must be between 2 and 20'); + return; + } + + peerInputs.innerHTML = ''; + + for (let i = 1; i <= peerCount; i++) { + const peerGroup = document.createElement('div'); + peerGroup.className = 'peer-input-group'; + peerGroup.innerHTML = ` +

Peer ${i}

+
+
+ + +
+
+ + +
+
+ + +
+
+ `; + peerInputs.appendChild(peerGroup); + } +} + +async function generateMeshConfigs() { + try { + const networkCIDR = document.getElementById('networkCIDR').value; + const dns = document.getElementById('dns').value; + const peerCount = parseInt(document.getElementById('peerCount').value); + + // Validate inputs + if (!validateMeshInputs(networkCIDR, peerCount)) { + return; + } + + // Show loading state + const generateBtn = document.querySelector('button[onclick="generateMeshConfigs()"]'); + const originalText = generateBtn.textContent; + generateBtn.textContent = 'Generating...'; + generateBtn.disabled = true; + + // Generate or use existing seed + const customSeedHex = document.getElementById('customSeed').value; + let seed; + + if (customSeedHex && customSeedHex.trim()) { + // Use custom seed from input + seed = crypto.parseSeed(customSeedHex); + } else if (currentSeed) { + // Use existing seed if available + seed = currentSeed; + } else { + // Generate new seed only if none exists + seed = crypto.generateSeed(); + } + + currentSeed = seed; + + // Update seed display and input field + const seedHex = crypto.arrayToHex(seed); + document.getElementById('seedDisplay').textContent = seedHex; + if (!customSeedHex || !customSeedHex.trim()) { + document.getElementById('customSeed').value = seedHex; + } + + // Reset key counter for deterministic generation + crypto.resetKeyCounter(); + + // Parse network + const [networkBase, cidr] = networkCIDR.split('/'); + const networkParts = networkBase.split('.').map(n => parseInt(n)); + + // Collect peer information + const peers = []; + for (let i = 1; i <= peerCount; i++) { + const name = document.getElementById(`peerName${i}`).value.trim(); + const endpoint = document.getElementById(`peerEndpoint${i}`).value.trim(); + const port = parseInt(document.getElementById(`peerPort${i}`).value); + + if (!name) { + alert(`Peer ${i} name is required`); + // Restore button + generateBtn.textContent = originalText; + generateBtn.disabled = false; + return; + } + + const privateKey = await crypto.generatePrivateKey(seed); + const publicKey = crypto.generatePublicKey(privateKey); + const ip = `${networkParts[0]}.${networkParts[1]}.${networkParts[2]}.${networkParts[3] + i}`; + + peers.push({ + name: name, + privateKey: privateKey, + publicKey: publicKey, + endpoint: endpoint, + port: port, + ip: ip + }); + } + + // Generate preshared keys for each pair of peers + const presharedKeys = {}; + for (let i = 0; i < peers.length; i++) { + for (let j = i + 1; j < peers.length; j++) { + const key = `${i}-${j}`; + presharedKeys[key] = await crypto.generatePresharedKey(seed); + } + } + + // Generate configurations for each peer + const meshConfigs = peers.map((peer, index) => ({ + name: peer.name, + config: generateMeshPeerConfig(peer, peers, index, cidr, dns, presharedKeys) + })); + + // Store configurations + currentMeshConfigs = meshConfigs; + + // Display configurations + displayMeshConfigurations(); + + // Restore button + generateBtn.textContent = originalText; + generateBtn.disabled = false; + + } catch (error) { + // Restore button on error + const generateBtn = document.querySelector('button[onclick="generateMeshConfigs()"]'); + if (generateBtn) { + generateBtn.textContent = 'Generate Mesh Configurations'; + generateBtn.disabled = false; + } + alert('Error generating mesh configurations: ' + error.message); + } +} + +function validateMeshInputs(networkCIDR, peerCount) { + // Validate network CIDR + const cidrRegex = /^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}\/([1-2]?[0-9]|3[0-2])$/; + if (!cidrRegex.test(networkCIDR)) { + alert('Invalid network CIDR format'); + return false; + } + + // Validate peer count + if (peerCount < 2 || peerCount > 20) { + alert('Number of peers must be between 2 and 20'); + return false; + } + + return true; +} + +function generateMeshPeerConfig(currentPeer, allPeers, currentIndex, cidr, dns, presharedKeys) { + let config = `# ${currentPeer.name} Configuration (Mesh Network) +[Interface] +PrivateKey = ${currentPeer.privateKey} +Address = ${currentPeer.ip}/${cidr}`; + + // Only add DNS if it's not empty + if (dns && dns.trim()) { + config += ` +DNS = ${dns.trim()}`; + } + + config += ` +ListenPort = ${currentPeer.port} + +`; + + // Add all other peers as peers + allPeers.forEach((peer, peerIndex) => { + if (peerIndex !== currentIndex) { + // Get preshared key for this pair + const minIndex = Math.min(currentIndex, peerIndex); + const maxIndex = Math.max(currentIndex, peerIndex); + const presharedKey = presharedKeys[`${minIndex}-${maxIndex}`]; + + config += `# ${peer.name} +[Peer] +PublicKey = ${peer.publicKey} +PresharedKey = ${presharedKey} +AllowedIPs = ${peer.ip}/32`; + + // Add endpoint if available + if (peer.endpoint) { + config += ` +Endpoint = ${peer.endpoint}:${peer.port}`; + } + + config += ` +PersistentKeepalive = 25 + +`; + } + }); + + return config; +} + +function displayMeshConfigurations() { + const meshConfigs = document.getElementById('meshConfigs'); + meshConfigs.innerHTML = ''; + + currentMeshConfigs.forEach((peerConfig, index) => { + const configItem = document.createElement('div'); + configItem.className = 'client-config-item'; + configItem.innerHTML = ` +
${peerConfig.name}
+
${peerConfig.config}
+ + `; + meshConfigs.appendChild(configItem); + }); + + document.getElementById('configOutput').style.display = 'block'; +} \ No newline at end of file diff --git a/static/styles.css b/static/styles.css new file mode 100644 index 0000000..1822b5c --- /dev/null +++ b/static/styles.css @@ -0,0 +1,312 @@ +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + max-width: 800px; + margin: 0 auto; + padding: 20px; + background-color: #f5f5f5; +} + +.container { + background: white; + border-radius: 8px; + padding: 30px; + box-shadow: 0 2px 10px rgba(0,0,0,0.1); +} + +.navigation { + margin-bottom: 20px; +} + +.nav-link { + color: #007bff; + text-decoration: none; + font-weight: 500; + padding: 8px 12px; + border: 1px solid #007bff; + border-radius: 4px; + transition: all 0.2s; +} + +.nav-link:hover { + background-color: #007bff; + color: white; +} + +h1 { + color: #333; + text-align: center; + margin-bottom: 30px; +} + +.privacy-disclaimer { + background-color: #e8f5e8; + border: 1px solid #28a745; + border-radius: 6px; + padding: 12px 16px; + margin-bottom: 25px; + color: #155724; + font-size: 14px; + line-height: 1.4; +} + +.privacy-disclaimer strong { + color: #0d4b14; +} + +.description { + text-align: center; + color: #666; + margin-bottom: 30px; + font-style: italic; +} + +.form-group { + margin-bottom: 20px; +} + +label { + display: block; + margin-bottom: 5px; + font-weight: 600; + color: #555; +} + +input, select { + width: 100%; + padding: 10px; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 14px; + box-sizing: border-box; +} + +button { + background-color: #007bff; + color: white; + padding: 12px 30px; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 16px; + margin: 10px 5px; +} + +button:hover { + background-color: #0056b3; +} + +.config-output { + margin-top: 30px; +} + +.config-section { + margin-bottom: 30px; + padding: 20px; + background-color: #f8f9fa; + border-radius: 6px; + border-left: 4px solid #007bff; +} + +.config-content { + background-color: #2d3748; + color: #e2e8f0; + padding: 15px; + border-radius: 4px; + font-family: 'Courier New', monospace; + font-size: 13px; + line-height: 1.4; + white-space: pre-wrap; + overflow-x: auto; +} + +.download-btn { + background-color: #28a745; + font-size: 14px; + padding: 8px 16px; +} + +.download-btn:hover { + background-color: #218838; +} + +.client-configs-row { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); + gap: 20px; + margin-top: 20px; +} + +.client-config-item { + background-color: #f8f9fa; + border-radius: 6px; + padding: 15px; + border-left: 4px solid #28a745; +} + +.client-config-title { + font-weight: 600; + color: #333; + margin-bottom: 10px; + font-size: 16px; +} + +.error { + color: #dc3545; + font-size: 14px; + margin-top: 5px; +} + +.form-row { + display: flex; + gap: 20px; +} + +.form-row .form-group { + flex: 1; +} + +.seed-section { + background-color: #f8f9fa; + border: 1px solid #dee2e6; + border-radius: 6px; + padding: 15px; + margin-bottom: 20px; +} + +.seed-display { + font-family: 'Courier New', monospace; + background-color: #e9ecef; + padding: 8px; + border-radius: 4px; + font-size: 12px; + word-break: break-all; + border: 1px solid #ced4da; +} + +.seed-buttons { + margin-top: 10px; +} + +.seed-btn { + background-color: #6c757d; + font-size: 12px; + padding: 6px 12px; + margin-right: 5px; +} + +.seed-btn:hover { + background-color: #545b62; +} + +/* === MESH-SPECIFIC STYLES === */ + +.form-section { + margin-bottom: 30px; + padding: 20px; + background-color: #f8f9fa; + border-radius: 6px; + border-left: 4px solid #007bff; +} + +.form-section h3 { + margin-top: 0; + color: #333; + margin-bottom: 15px; +} + +#updatePeersBtn { + background-color: #6c757d; + font-size: 14px; + padding: 8px 16px; + margin: 5px 0 0 10px; +} + +#updatePeersBtn:hover { + background-color: #545b62; +} + + +.peer-input-group { + background-color: #ffffff; + border: 1px solid #dee2e6; + border-radius: 6px; + padding: 15px; + margin-bottom: 15px; +} + +.peer-input-group h4 { + margin-top: 0; + margin-bottom: 15px; + color: #495057; + font-size: 16px; +} + +.peer-row { + display: grid; + grid-template-columns: 1fr 1fr 100px; + gap: 15px; + align-items: end; +} + +@media (max-width: 768px) { + .peer-row { + grid-template-columns: 1fr; + gap: 10px; + } + + .form-row { + flex-direction: column; + gap: 0; + } + + .client-configs-row { + grid-template-columns: 1fr; + } +} + +.footer { + margin-top: 50px; + background-color: #343a40; + color: #fff; + padding: 30px 0; +} + +.footer-content { + max-width: 800px; + margin: 0 auto; + padding: 0 20px; + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 30px; +} + +.footer-column h4 { + margin-bottom: 15px; + color: #fff; + font-size: 16px; +} + +.footer-column p { + margin: 0; + color: #adb5bd; + font-size: 14px; + line-height: 1.5; +} + +.footer-column a { + color: #007bff; + text-decoration: none; +} + +.footer-column a:hover { + color: #0056b3; + text-decoration: underline; +} + +@media (max-width: 768px) { + .footer-content { + grid-template-columns: 1fr; + gap: 20px; + text-align: center; + } +} \ No newline at end of file