Initial commit: WireGuard config generator (génération de clés côté navigateur)
This commit is contained in:
commit
581a7c9ed3
11 files changed
+3014
No files matched your search
+257
@@ -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));
|
||||
}
|
||||
});
|
||||
})();
|
||||
Reference in new issue
Block a user