Initial commit: WireGuard config generator (génération de clés côté navigateur)

This commit is contained in:
florian committed 2026-08-27 15:06:00 +02:00
commit 581a7c9ed3
11 files changed
+3014

No files matched your search

+257
View File
@@ -0,0 +1,257 @@
(() => {
"use strict";
// ---------- base64 helpers ----------
function toBase64(bytes) {
let bin = "";
for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
return btoa(bin);
}
// ---------- WireGuard key generation (X25519 via TweetNaCl) ----------
function genKeyPair() {
const kp = nacl.box.keyPair();
return {
privateKey: toBase64(kp.secretKey),
publicKey: toBase64(kp.publicKey),
};
}
function genPresharedKey() {
return toBase64(nacl.randomBytes(32));
}
// ---------- IP helpers (IPv4 only) ----------
function parseCidr(cidr) {
const [ip, prefixStr] = cidr.trim().split("/");
const prefix = parseInt(prefixStr, 10);
const parts = ip.split(".").map(Number);
if (parts.length !== 4 || parts.some((p) => isNaN(p) || p < 0 || p > 255) || isNaN(prefix)) {
throw new Error("Réseau CIDR invalide: " + cidr);
}
const base = ((parts[0] << 24) >>> 0) + (parts[1] << 16) + (parts[2] << 8) + parts[3];
return { base: base >>> 0, prefix };
}
function intToIp(int) {
return [
(int >>> 24) & 255,
(int >>> 16) & 255,
(int >>> 8) & 255,
int & 255,
].join(".");
}
function nthAddress(cidr, n) {
const { base, prefix } = parseCidr(cidr);
const hostBits = 32 - prefix;
const maxHosts = Math.pow(2, hostBits);
if (n >= maxHosts) throw new Error("Le réseau " + cidr + " est trop petit pour ce nombre de pairs.");
const network = (base >>> hostBits) << hostBits;
return intToIp((network >>> 0) + n);
}
// ---------- DOM ----------
const clientsList = document.getElementById("clients-list");
const addClientBtn = document.getElementById("add-client");
const generateBtn = document.getElementById("generate-btn");
const resetBtn = document.getElementById("reset-btn");
const resultsEl = document.getElementById("results");
const serverSummary = document.getElementById("server-summary");
const serverConfText = document.getElementById("server-conf-text");
let clientCounter = 0;
function addClientRow(name) {
clientCounter++;
const row = document.createElement("div");
row.className = "client-row";
row.innerHTML = `
<input type="text" class="client-name" placeholder="Nom du client" value="${name || "client" + clientCounter}" />
<button class="btn-danger remove-client" title="Supprimer">✕</button>
`;
row.querySelector(".remove-client").addEventListener("click", () => {
row.remove();
});
clientsList.appendChild(row);
}
addClientBtn.addEventListener("click", () => addClientRow());
addClientRow("client1");
resetBtn.addEventListener("click", () => {
clientsList.innerHTML = "";
clientCounter = 0;
addClientRow("client1");
resultsEl.innerHTML = "";
serverSummary.style.display = "none";
});
document.addEventListener("click", (e) => {
if (e.target.matches("[data-copy-target]")) {
const targetId = e.target.getAttribute("data-copy-target");
const el = document.getElementById(targetId);
const text = el.tagName === "TEXTAREA" || el.tagName === "INPUT" ? el.value : el.textContent;
navigator.clipboard.writeText(text).then(() => {
const original = e.target.textContent;
e.target.textContent = "Copié !";
setTimeout(() => (e.target.textContent = original), 1200);
});
}
});
function buildClientConfig({ clientPrivateKey, clientIp, network, dns, serverPubKey, endpoint, port, allowedIps, psk }) {
const prefixLen = network.split("/")[1];
let conf = `[Interface]\n`;
conf += `PrivateKey = ${clientPrivateKey}\n`;
conf += `Address = ${clientIp}/${prefixLen}\n`;
if (dns) conf += `DNS = ${dns}\n`;
conf += `\n[Peer]\n`;
conf += `PublicKey = ${serverPubKey}\n`;
if (psk) conf += `PresharedKey = ${psk}\n`;
conf += `Endpoint = ${endpoint}:${port}\n`;
conf += `AllowedIPs = ${allowedIps}\n`;
conf += `PersistentKeepalive = 25\n`;
return conf;
}
function buildServerPeerBlock({ name, clientPubKey, clientIp, psk }) {
let block = `# ${name}\n[Peer]\n`;
block += `PublicKey = ${clientPubKey}\n`;
if (psk) block += `PresharedKey = ${psk}\n`;
block += `AllowedIPs = ${clientIp}/32\n`;
return block;
}
function renderQr(container, text) {
try {
const qr = qrcode(0, "M");
qr.addData(text);
qr.make();
container.innerHTML = qr.createSvgTag(4, 8);
} catch (err) {
container.textContent = "QR indisponible (config trop longue)";
}
}
function downloadFile(filename, content) {
const blob = new Blob([content], { type: "text/plain;charset=utf-8" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
generateBtn.addEventListener("click", () => {
resultsEl.innerHTML = "";
serverSummary.style.display = "none";
try {
const endpoint = document.getElementById("srv-endpoint").value.trim();
const port = document.getElementById("srv-port").value.trim() || "51820";
const network = document.getElementById("srv-network").value.trim();
const serverIp = document.getElementById("srv-server-ip").value.trim();
const dns = document.getElementById("srv-dns").value.trim();
const allowedIps = document.getElementById("srv-allowed-ips").value.trim() || "0.0.0.0/0, ::/0";
const usePsk = document.getElementById("use-psk").checked;
let serverPubKeyInput = document.getElementById("srv-pubkey").value.trim();
if (!endpoint) {
alert("Merci de renseigner l'endpoint du serveur.");
return;
}
parseCidr(network); // validate
const clientNames = Array.from(document.querySelectorAll(".client-name"))
.map((i) => i.value.trim())
.filter((v) => v.length > 0);
if (clientNames.length === 0) {
alert("Ajoutez au moins un client.");
return;
}
let serverKeyPair = null;
let serverPubKey = serverPubKeyInput;
if (!serverPubKey) {
serverKeyPair = genKeyPair();
serverPubKey = serverKeyPair.publicKey;
}
const serverPeerBlocks = [];
const resultCards = [];
clientNames.forEach((name, idx) => {
const clientIp = nthAddress(network, idx + 2); // .1 reserved for server
const clientKeys = genKeyPair();
const psk = usePsk ? genPresharedKey() : null;
const clientConf = buildClientConfig({
clientPrivateKey: clientKeys.privateKey,
clientIp,
network,
dns,
serverPubKey,
endpoint,
port,
allowedIps,
psk,
});
serverPeerBlocks.push(
buildServerPeerBlock({ name, clientPubKey: clientKeys.publicKey, clientIp, psk })
);
resultCards.push({ name, clientIp, clientConf });
});
// Server config summary
let serverConf = `[Interface]\n`;
serverConf += `Address = ${serverIp}/${network.split("/")[1]}\n`;
serverConf += `ListenPort = ${port}\n`;
if (serverKeyPair) {
serverConf += `PrivateKey = ${serverKeyPair.privateKey}\n`;
} else {
serverConf += `PrivateKey = <votre clé privée serveur existante>\n`;
}
serverConf += `\n` + serverPeerBlocks.join("\n");
serverConfText.textContent = serverConf;
serverSummary.style.display = "block";
resultCards.forEach((card, i) => {
const wrapper = document.createElement("div");
wrapper.className = "panel result-card";
const preId = `client-conf-${i}`;
wrapper.innerHTML = `
<h3>${card.name} — ${card.clientIp}</h3>
<div class="result-body">
<pre id="${preId}">${card.clientConf}</pre>
<div class="qr-wrap">
<div class="qr-canvas"></div>
<span class="hint">Scanner avec l'app WireGuard mobile</span>
</div>
</div>
<div class="actions">
<button class="btn-secondary copy-btn" data-copy-target="${preId}">Copier la config</button>
<button class="btn-secondary download-btn">Télécharger .conf</button>
</div>
`;
wrapper.querySelector(".download-btn").addEventListener("click", () => {
downloadFile(`${card.name}.conf`, card.clientConf);
});
renderQr(wrapper.querySelector(".qr-canvas"), card.clientConf);
resultsEl.appendChild(wrapper);
});
resultsEl.scrollIntoView({ behavior: "smooth", block: "start" });
} catch (err) {
alert(err.message || String(err));
}
});
})();