907 lines
55 KiB
HTML
907 lines
55 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="de">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>Fritzbox Reverse WireGuard Tunnel Generator</title>
|
|
<style>
|
|
:root {
|
|
--bg: #f6f7f9;
|
|
--fg: #1d1f23;
|
|
--muted: #5f6671;
|
|
--border: #d8dde3;
|
|
--card: #ffffff;
|
|
--accent: #2563eb;
|
|
--accent-fg: #ffffff;
|
|
--hint: #8b6914;
|
|
--hint-bg: #fff8e1;
|
|
--code-bg: #0f172a;
|
|
--code-fg: #e2e8f0;
|
|
}
|
|
* { box-sizing: border-box; }
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
|
|
max-width: 920px;
|
|
margin: 1.5em auto;
|
|
padding: 0 1em 4em;
|
|
background: var(--bg);
|
|
color: var(--fg);
|
|
line-height: 1.5;
|
|
}
|
|
h1 { font-size: 1.6em; margin: 0 0 0.3em; }
|
|
h2 { font-size: 1.25em; margin: 1.6em 0 0.6em; }
|
|
h3 { font-size: 1.05em; margin: 1.2em 0 0.4em; }
|
|
h3 small { color: var(--muted); font-weight: normal; font-size: 0.85em; }
|
|
header p { color: var(--muted); margin: 0.4em 0; }
|
|
fieldset {
|
|
background: var(--card);
|
|
border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
padding: 1em 1.25em 1.25em;
|
|
margin: 0 0 1em;
|
|
}
|
|
legend { padding: 0 0.4em; font-weight: 600; }
|
|
label { display: block; margin: 0.6em 0 0.2em; font-size: 0.9em; }
|
|
label input[type="checkbox"] { margin-right: 0.4em; }
|
|
input[type="text"], input[type="number"] {
|
|
width: 100%;
|
|
padding: 0.45em 0.6em;
|
|
font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
|
|
font-size: 0.9em;
|
|
border: 1px solid var(--border);
|
|
border-radius: 5px;
|
|
background: #fff;
|
|
color: var(--fg);
|
|
}
|
|
input[readonly] { background: #f0f2f5; color: var(--muted); }
|
|
.row { display: grid; grid-template-columns: 1fr 1fr; gap: 0.75em; }
|
|
@media (max-width: 600px) { .row { grid-template-columns: 1fr; } }
|
|
button {
|
|
font: inherit;
|
|
padding: 0.5em 1em;
|
|
border: 1px solid var(--border);
|
|
border-radius: 5px;
|
|
background: #fff;
|
|
cursor: pointer;
|
|
}
|
|
button:hover { background: #eef0f3; }
|
|
button.primary, button[type="submit"] {
|
|
background: var(--accent);
|
|
color: var(--accent-fg);
|
|
border-color: var(--accent);
|
|
font-weight: 600;
|
|
}
|
|
button.primary:hover, button[type="submit"]:hover { background: #1d4ed8; }
|
|
hr { border: none; border-top: 1px solid var(--border); margin: 1em 0; }
|
|
.hint {
|
|
background: var(--hint-bg);
|
|
color: var(--hint);
|
|
padding: 0.5em 0.75em;
|
|
border-left: 3px solid var(--hint);
|
|
border-radius: 4px;
|
|
font-size: 0.88em;
|
|
margin: 0.4em 0;
|
|
}
|
|
pre {
|
|
background: var(--code-bg);
|
|
color: var(--code-fg);
|
|
padding: 0.9em 1em;
|
|
border-radius: 6px;
|
|
overflow-x: auto;
|
|
font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
|
|
font-size: 0.85em;
|
|
line-height: 1.45;
|
|
}
|
|
code {
|
|
background: #ebeef2;
|
|
padding: 0.1em 0.35em;
|
|
border-radius: 3px;
|
|
font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
|
|
font-size: 0.9em;
|
|
}
|
|
.conf-block {
|
|
background: var(--card);
|
|
border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
padding: 1em 1.25em;
|
|
margin: 0 0 1em;
|
|
}
|
|
.qr { padding: 1em 0; }
|
|
.qr img { max-width: 280px; height: auto; image-rendering: pixelated; }
|
|
details summary { cursor: pointer; color: var(--accent); font-size: 0.9em; padding: 0.4em 0; }
|
|
#instructions ol { padding-left: 1.4em; }
|
|
#instructions li { margin: 0.4em 0; }
|
|
#instructions h3 { border-bottom: 1px solid var(--border); padding-bottom: 0.2em; }
|
|
.toast {
|
|
position: fixed; bottom: 1em; right: 1em;
|
|
background: #1d1f23; color: #fff;
|
|
padding: 0.5em 0.9em; border-radius: 5px;
|
|
font-size: 0.9em; opacity: 0; transition: opacity 0.2s;
|
|
pointer-events: none; z-index: 100;
|
|
}
|
|
.toast.show { opacity: 1; }
|
|
.err { color: #b91c1c; font-size: 0.88em; margin: 0.3em 0; }
|
|
footer { color: var(--muted); font-size: 0.8em; text-align: center; margin-top: 2em; }
|
|
footer a { color: var(--muted); }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<header>
|
|
<h1>Fritzbox ↔ Windows Reverse WireGuard Tunnel</h1>
|
|
<p>Erzeugt WireGuard-Konfigurationen für ein Reverse-Setup: der Windows-PC ist WireGuard-<em>Server</em> (öffnet UDP-Port), die Fritzbox baut als <em>Client</em> ausgehend zum Windows auf. Vorteil: dynamische IPs auf der Fritzbox-Seite stören nicht mehr — die Fritzbox initiiert den Tunnel und reconnectet selbst bei IP-Wechsel.</p>
|
|
<p><strong>Single-File-App</strong> — alle Berechnungen laufen lokal im Browser. Keine Netzwerkverbindungen. Diese Datei kann offline gespeichert und verwendet werden.</p>
|
|
</header>
|
|
|
|
<form id="form">
|
|
|
|
<fieldset>
|
|
<legend>1. Schlüssel</legend>
|
|
|
|
<p>
|
|
<button type="button" id="genAll" class="primary">Alle Schlüssel neu generieren</button>
|
|
<button type="button" id="genWin">Nur Windows-Schlüssel</button>
|
|
<button type="button" id="genFb">Nur Fritzbox-Schlüssel</button>
|
|
<button type="button" id="genPsk">Nur Pre-Shared Key</button>
|
|
</p>
|
|
<div id="keyErr" class="err" hidden></div>
|
|
|
|
<h3>Windows (WireGuard-Server)</h3>
|
|
<div class="row">
|
|
<label>Private Key (Base64, 32 Byte)
|
|
<input type="text" id="winPriv" autocomplete="off" spellcheck="false" required>
|
|
</label>
|
|
<label>Public Key
|
|
<input type="text" id="winPub" autocomplete="off" spellcheck="false" readonly>
|
|
</label>
|
|
</div>
|
|
|
|
<h3>Fritzbox (WireGuard-Client)</h3>
|
|
<p>
|
|
<label><input type="checkbox" id="fbExisting"> Auf der Fritzbox existiert bereits eine WireGuard-Verbindung — den vorhandenen Private Key verwenden, statt einen neuen zu generieren.</label>
|
|
</p>
|
|
<p class="hint" id="fbExistingHint" hidden>Die Fritzbox lehnt den Import einer kompletten <code>.conf</code> ab, sobald schon ein Schlüsselpaar existiert. In diesem Fall trage hier den existierenden Fritzbox-Private-Key ein. Den findest du in der ursprünglich von der Fritzbox erzeugten <code>.conf</code>-Datei für einen ihrer Peers (Zeile <code>PrivateKey = ...</code> — das ist der Schlüssel der Fritzbox selbst, der in jeder ihrer Peer-Configs identisch eingetragen ist). Der Public Key wird daraus abgeleitet und im Output siehst du dann die Werte, die du im Fritzbox-Wizard <em>„Peer hinzufügen“</em> eintragen musst.</p>
|
|
<div class="row">
|
|
<label>Private Key (Base64, 32 Byte)
|
|
<input type="text" id="fbPriv" autocomplete="off" spellcheck="false" required>
|
|
</label>
|
|
<label>Public Key
|
|
<input type="text" id="fbPub" autocomplete="off" spellcheck="false" readonly>
|
|
</label>
|
|
</div>
|
|
|
|
<p style="margin-top:1em;">
|
|
<label><input type="checkbox" id="usePsk" checked> Pre-Shared Key verwenden (empfohlen, Quanten-Resistenz für die Tunnel-Verschlüsselung)</label>
|
|
</p>
|
|
<label>Pre-Shared Key
|
|
<input type="text" id="psk" autocomplete="off" spellcheck="false">
|
|
</label>
|
|
</fieldset>
|
|
|
|
<fieldset>
|
|
<legend>2. Netzwerk</legend>
|
|
|
|
<div class="row">
|
|
<label>Endpoint Windows-PC (DynDNS-Hostname oder IP)
|
|
<input type="text" id="endpoint" placeholder="meinpc.dyndns.org" required>
|
|
</label>
|
|
<label>UDP-Port auf Windows-Seite
|
|
<input type="number" id="port" value="51820" min="1" max="65535" required>
|
|
</label>
|
|
</div>
|
|
|
|
<hr>
|
|
|
|
<label>Tunnel-Subnetz (CIDR, intern für die WG-Interfaces)
|
|
<input type="text" id="tunnelNet" value="10.10.10.0/24" required>
|
|
</label>
|
|
<div class="row">
|
|
<label>Tunnel-IP Windows
|
|
<input type="text" id="winTunnelIp" value="10.10.10.1" required>
|
|
</label>
|
|
<label>Tunnel-IP Fritzbox
|
|
<input type="text" id="fbTunnelIp" value="10.10.10.2" required>
|
|
</label>
|
|
</div>
|
|
|
|
<hr>
|
|
|
|
<label>Fritzbox-LAN — Zielnetz, das vom Windows-PC durch den Tunnel erreichbar werden soll
|
|
<input type="text" id="fbLan" value="192.168.178.0/24" required>
|
|
</label>
|
|
<label>PersistentKeepalive (Sekunden, hält NAT-Mapping offen; 0 = deaktiviert)
|
|
<input type="number" id="keepalive" value="25" min="0" max="600" required>
|
|
</label>
|
|
<div id="netErr" class="err" hidden></div>
|
|
</fieldset>
|
|
|
|
<fieldset>
|
|
<legend>3. Konfiguration speichern / laden</legend>
|
|
<p style="margin-top:0; color:var(--muted); font-size:0.9em;">Verschlüsseltes Token mit allen oben eingetragenen Werten (inkl. Schlüsseln und PSK). Mit dem Token + Passwort kannst du die Konfiguration jederzeit wieder herstellen und neue <code>.conf</code>-Dateien exportieren. Verschlüsselung: AES-256-GCM, Schlüsselableitung PBKDF2-HMAC-SHA256 mit 600.000 Iterationen — alles lokal im Browser.</p>
|
|
|
|
<div class="row">
|
|
<label>Passwort
|
|
<input type="password" id="savePassword" autocomplete="off">
|
|
</label>
|
|
<label style="display:flex; align-items:flex-end;">
|
|
<span style="display:flex; gap:0.4em;">
|
|
<button type="button" id="saveBtn">Token erzeugen</button>
|
|
<button type="button" id="loadBtn">Token laden</button>
|
|
</span>
|
|
</label>
|
|
</div>
|
|
|
|
<label>Token
|
|
<textarea id="saveToken" rows="3" autocomplete="off" spellcheck="false" style="width:100%; padding:0.45em 0.6em; font-family:ui-monospace,Menlo,Consolas,monospace; font-size:0.85em; border:1px solid var(--border); border-radius:5px; resize:vertical;"></textarea>
|
|
</label>
|
|
<p>
|
|
<button type="button" data-action="copy" data-target="saveToken">Token kopieren</button>
|
|
</p>
|
|
<div id="saveErr" class="err" hidden></div>
|
|
</fieldset>
|
|
|
|
<p>
|
|
<button type="submit" class="primary">Konfigurationen erzeugen</button>
|
|
</p>
|
|
</form>
|
|
|
|
<section id="output" hidden>
|
|
<h2>Konfigurationen</h2>
|
|
|
|
<div class="conf-block">
|
|
<h3>Windows-Konfiguration <small>— in den WireGuard-Windows-Client importieren</small></h3>
|
|
<pre id="winConf"></pre>
|
|
<p>
|
|
<button type="button" data-action="copy" data-target="winConf">In Zwischenablage kopieren</button>
|
|
<button type="button" data-action="download" data-target="winConf" data-filename="windows-server.conf">Als .conf herunterladen</button>
|
|
</p>
|
|
<details>
|
|
<summary>QR-Code anzeigen</summary>
|
|
<div id="winQr" class="qr"></div>
|
|
</details>
|
|
</div>
|
|
|
|
<div class="conf-block">
|
|
<h3>Fritzbox <small id="fbSubtitle">— in der Fritzbox-GUI als WireGuard-Verbindung importieren</small></h3>
|
|
|
|
<div id="fbManualBox" hidden>
|
|
<p class="hint">Auf der Fritzbox existiert bereits eine WireGuard-Verbindung. Folgenden <code>[Peer]</code>-Block in eine neue <code>.conf</code>-Datei speichern und in der Fritzbox als WireGuard-Verbindung importieren — die Fritzbox erkennt, dass es sich nur um einen zusätzlichen Peer handelt, und ergänzt ihn an die bestehende Verbindung. Die alte Verbindung muss <strong>nicht</strong> gelöscht werden.</p>
|
|
<pre id="fbManualValues"></pre>
|
|
<p>
|
|
<button type="button" data-action="copy" data-target="fbManualValues">Peer-Block kopieren</button>
|
|
<button type="button" data-action="download" data-target="fbManualValues" data-filename="fritzbox-peer.conf">Als peer.conf herunterladen</button>
|
|
</p>
|
|
<hr>
|
|
<h4 style="font-size:0.95em; margin:0.4em 0; color:var(--muted)">Komplette WireGuard-Konfiguration <small>(zum Nachschlagen / falls die Fritzbox-Verbindung mal neu aufgesetzt wird)</small></h4>
|
|
</div>
|
|
|
|
<pre id="fbConf"></pre>
|
|
<p>
|
|
<button type="button" data-action="copy" data-target="fbConf">In Zwischenablage kopieren</button>
|
|
<button type="button" data-action="download" data-target="fbConf" data-filename="fritzbox-client.conf">Als .conf herunterladen</button>
|
|
</p>
|
|
<details>
|
|
<summary>QR-Code anzeigen</summary>
|
|
<div id="fbQr" class="qr"></div>
|
|
</details>
|
|
</div>
|
|
|
|
<h2>Setup-Anleitung</h2>
|
|
<div id="instructions"></div>
|
|
</section>
|
|
|
|
<div id="toast" class="toast"></div>
|
|
|
|
<footer>
|
|
<p>QR-Code: <a href="https://github.com/kazuhikoarase/qrcode-generator">qrcode-generator</a> (Kazuhiko Arase, MIT) · Schlüssel-Erzeugung: WebCrypto X25519 (RFC 7748) · Single-File, keine externen Abhängigkeiten</p>
|
|
</footer>
|
|
|
|
<script>
|
|
/* === qrcode-generator (Kazuhiko Arase, MIT) v1.4.4 — inline === */
|
|
var qrcode=function(){var t=function(t,r){var e=t,n=g[r],o=null,i=0,a=null,u=[],f={},c=function(t,r){o=function(t){for(var r=new Array(t),e=0;e<t;e+=1){r[e]=new Array(t);for(var n=0;n<t;n+=1)r[e][n]=null}return r}(i=4*e+17),l(0,0),l(i-7,0),l(0,i-7),s(),h(),d(t,r),e>=7&&v(t),null==a&&(a=p(e,n,u)),w(a,r)},l=function(t,r){for(var e=-1;e<=7;e+=1)if(!(t+e<=-1||i<=t+e))for(var n=-1;n<=7;n+=1)r+n<=-1||i<=r+n||(o[t+e][r+n]=0<=e&&e<=6&&(0==n||6==n)||0<=n&&n<=6&&(0==e||6==e)||2<=e&&e<=4&&2<=n&&n<=4)},h=function(){for(var t=8;t<i-8;t+=1)null==o[t][6]&&(o[t][6]=t%2==0);for(var r=8;r<i-8;r+=1)null==o[6][r]&&(o[6][r]=r%2==0)},s=function(){for(var t=B.getPatternPosition(e),r=0;r<t.length;r+=1)for(var n=0;n<t.length;n+=1){var i=t[r],a=t[n];if(null==o[i][a])for(var u=-2;u<=2;u+=1)for(var f=-2;f<=2;f+=1)o[i+u][a+f]=-2==u||2==u||-2==f||2==f||0==u&&0==f}},v=function(t){for(var r=B.getBCHTypeNumber(e),n=0;n<18;n+=1){var a=!t&&1==(r>>n&1);o[Math.floor(n/3)][n%3+i-8-3]=a}for(n=0;n<18;n+=1){a=!t&&1==(r>>n&1);o[n%3+i-8-3][Math.floor(n/3)]=a}},d=function(t,r){for(var e=n<<3|r,a=B.getBCHTypeInfo(e),u=0;u<15;u+=1){var f=!t&&1==(a>>u&1);u<6?o[u][8]=f:u<8?o[u+1][8]=f:o[i-15+u][8]=f}for(u=0;u<15;u+=1){f=!t&&1==(a>>u&1);u<8?o[8][i-u-1]=f:u<9?o[8][15-u-1+1]=f:o[8][15-u-1]=f}o[i-8][8]=!t},w=function(t,r){for(var e=-1,n=i-1,a=7,u=0,f=B.getMaskFunction(r),c=i-1;c>0;c-=2)for(6==c&&(c-=1);;){for(var g=0;g<2;g+=1)if(null==o[n][c-g]){var l=!1;u<t.length&&(l=1==(t[u]>>>a&1)),f(n,c-g)&&(l=!l),o[n][c-g]=l,-1==(a-=1)&&(u+=1,a=7)}if((n+=e)<0||i<=n){n-=e,e=-e;break}}},p=function(t,r,e){for(var n=A.getRSBlocks(t,r),o=b(),i=0;i<e.length;i+=1){var a=e[i];o.put(a.getMode(),4),o.put(a.getLength(),B.getLengthInBits(a.getMode(),t)),a.write(o)}var u=0;for(i=0;i<n.length;i+=1)u+=n[i].dataCount;if(o.getLengthInBits()>8*u)throw"code length overflow. ("+o.getLengthInBits()+">"+8*u+")";for(o.getLengthInBits()+4<=8*u&&o.put(0,4);o.getLengthInBits()%8!=0;)o.putBit(!1);for(;!(o.getLengthInBits()>=8*u||(o.put(236,8),o.getLengthInBits()>=8*u));)o.put(17,8);return function(t,r){for(var e=0,n=0,o=0,i=new Array(r.length),a=new Array(r.length),u=0;u<r.length;u+=1){var f=r[u].dataCount,c=r[u].totalCount-f;n=Math.max(n,f),o=Math.max(o,c),i[u]=new Array(f);for(var g=0;g<i[u].length;g+=1)i[u][g]=255&t.getBuffer()[g+e];e+=f;var l=B.getErrorCorrectPolynomial(c),h=k(i[u],l.getLength()-1).mod(l);for(a[u]=new Array(l.getLength()-1),g=0;g<a[u].length;g+=1){var s=g+h.getLength()-a[u].length;a[u][g]=s>=0?h.getAt(s):0}}var v=0;for(g=0;g<r.length;g+=1)v+=r[g].totalCount;var d=new Array(v),w=0;for(g=0;g<n;g+=1)for(u=0;u<r.length;u+=1)g<i[u].length&&(d[w]=i[u][g],w+=1);for(g=0;g<o;g+=1)for(u=0;u<r.length;u+=1)g<a[u].length&&(d[w]=a[u][g],w+=1);return d}(o,n)};f.addData=function(t,r){var e=null;switch(r=r||"Byte"){case"Numeric":e=M(t);break;case"Alphanumeric":e=x(t);break;case"Byte":e=m(t);break;case"Kanji":e=L(t);break;default:throw"mode:"+r}u.push(e),a=null},f.isDark=function(t,r){if(t<0||i<=t||r<0||i<=r)throw t+","+r;return o[t][r]},f.getModuleCount=function(){return i},f.make=function(){if(e<1){for(var t=1;t<40;t++){for(var r=A.getRSBlocks(t,n),o=b(),i=0;i<u.length;i++){var a=u[i];o.put(a.getMode(),4),o.put(a.getLength(),B.getLengthInBits(a.getMode(),t)),a.write(o)}var g=0;for(i=0;i<r.length;i++)g+=r[i].dataCount;if(o.getLengthInBits()<=8*g)break}e=t}c(!1,function(){for(var t=0,r=0,e=0;e<8;e+=1){c(!0,e);var n=B.getLostPoint(f);(0==e||t>n)&&(t=n,r=e)}return r}())},f.createTableTag=function(t,r){t=t||2;var e="";e+='<table style="',e+=" border-width: 0px; border-style: none;",e+=" border-collapse: collapse;",e+=" padding: 0px; margin: "+(r=void 0===r?4*t:r)+"px;",e+='">',e+="<tbody>";for(var n=0;n<f.getModuleCount();n+=1){e+="<tr>";for(var o=0;o<f.getModuleCount();o+=1)e+='<td style="',e+=" border-width: 0px; border-style: none;",e+=" border-collapse: collapse;",e+=" padding: 0px; margin: 0px;",e+=" width: "+t+"px;",e+=" height: "+t+"px;",e+=" background-color: ",e+=f.isDark(n,o)?"#000000":"#ffffff",e+=";",e+='"/>';e+="</tr>"}return e+="</tbody>",e+="</table>"},f.createSvgTag=function(t,r,e,n){var o={};"object"==typeof arguments[0]&&(t=(o=arguments[0]).cellSize,r=o.margin,e=o.alt,n=o.title),t=t||2,r=void 0===r?4*t:r,(e="string"==typeof e?{text:e}:e||{}).text=e.text||null,e.id=e.text?e.id||"qrcode-description":null,(n="string"==typeof n?{text:n}:n||{}).text=n.text||null,n.id=n.text?n.id||"qrcode-title":null;var i,a,u,c,g=f.getModuleCount()*t+2*r,l="";for(c="l"+t+",0 0,"+t+" -"+t+",0 0,-"+t+"z ",l+='<svg version="1.1" xmlns="http://www.w3.org/2000/svg"',l+=o.scalable?"":' width="'+g+'px" height="'+g+'px"',l+=' viewBox="0 0 '+g+" "+g+'" ',l+=' preserveAspectRatio="xMinYMin meet"',l+=n.text||e.text?' role="img" aria-labelledby="'+y([n.id,e.id].join(" ").trim())+'"':"",l+=">",l+=n.text?'<title id="'+y(n.id)+'">'+y(n.text)+"</title>":"",l+=e.text?'<description id="'+y(e.id)+'">'+y(e.text)+"</description>":"",l+='<rect width="100%" height="100%" fill="white" cx="0" cy="0"/>',l+='<path d="',a=0;a<f.getModuleCount();a+=1)for(u=a*t+r,i=0;i<f.getModuleCount();i+=1)f.isDark(a,i)&&(l+="M"+(i*t+r)+","+u+c);return l+='" stroke="transparent" fill="black"/>',l+="</svg>"},f.createDataURL=function(t,r){t=t||2,r=void 0===r?4*t:r;var e=f.getModuleCount()*t+2*r,n=r,o=e-r;return I(e,e,(function(r,e){if(n<=r&&r<o&&n<=e&&e<o){var i=Math.floor((r-n)/t),a=Math.floor((e-n)/t);return f.isDark(a,i)?0:1}return 1}))},f.createImgTag=function(t,r,e){t=t||2,r=void 0===r?4*t:r;var n=f.getModuleCount()*t+2*r,o="";return o+="<img",o+=' src="',o+=f.createDataURL(t,r),o+='"',o+=' width="',o+=n,o+='"',o+=' height="',o+=n,o+='"',e&&(o+=' alt="',o+=y(e),o+='"'),o+="/>"};var y=function(t){for(var r="",e=0;e<t.length;e+=1){var n=t.charAt(e);switch(n){case"<":r+="<";break;case">":r+=">";break;case"&":r+="&";break;case'"':r+=""";break;default:r+=n}}return r};return f.createASCII=function(t,r){if((t=t||1)<2)return function(t){t=void 0===t?2:t;var r,e,n,o,i,a=1*f.getModuleCount()+2*t,u=t,c=a-t,g={"██":"█","█ ":"▀"," █":"▄"," ":" "},l={"██":"▀","█ ":"▀"," █":" "," ":" "},h="";for(r=0;r<a;r+=2){for(n=Math.floor((r-u)/1),o=Math.floor((r+1-u)/1),e=0;e<a;e+=1)i="█",u<=e&&e<c&&u<=r&&r<c&&f.isDark(n,Math.floor((e-u)/1))&&(i=" "),u<=e&&e<c&&u<=r+1&&r+1<c&&f.isDark(o,Math.floor((e-u)/1))?i+=" ":i+="█",h+=t<1&&r+1>=c?l[i]:g[i];h+="\n"}return a%2&&t>0?h.substring(0,h.length-a-1)+Array(a+1).join("▀"):h.substring(0,h.length-1)}(r);t-=1,r=void 0===r?2*t:r;var e,n,o,i,a=f.getModuleCount()*t+2*r,u=r,c=a-r,g=Array(t+1).join("██"),l=Array(t+1).join(" "),h="",s="";for(e=0;e<a;e+=1){for(o=Math.floor((e-u)/t),s="",n=0;n<a;n+=1)i=1,u<=n&&n<c&&u<=e&&e<c&&f.isDark(o,Math.floor((n-u)/t))&&(i=0),s+=i?g:l;for(o=0;o<t;o+=1)h+=s+"\n"}return h.substring(0,h.length-1)},f.renderTo2dContext=function(t,r){r=r||2;for(var e=f.getModuleCount(),n=0;n<e;n++)for(var o=0;o<e;o++)t.fillStyle=f.isDark(n,o)?"black":"white",t.fillRect(n*r,o*r,r,r)},f};t.stringToBytes=(t.stringToBytesFuncs={default:function(t){for(var r=[],e=0;e<t.length;e+=1){var n=t.charCodeAt(e);r.push(255&n)}return r}}).default,t.createStringToBytes=function(t,r){var e=function(){for(var e=S(t),n=function(){var t=e.read();if(-1==t)throw"eof";return t},o=0,i={};;){var a=e.read();if(-1==a)break;var u=n(),f=n()<<8|n();i[String.fromCharCode(a<<8|u)]=f,o+=1}if(o!=r)throw o+" != "+r;return i}(),n="?".charCodeAt(0);return function(t){for(var r=[],o=0;o<t.length;o+=1){var i=t.charCodeAt(o);if(i<128)r.push(i);else{var a=e[t.charAt(o)];"number"==typeof a?(255&a)==a?r.push(a):(r.push(a>>>8),r.push(255&a)):r.push(n)}}return r}};var r,e,n,o,i,a=1,u=2,f=4,c=8,g={L:1,M:0,Q:3,H:2},l=0,h=1,s=2,v=3,d=4,w=5,p=6,y=7,B=(r=[[],[6,18],[6,22],[6,26],[6,30],[6,34],[6,22,38],[6,24,42],[6,26,46],[6,28,50],[6,30,54],[6,32,58],[6,34,62],[6,26,46,66],[6,26,48,70],[6,26,50,74],[6,30,54,78],[6,30,56,82],[6,30,58,86],[6,34,62,90],[6,28,50,72,94],[6,26,50,74,98],[6,30,54,78,102],[6,28,54,80,106],[6,32,58,84,110],[6,30,58,86,114],[6,34,62,90,118],[6,26,50,74,98,122],[6,30,54,78,102,126],[6,26,52,78,104,130],[6,30,56,82,108,134],[6,34,60,86,112,138],[6,30,58,86,114,142],[6,34,62,90,118,146],[6,30,54,78,102,126,150],[6,24,50,76,102,128,154],[6,28,54,80,106,132,158],[6,32,58,84,110,136,162],[6,26,54,82,110,138,166],[6,30,58,86,114,142,170]],e=1335,n=7973,i=function(t){for(var r=0;0!=t;)r+=1,t>>>=1;return r},(o={}).getBCHTypeInfo=function(t){for(var r=t<<10;i(r)-i(e)>=0;)r^=e<<i(r)-i(e);return 21522^(t<<10|r)},o.getBCHTypeNumber=function(t){for(var r=t<<12;i(r)-i(n)>=0;)r^=n<<i(r)-i(n);return t<<12|r},o.getPatternPosition=function(t){return r[t-1]},o.getMaskFunction=function(t){switch(t){case l:return function(t,r){return(t+r)%2==0};case h:return function(t,r){return t%2==0};case s:return function(t,r){return r%3==0};case v:return function(t,r){return(t+r)%3==0};case d:return function(t,r){return(Math.floor(t/2)+Math.floor(r/3))%2==0};case w:return function(t,r){return t*r%2+t*r%3==0};case p:return function(t,r){return(t*r%2+t*r%3)%2==0};case y:return function(t,r){return(t*r%3+(t+r)%2)%2==0};default:throw"bad maskPattern:"+t}},o.getErrorCorrectPolynomial=function(t){for(var r=k([1],0),e=0;e<t;e+=1)r=r.multiply(k([1,C.gexp(e)],0));return r},o.getLengthInBits=function(t,r){if(1<=r&&r<10)switch(t){case a:return 10;case u:return 9;case f:case c:return 8;default:throw"mode:"+t}else if(r<27)switch(t){case a:return 12;case u:return 11;case f:return 16;case c:return 10;default:throw"mode:"+t}else{if(!(r<41))throw"type:"+r;switch(t){case a:return 14;case u:return 13;case f:return 16;case c:return 12;default:throw"mode:"+t}}},o.getLostPoint=function(t){for(var r=t.getModuleCount(),e=0,n=0;n<r;n+=1)for(var o=0;o<r;o+=1){for(var i=0,a=t.isDark(n,o),u=-1;u<=1;u+=1)if(!(n+u<0||r<=n+u))for(var f=-1;f<=1;f+=1)o+f<0||r<=o+f||0==u&&0==f||a==t.isDark(n+u,o+f)&&(i+=1);i>5&&(e+=3+i-5)}for(n=0;n<r-1;n+=1)for(o=0;o<r-1;o+=1){var c=0;t.isDark(n,o)&&(c+=1),t.isDark(n+1,o)&&(c+=1),t.isDark(n,o+1)&&(c+=1),t.isDark(n+1,o+1)&&(c+=1),0!=c&&4!=c||(e+=3)}for(n=0;n<r;n+=1)for(o=0;o<r-6;o+=1)t.isDark(n,o)&&!t.isDark(n,o+1)&&t.isDark(n,o+2)&&t.isDark(n,o+3)&&t.isDark(n,o+4)&&!t.isDark(n,o+5)&&t.isDark(n,o+6)&&(e+=40);for(o=0;o<r;o+=1)for(n=0;n<r-6;n+=1)t.isDark(n,o)&&!t.isDark(n+1,o)&&t.isDark(n+2,o)&&t.isDark(n+3,o)&&t.isDark(n+4,o)&&!t.isDark(n+5,o)&&t.isDark(n+6,o)&&(e+=40);var g=0;for(o=0;o<r;o+=1)for(n=0;n<r;n+=1)t.isDark(n,o)&&(g+=1);return e+=Math.abs(100*g/r/r-50)/5*10},o),C=function(){for(var t=new Array(256),r=new Array(256),e=0;e<8;e+=1)t[e]=1<<e;for(e=8;e<256;e+=1)t[e]=t[e-4]^t[e-5]^t[e-6]^t[e-8];for(e=0;e<255;e+=1)r[t[e]]=e;var n={glog:function(t){if(t<1)throw"glog("+t+")";return r[t]},gexp:function(r){for(;r<0;)r+=255;for(;r>=256;)r-=255;return t[r]}};return n}();function k(t,r){if(void 0===t.length)throw t.length+"/"+r;var e=function(){for(var e=0;e<t.length&&0==t[e];)e+=1;for(var n=new Array(t.length-e+r),o=0;o<t.length-e;o+=1)n[o]=t[o+e];return n}(),n={getAt:function(t){return e[t]},getLength:function(){return e.length},multiply:function(t){for(var r=new Array(n.getLength()+t.getLength()-1),e=0;e<n.getLength();e+=1)for(var o=0;o<t.getLength();o+=1)r[e+o]^=C.gexp(C.glog(n.getAt(e))+C.glog(t.getAt(o)));return k(r,0)},mod:function(t){if(n.getLength()-t.getLength()<0)return n;for(var r=C.glog(n.getAt(0))-C.glog(t.getAt(0)),e=new Array(n.getLength()),o=0;o<n.getLength();o+=1)e[o]=n.getAt(o);for(o=0;o<t.getLength();o+=1)e[o]^=C.gexp(C.glog(t.getAt(o))+r);return k(e,0).mod(t)}};return n}var A=function(){var t=[[1,26,19],[1,26,16],[1,26,13],[1,26,9],[1,44,34],[1,44,28],[1,44,22],[1,44,16],[1,70,55],[1,70,44],[2,35,17],[2,35,13],[1,100,80],[2,50,32],[2,50,24],[4,25,9],[1,134,108],[2,67,43],[2,33,15,2,34,16],[2,33,11,2,34,12],[2,86,68],[4,43,27],[4,43,19],[4,43,15],[2,98,78],[4,49,31],[2,32,14,4,33,15],[4,39,13,1,40,14],[2,121,97],[2,60,38,2,61,39],[4,40,18,2,41,19],[4,40,14,2,41,15],[2,146,116],[3,58,36,2,59,37],[4,36,16,4,37,17],[4,36,12,4,37,13],[2,86,68,2,87,69],[4,69,43,1,70,44],[6,43,19,2,44,20],[6,43,15,2,44,16],[4,101,81],[1,80,50,4,81,51],[4,50,22,4,51,23],[3,36,12,8,37,13],[2,116,92,2,117,93],[6,58,36,2,59,37],[4,46,20,6,47,21],[7,42,14,4,43,15],[4,133,107],[8,59,37,1,60,38],[8,44,20,4,45,21],[12,33,11,4,34,12],[3,145,115,1,146,116],[4,64,40,5,65,41],[11,36,16,5,37,17],[11,36,12,5,37,13],[5,109,87,1,110,88],[5,65,41,5,66,42],[5,54,24,7,55,25],[11,36,12,7,37,13],[5,122,98,1,123,99],[7,73,45,3,74,46],[15,43,19,2,44,20],[3,45,15,13,46,16],[1,135,107,5,136,108],[10,74,46,1,75,47],[1,50,22,15,51,23],[2,42,14,17,43,15],[5,150,120,1,151,121],[9,69,43,4,70,44],[17,50,22,1,51,23],[2,42,14,19,43,15],[3,141,113,4,142,114],[3,70,44,11,71,45],[17,47,21,4,48,22],[9,39,13,16,40,14],[3,135,107,5,136,108],[3,67,41,13,68,42],[15,54,24,5,55,25],[15,43,15,10,44,16],[4,144,116,4,145,117],[17,68,42],[17,50,22,6,51,23],[19,46,16,6,47,17],[2,139,111,7,140,112],[17,74,46],[7,54,24,16,55,25],[34,37,13],[4,151,121,5,152,122],[4,75,47,14,76,48],[11,54,24,14,55,25],[16,45,15,14,46,16],[6,147,117,4,148,118],[6,73,45,14,74,46],[11,54,24,16,55,25],[30,46,16,2,47,17],[8,132,106,4,133,107],[8,75,47,13,76,48],[7,54,24,22,55,25],[22,45,15,13,46,16],[10,142,114,2,143,115],[19,74,46,4,75,47],[28,50,22,6,51,23],[33,46,16,4,47,17],[8,152,122,4,153,123],[22,73,45,3,74,46],[8,53,23,26,54,24],[12,45,15,28,46,16],[3,147,117,10,148,118],[3,73,45,23,74,46],[4,54,24,31,55,25],[11,45,15,31,46,16],[7,146,116,7,147,117],[21,73,45,7,74,46],[1,53,23,37,54,24],[19,45,15,26,46,16],[5,145,115,10,146,116],[19,75,47,10,76,48],[15,54,24,25,55,25],[23,45,15,25,46,16],[13,145,115,3,146,116],[2,74,46,29,75,47],[42,54,24,1,55,25],[23,45,15,28,46,16],[17,145,115],[10,74,46,23,75,47],[10,54,24,35,55,25],[19,45,15,35,46,16],[17,145,115,1,146,116],[14,74,46,21,75,47],[29,54,24,19,55,25],[11,45,15,46,46,16],[13,145,115,6,146,116],[14,74,46,23,75,47],[44,54,24,7,55,25],[59,46,16,1,47,17],[12,151,121,7,152,122],[12,75,47,26,76,48],[39,54,24,14,55,25],[22,45,15,41,46,16],[6,151,121,14,152,122],[6,75,47,34,76,48],[46,54,24,10,55,25],[2,45,15,64,46,16],[17,152,122,4,153,123],[29,74,46,14,75,47],[49,54,24,10,55,25],[24,45,15,46,46,16],[4,152,122,18,153,123],[13,74,46,32,75,47],[48,54,24,14,55,25],[42,45,15,32,46,16],[20,147,117,4,148,118],[40,75,47,7,76,48],[43,54,24,22,55,25],[10,45,15,67,46,16],[19,148,118,6,149,119],[18,75,47,31,76,48],[34,54,24,34,55,25],[20,45,15,61,46,16]],r=function(t,r){var e={};return e.totalCount=t,e.dataCount=r,e},e={};return e.getRSBlocks=function(e,n){var o=function(r,e){switch(e){case g.L:return t[4*(r-1)+0];case g.M:return t[4*(r-1)+1];case g.Q:return t[4*(r-1)+2];case g.H:return t[4*(r-1)+3];default:return}}(e,n);if(void 0===o)throw"bad rs block @ typeNumber:"+e+"/errorCorrectionLevel:"+n;for(var i=o.length/3,a=[],u=0;u<i;u+=1)for(var f=o[3*u+0],c=o[3*u+1],l=o[3*u+2],h=0;h<f;h+=1)a.push(r(c,l));return a},e}(),b=function(){var t=[],r=0,e={getBuffer:function(){return t},getAt:function(r){var e=Math.floor(r/8);return 1==(t[e]>>>7-r%8&1)},put:function(t,r){for(var n=0;n<r;n+=1)e.putBit(1==(t>>>r-n-1&1))},getLengthInBits:function(){return r},putBit:function(e){var n=Math.floor(r/8);t.length<=n&&t.push(0),e&&(t[n]|=128>>>r%8),r+=1}};return e},M=function(t){var r=a,e=t,n={getMode:function(){return r},getLength:function(t){return e.length},write:function(t){for(var r=e,n=0;n+2<r.length;)t.put(o(r.substring(n,n+3)),10),n+=3;n<r.length&&(r.length-n==1?t.put(o(r.substring(n,n+1)),4):r.length-n==2&&t.put(o(r.substring(n,n+2)),7))}},o=function(t){for(var r=0,e=0;e<t.length;e+=1)r=10*r+i(t.charAt(e));return r},i=function(t){if("0"<=t&&t<="9")return t.charCodeAt(0)-"0".charCodeAt(0);throw"illegal char :"+t};return n},x=function(t){var r=u,e=t,n={getMode:function(){return r},getLength:function(t){return e.length},write:function(t){for(var r=e,n=0;n+1<r.length;)t.put(45*o(r.charAt(n))+o(r.charAt(n+1)),11),n+=2;n<r.length&&t.put(o(r.charAt(n)),6)}},o=function(t){if("0"<=t&&t<="9")return t.charCodeAt(0)-"0".charCodeAt(0);if("A"<=t&&t<="Z")return t.charCodeAt(0)-"A".charCodeAt(0)+10;switch(t){case" ":return 36;case"$":return 37;case"%":return 38;case"*":return 39;case"+":return 40;case"-":return 41;case".":return 42;case"/":return 43;case":":return 44;default:throw"illegal char :"+t}};return n},m=function(r){var e=f,n=t.stringToBytes(r),o={getMode:function(){return e},getLength:function(t){return n.length},write:function(t){for(var r=0;r<n.length;r+=1)t.put(n[r],8)}};return o},L=function(r){var e=c,n=t.stringToBytesFuncs.SJIS;if(!n)throw"sjis not supported.";!function(){var t=n("友");if(2!=t.length||38726!=(t[0]<<8|t[1]))throw"sjis not supported."}();var o=n(r),i={getMode:function(){return e},getLength:function(t){return~~(o.length/2)},write:function(t){for(var r=o,e=0;e+1<r.length;){var n=(255&r[e])<<8|255&r[e+1];if(33088<=n&&n<=40956)n-=33088;else{if(!(57408<=n&&n<=60351))throw"illegal char at "+(e+1)+"/"+n;n-=49472}n=192*(n>>>8&255)+(255&n),t.put(n,13),e+=2}if(e<r.length)throw"illegal char at "+(e+1)}};return i},D=function(){var t=[],r={writeByte:function(r){t.push(255&r)},writeShort:function(t){r.writeByte(t),r.writeByte(t>>>8)},writeBytes:function(t,e,n){e=e||0,n=n||t.length;for(var o=0;o<n;o+=1)r.writeByte(t[o+e])},writeString:function(t){for(var e=0;e<t.length;e+=1)r.writeByte(t.charCodeAt(e))},toByteArray:function(){return t},toString:function(){var r="";r+="[";for(var e=0;e<t.length;e+=1)e>0&&(r+=","),r+=t[e];return r+="]"}};return r},S=function(t){var r=t,e=0,n=0,o=0,i={read:function(){for(;o<8;){if(e>=r.length){if(0==o)return-1;throw"unexpected end of file./"+o}var t=r.charAt(e);if(e+=1,"="==t)return o=0,-1;t.match(/^\s$/)||(n=n<<6|a(t.charCodeAt(0)),o+=6)}var i=n>>>o-8&255;return o-=8,i}},a=function(t){if(65<=t&&t<=90)return t-65;if(97<=t&&t<=122)return t-97+26;if(48<=t&&t<=57)return t-48+52;if(43==t)return 62;if(47==t)return 63;throw"c:"+t};return i},I=function(t,r,e){for(var n=function(t,r){var e=t,n=r,o=new Array(t*r),i={setPixel:function(t,r,n){o[r*e+t]=n},write:function(t){t.writeString("GIF87a"),t.writeShort(e),t.writeShort(n),t.writeByte(128),t.writeByte(0),t.writeByte(0),t.writeByte(0),t.writeByte(0),t.writeByte(0),t.writeByte(255),t.writeByte(255),t.writeByte(255),t.writeString(","),t.writeShort(0),t.writeShort(0),t.writeShort(e),t.writeShort(n),t.writeByte(0);var r=a(2);t.writeByte(2);for(var o=0;r.length-o>255;)t.writeByte(255),t.writeBytes(r,o,255),o+=255;t.writeByte(r.length-o),t.writeBytes(r,o,r.length-o),t.writeByte(0),t.writeString(";")}},a=function(t){for(var r=1<<t,e=1+(1<<t),n=t+1,i=u(),a=0;a<r;a+=1)i.add(String.fromCharCode(a));i.add(String.fromCharCode(r)),i.add(String.fromCharCode(e));var f,c,g,l=D(),h=(f=l,c=0,g=0,{write:function(t,r){if(t>>>r!=0)throw"length over";for(;c+r>=8;)f.writeByte(255&(t<<c|g)),r-=8-c,t>>>=8-c,g=0,c=0;g|=t<<c,c+=r},flush:function(){c>0&&f.writeByte(g)}});h.write(r,n);var s=0,v=String.fromCharCode(o[s]);for(s+=1;s<o.length;){var d=String.fromCharCode(o[s]);s+=1,i.contains(v+d)?v+=d:(h.write(i.indexOf(v),n),i.size()<4095&&(i.size()==1<<n&&(n+=1),i.add(v+d)),v=d)}return h.write(i.indexOf(v),n),h.write(e,n),h.flush(),l.toByteArray()},u=function(){var t={},r=0,e={add:function(n){if(e.contains(n))throw"dup key:"+n;t[n]=r,r+=1},size:function(){return r},indexOf:function(r){return t[r]},contains:function(r){return void 0!==t[r]}};return e};return i}(t,r),o=0;o<r;o+=1)for(var i=0;i<t;i+=1)n.setPixel(i,o,e(i,o));var a=D();n.write(a);for(var u=function(){var t=0,r=0,e=0,n="",o={},i=function(t){n+=String.fromCharCode(a(63&t))},a=function(t){if(t<0);else{if(t<26)return 65+t;if(t<52)return t-26+97;if(t<62)return t-52+48;if(62==t)return 43;if(63==t)return 47}throw"n:"+t};return o.writeByte=function(n){for(t=t<<8|255&n,r+=8,e+=1;r>=6;)i(t>>>r-6),r-=6},o.flush=function(){if(r>0&&(i(t<<6-r),t=0,r=0),e%3!=0)for(var o=3-e%3,a=0;a<o;a+=1)n+="="},o.toString=function(){return n},o}(),f=a.toByteArray(),c=0;c<f.length;c+=1)u.writeByte(f[c]);return u.flush(),"data:image/gif;base64,"+u};return t}();qrcode.stringToBytesFuncs["UTF-8"]=function(t){return function(t){for(var r=[],e=0;e<t.length;e++){var n=t.charCodeAt(e);n<128?r.push(n):n<2048?r.push(192|n>>6,128|63&n):n<55296||n>=57344?r.push(224|n>>12,128|n>>6&63,128|63&n):(e++,n=65536+((1023&n)<<10|1023&t.charCodeAt(e)),r.push(240|n>>18,128|n>>12&63,128|n>>6&63,128|63&n))}return r}(t)},function(t){"function"==typeof define&&define.amd?define([],t):"object"==typeof exports&&(module.exports=t())}((function(){return qrcode}));
|
|
</script>
|
|
|
|
<script>
|
|
"use strict";
|
|
|
|
/* ============================================================
|
|
* Crypto helpers — Curve25519 / X25519 via WebCrypto
|
|
* ============================================================ */
|
|
|
|
function bytesToBase64(bytes) {
|
|
let s = "";
|
|
for (let i = 0; i < bytes.length; i++) s += String.fromCharCode(bytes[i]);
|
|
return btoa(s);
|
|
}
|
|
|
|
function base64ToBytes(b64) {
|
|
const s = atob(b64.trim());
|
|
const out = new Uint8Array(s.length);
|
|
for (let i = 0; i < s.length; i++) out[i] = s.charCodeAt(i);
|
|
return out;
|
|
}
|
|
|
|
function isValidWgKey(b64) {
|
|
try {
|
|
const bytes = base64ToBytes(b64);
|
|
return bytes.length === 32;
|
|
} catch (e) { return false; }
|
|
}
|
|
|
|
/* PKCS8 wrapper for raw 32-byte X25519 private key (RFC 8410). */
|
|
function buildX25519Pkcs8(rawPriv) {
|
|
const prefix = new Uint8Array([
|
|
0x30, 0x2e, 0x02, 0x01, 0x00,
|
|
0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x6e,
|
|
0x04, 0x22, 0x04, 0x20
|
|
]);
|
|
const out = new Uint8Array(prefix.length + 32);
|
|
out.set(prefix, 0);
|
|
out.set(rawPriv, prefix.length);
|
|
return out;
|
|
}
|
|
|
|
/* SubjectPublicKeyInfo wrapper for raw 32-byte X25519 public key. */
|
|
function buildX25519Spki(rawPub) {
|
|
const prefix = new Uint8Array([
|
|
0x30, 0x2a,
|
|
0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x6e,
|
|
0x03, 0x21, 0x00
|
|
]);
|
|
const out = new Uint8Array(prefix.length + 32);
|
|
out.set(prefix, 0);
|
|
out.set(rawPub, prefix.length);
|
|
return out;
|
|
}
|
|
|
|
async function generateWgKeyPair() {
|
|
if (!crypto.subtle || !crypto.subtle.generateKey) {
|
|
throw new Error("WebCrypto nicht verfügbar.");
|
|
}
|
|
let kp;
|
|
try {
|
|
kp = await crypto.subtle.generateKey({ name: "X25519" }, true, ["deriveBits"]);
|
|
} catch (e) {
|
|
throw new Error("Dein Browser unterstützt X25519 in WebCrypto nicht. Nutze einen aktuellen Chrome/Edge/Firefox/Safari oder generiere die Schlüssel extern mit `wg genkey | tee priv | wg pubkey > pub`.");
|
|
}
|
|
const pkcs8 = new Uint8Array(await crypto.subtle.exportKey("pkcs8", kp.privateKey));
|
|
const rawPub = new Uint8Array(await crypto.subtle.exportKey("raw", kp.publicKey));
|
|
const rawPriv = pkcs8.slice(pkcs8.length - 32);
|
|
return {
|
|
privateKey: bytesToBase64(rawPriv),
|
|
publicKey: bytesToBase64(rawPub)
|
|
};
|
|
}
|
|
|
|
/* Derive public X25519 key from a raw 32-byte private key by computing
|
|
scalar * basepoint via deriveBits with the basepoint as the peer key. */
|
|
async function derivePublicFromPrivate(privBase64) {
|
|
const rawPriv = base64ToBytes(privBase64);
|
|
if (rawPriv.length !== 32) throw new Error("Private Key hat nicht 32 Bytes.");
|
|
|
|
const pkcs8 = buildX25519Pkcs8(rawPriv);
|
|
const privKey = await crypto.subtle.importKey(
|
|
"pkcs8", pkcs8, { name: "X25519" }, false, ["deriveBits"]
|
|
);
|
|
|
|
const basepoint = new Uint8Array(32);
|
|
basepoint[0] = 9;
|
|
const spki = buildX25519Spki(basepoint);
|
|
const pubKey = await crypto.subtle.importKey(
|
|
"spki", spki, { name: "X25519" }, false, []
|
|
);
|
|
|
|
const result = await crypto.subtle.deriveBits({ name: "X25519", public: pubKey }, privKey, 256);
|
|
return bytesToBase64(new Uint8Array(result));
|
|
}
|
|
|
|
function generatePsk() {
|
|
const buf = new Uint8Array(32);
|
|
crypto.getRandomValues(buf);
|
|
return bytesToBase64(buf);
|
|
}
|
|
|
|
/* ============================================================
|
|
* Konfig-Token: AES-256-GCM, Key via PBKDF2-HMAC-SHA256
|
|
* Format: "FBWG1:" + base64( salt[16] | iv[12] | ciphertext|tag )
|
|
* ============================================================ */
|
|
|
|
const TOKEN_PREFIX = "FBWG1:";
|
|
const PBKDF2_ITERATIONS = 600000;
|
|
|
|
async function deriveTokenKey(password, salt) {
|
|
const enc = new TextEncoder();
|
|
const baseKey = await crypto.subtle.importKey(
|
|
"raw", enc.encode(password), { name: "PBKDF2" }, false, ["deriveKey"]
|
|
);
|
|
return crypto.subtle.deriveKey(
|
|
{ name: "PBKDF2", salt, iterations: PBKDF2_ITERATIONS, hash: "SHA-256" },
|
|
baseKey,
|
|
{ name: "AES-GCM", length: 256 },
|
|
false,
|
|
["encrypt", "decrypt"]
|
|
);
|
|
}
|
|
|
|
async function encryptConfig(data, password) {
|
|
const salt = crypto.getRandomValues(new Uint8Array(16));
|
|
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
const key = await deriveTokenKey(password, salt);
|
|
const enc = new TextEncoder();
|
|
const ct = await crypto.subtle.encrypt(
|
|
{ name: "AES-GCM", iv }, key, enc.encode(JSON.stringify(data))
|
|
);
|
|
const out = new Uint8Array(salt.length + iv.length + ct.byteLength);
|
|
out.set(salt, 0);
|
|
out.set(iv, salt.length);
|
|
out.set(new Uint8Array(ct), salt.length + iv.length);
|
|
return TOKEN_PREFIX + bytesToBase64(out);
|
|
}
|
|
|
|
async function decryptConfig(token, password) {
|
|
const t = String(token).trim();
|
|
if (!t.startsWith(TOKEN_PREFIX)) throw new Error("Token-Format unbekannt (erwartet Präfix " + TOKEN_PREFIX + ")");
|
|
let raw;
|
|
try { raw = base64ToBytes(t.slice(TOKEN_PREFIX.length)); }
|
|
catch (e) { throw new Error("Token ist kein gültiges Base64"); }
|
|
if (raw.length < 16 + 12 + 16) throw new Error("Token zu kurz");
|
|
const salt = raw.slice(0, 16);
|
|
const iv = raw.slice(16, 28);
|
|
const ct = raw.slice(28);
|
|
const key = await deriveTokenKey(password, salt);
|
|
const pt = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, ct);
|
|
return JSON.parse(new TextDecoder().decode(pt));
|
|
}
|
|
|
|
/* ============================================================
|
|
* Network/CIDR helpers
|
|
* ============================================================ */
|
|
|
|
function parseIp(s) {
|
|
const m = String(s).trim().match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
|
|
if (!m) return null;
|
|
const parts = [m[1], m[2], m[3], m[4]].map(Number);
|
|
if (parts.some(p => p < 0 || p > 255)) return null;
|
|
return parts;
|
|
}
|
|
|
|
function parseCidr(s) {
|
|
const parts = String(s).trim().split("/");
|
|
if (parts.length !== 2) return null;
|
|
const ip = parseIp(parts[0]);
|
|
const prefix = Number(parts[1]);
|
|
if (!ip || isNaN(prefix) || prefix < 0 || prefix > 32) return null;
|
|
return { ip, prefix };
|
|
}
|
|
|
|
/* ============================================================
|
|
* Config builder
|
|
* ============================================================ */
|
|
|
|
function buildWindowsConfig(p) {
|
|
const lines = [];
|
|
lines.push("# Windows-Server-Konfiguration (WireGuard für Windows)");
|
|
lines.push("# Generiert: " + new Date().toISOString());
|
|
lines.push("# Endpoint: dieser PC ist Server, Fritzbox connectet ausgehend hierher");
|
|
lines.push("");
|
|
lines.push("[Interface]");
|
|
lines.push("PrivateKey = " + p.winPriv);
|
|
lines.push("ListenPort = " + p.port);
|
|
lines.push("Address = " + p.winTunnelIp + "/" + p.tunnelPrefix);
|
|
lines.push("");
|
|
lines.push("[Peer]");
|
|
lines.push("# Fritzbox");
|
|
lines.push("PublicKey = " + p.fbPub);
|
|
if (p.psk) lines.push("PresharedKey = " + p.psk);
|
|
lines.push("AllowedIPs = " + p.fbTunnelIp + "/32, " + p.fbLan);
|
|
if (p.keepalive > 0) lines.push("PersistentKeepalive = " + p.keepalive);
|
|
return lines.join("\n") + "\n";
|
|
}
|
|
|
|
function buildFritzboxConfig(p) {
|
|
const lines = [];
|
|
lines.push("# Fritzbox-Client-Konfiguration");
|
|
lines.push("# Generiert: " + new Date().toISOString());
|
|
lines.push("# Fritzbox baut ausgehend zum Windows-PC auf (Reverse-Tunnel)");
|
|
lines.push("");
|
|
lines.push("[Interface]");
|
|
lines.push("PrivateKey = " + p.fbPriv);
|
|
lines.push("Address = " + p.fbTunnelIp + "/" + p.tunnelPrefix);
|
|
lines.push("");
|
|
lines.push("[Peer]");
|
|
lines.push("# Windows-Server");
|
|
lines.push("PublicKey = " + p.winPub);
|
|
if (p.psk) lines.push("PresharedKey = " + p.psk);
|
|
lines.push("Endpoint = " + p.endpoint + ":" + p.port);
|
|
lines.push("AllowedIPs = " + p.winTunnelIp + "/32");
|
|
if (p.keepalive > 0) lines.push("PersistentKeepalive = " + p.keepalive);
|
|
return lines.join("\n") + "\n";
|
|
}
|
|
|
|
/* ============================================================
|
|
* Instructions builder
|
|
* ============================================================ */
|
|
|
|
function buildInstructions(p) {
|
|
const html = [];
|
|
|
|
html.push("<h3>A. Auf dem Windows-PC (WireGuard-Server)</h3>");
|
|
html.push("<ol>");
|
|
html.push("<li><strong>WireGuard für Windows</strong> installieren (<code>https://www.wireguard.com/install/</code>).</li>");
|
|
html.push("<li>Im WireGuard-Client: <em>Tunnel hinzufügen → Tunnel aus Datei importieren</em> → die <code>windows-server.conf</code> auswählen. Routen ins Fritzbox-LAN setzt der Client anhand der <code>AllowedIPs</code> automatisch.</li>");
|
|
html.push("<li><strong>Port-Forwarding</strong> auf deinem Internet-Router/Modem (vor dem Windows-PC) einrichten: <code>UDP " + p.port + " → Windows-PC</code>. Hängt der Windows-PC direkt am Modem mit öffentlicher IP, entfällt das.</li>");
|
|
html.push("<li><strong>DynDNS</strong> für den Windows-Anschluss einrichten, damit <code>" + p.endpoint + "</code> immer auf den aktuellen WAN-IP zeigt (z.B. dyn.com, no-ip.com, oder freie Anbieter).</li>");
|
|
html.push("</ol>");
|
|
html.push("<p class=\"hint\" style=\"margin-top:0.4em\">Hinweis: IP-Forwarding und Anpassungen am Windows-Firewall-Profil sind <em>nicht</em> nötig. Der Windows-PC initiiert den Verkehr Richtung Fritzbox-LAN selbst — das ist ausgehender Traffic und braucht weder Routing-Forwarding noch zusätzliche Firewall-Regeln. (IP-Forwarding wäre nur erforderlich, wenn Windows als Router für andere Geräte im Windows-LAN dienen sollte.)</p>");
|
|
|
|
html.push("<h3>B. Auf der Fritzbox (WireGuard-Client)</h3>");
|
|
html.push("<ol>");
|
|
html.push("<li>Fritzbox-GUI öffnen → <em>Internet → Freigaben → VPN (WireGuard)</em>.</li>");
|
|
if (p.fbExisting) {
|
|
html.push("<li>Den oben angezeigten <code>[Peer]</code>-Block als <code>fritzbox-peer.conf</code> herunterladen (oder kopieren und in eine neue Datei speichern).</li>");
|
|
html.push("<li><em>Verbindung hinzufügen</em> → die <code>fritzbox-peer.conf</code> hochladen. Die Fritzbox erkennt den isolierten Peer-Block und hängt ihn an die bestehende WireGuard-Verbindung an — die existierende Verbindung muss nicht gelöscht werden.</li>");
|
|
html.push("<li>Im Import-Assistenten die Option für <strong>NetBIOS / Datei-Freigabe</strong> anhaken, falls SMB durch den Tunnel laufen soll, und <strong>Heimnetzfreigabe</strong> bzw. <strong>Zugriff auf das Heimnetz aktivieren</strong> einschalten.</li>");
|
|
} else {
|
|
html.push("<li><em>Verbindung hinzufügen → Benutzerdefinierte Einstellungen vornehmen</em> wählen.</li>");
|
|
html.push("<li>Im nachfolgenden Dialog die <code>fritzbox-client.conf</code> hochladen <em>oder</em> die Werte aus der Datei manuell eintragen (PrivateKey, PublicKey, Endpoint, AllowedIPs).</li>");
|
|
html.push("<li>Im Wizard die Option für <strong>NetBIOS / Datei-Freigabe</strong> anhaken, falls SMB durch den Tunnel laufen soll (das macht den passenden Filter automatisch auf).</li>");
|
|
html.push("<li>Wichtig: <strong>Heimnetzfreigabe</strong> bzw. <strong>Zugriff auf das Heimnetz aktivieren</strong> einschalten, damit das Fritzbox-LAN über den Tunnel erreichbar wird.</li>");
|
|
}
|
|
html.push("<li>Speichern. Die Fritzbox baut den Tunnel ausgehend zu <code>" + p.endpoint + ":" + p.port + "</code> auf.</li>");
|
|
html.push("<li>Status prüfen: in der WireGuard-Übersicht muss die Verbindung als <em>aktiv</em> erscheinen, der letzte Handshake darf nicht älter als ~2 Min. sein.</li>");
|
|
html.push("</ol>");
|
|
|
|
html.push("<h3>C. Test</h3>");
|
|
html.push("<ol>");
|
|
html.push("<li>Auf dem Windows-PC: <code>ping " + p.fbTunnelIp + "</code> — die Fritzbox-Tunnel-IP muss antworten (Handshake erfolgt ggf. erst beim ersten Paket).</li>");
|
|
html.push("<li>Anschließend: <code>ping " + p.fbLanFirstHost + "</code> — ein Gerät im Fritzbox-LAN muss antworten.</li>");
|
|
html.push("<li>Bei Problemen: WireGuard-Log auf Windows-Seite (im Client-UI) prüfen, dort sieht man Handshake- und Routing-Probleme sofort.</li>");
|
|
html.push("</ol>");
|
|
|
|
html.push("<h3 style=\"margin-top:1.5em\">Topologie-Übersicht</h3>");
|
|
html.push("<pre>");
|
|
html.push("[Fritzbox-LAN " + p.fbLan + "]");
|
|
html.push(" │");
|
|
html.push("[Fritzbox " + p.fbTunnelIp + "] ──── ausgehend WG ───▶ [Internet] ────▶ " + p.endpoint + ":" + p.port);
|
|
html.push(" │");
|
|
html.push(" [Windows " + p.winTunnelIp + "]");
|
|
html.push("");
|
|
html.push("Datenfluss von Windows zu " + p.fbLan + ":");
|
|
html.push("Windows-App ─▶ WG-Interface ─▶ (Tunnel zur Fritzbox) ─▶ Fritzbox-LAN");
|
|
html.push("</pre>");
|
|
|
|
return html.join("\n");
|
|
}
|
|
|
|
/* ============================================================
|
|
* UI wiring
|
|
* ============================================================ */
|
|
|
|
const $ = id => document.getElementById(id);
|
|
|
|
function showErr(elId, msg) {
|
|
const el = $(elId);
|
|
el.textContent = msg;
|
|
el.hidden = false;
|
|
}
|
|
function clearErr(elId) {
|
|
const el = $(elId);
|
|
el.textContent = "";
|
|
el.hidden = true;
|
|
}
|
|
|
|
function toast(msg) {
|
|
const t = $("toast");
|
|
t.textContent = msg;
|
|
t.classList.add("show");
|
|
clearTimeout(toast._t);
|
|
toast._t = setTimeout(() => t.classList.remove("show"), 1800);
|
|
}
|
|
|
|
async function setKeyPair(privInputId, pubInputId, kp) {
|
|
$(privInputId).value = kp.privateKey;
|
|
$(pubInputId).value = kp.publicKey;
|
|
}
|
|
|
|
async function refreshPubFromPriv(privInputId, pubInputId) {
|
|
const priv = $(privInputId).value.trim();
|
|
if (!priv) { $(pubInputId).value = ""; return; }
|
|
if (!isValidWgKey(priv)) {
|
|
$(pubInputId).value = "";
|
|
showErr("keyErr", "Ungültiger Private Key in „" + privInputId + "“ (muss Base64-kodiert, 32 Bytes sein).");
|
|
return;
|
|
}
|
|
clearErr("keyErr");
|
|
try {
|
|
const pub = await derivePublicFromPrivate(priv);
|
|
$(pubInputId).value = pub;
|
|
} catch (e) {
|
|
showErr("keyErr", "Public Key konnte nicht abgeleitet werden: " + e.message);
|
|
}
|
|
}
|
|
|
|
function fbExistingActive() { return $("fbExisting").checked; }
|
|
|
|
function applyFbExistingState() {
|
|
const on = fbExistingActive();
|
|
$("fbExistingHint").hidden = !on;
|
|
$("genFb").disabled = on;
|
|
}
|
|
|
|
async function init() {
|
|
$("genAll").addEventListener("click", async () => {
|
|
clearErr("keyErr");
|
|
try {
|
|
const win = await generateWgKeyPair();
|
|
setKeyPair("winPriv", "winPub", win);
|
|
if (!fbExistingActive()) {
|
|
const fb = await generateWgKeyPair();
|
|
setKeyPair("fbPriv", "fbPub", fb);
|
|
}
|
|
$("psk").value = generatePsk();
|
|
toast(fbExistingActive() ? "Windows-Schlüssel + PSK erzeugt (Fritzbox-Key bleibt)" : "Schlüssel erzeugt");
|
|
} catch (e) { showErr("keyErr", e.message); }
|
|
});
|
|
|
|
$("genWin").addEventListener("click", async () => {
|
|
clearErr("keyErr");
|
|
try {
|
|
const kp = await generateWgKeyPair();
|
|
setKeyPair("winPriv", "winPub", kp);
|
|
toast("Windows-Schlüssel erzeugt");
|
|
} catch (e) { showErr("keyErr", e.message); }
|
|
});
|
|
|
|
$("genFb").addEventListener("click", async () => {
|
|
clearErr("keyErr");
|
|
if (fbExistingActive()) {
|
|
showErr("keyErr", "Modus „Fritzbox hat bereits eine Verbindung“ ist aktiv — den existierenden Private Key bitte manuell eintragen.");
|
|
return;
|
|
}
|
|
try {
|
|
const kp = await generateWgKeyPair();
|
|
setKeyPair("fbPriv", "fbPub", kp);
|
|
toast("Fritzbox-Schlüssel erzeugt");
|
|
} catch (e) { showErr("keyErr", e.message); }
|
|
});
|
|
|
|
$("genPsk").addEventListener("click", () => {
|
|
$("psk").value = generatePsk();
|
|
toast("Pre-Shared Key erzeugt");
|
|
});
|
|
|
|
$("winPriv").addEventListener("change", () => refreshPubFromPriv("winPriv", "winPub"));
|
|
$("fbPriv").addEventListener("change", () => refreshPubFromPriv("fbPriv", "fbPub"));
|
|
$("fbPriv").addEventListener("input", () => refreshPubFromPriv("fbPriv", "fbPub"));
|
|
|
|
$("fbExisting").addEventListener("change", () => {
|
|
applyFbExistingState();
|
|
if (fbExistingActive()) {
|
|
// Vorbefüllten Auto-Key leeren, damit der User seinen echten Fritzbox-Key eintragen muss.
|
|
$("fbPriv").value = "";
|
|
$("fbPub").value = "";
|
|
$("fbPriv").focus();
|
|
}
|
|
});
|
|
|
|
$("saveBtn").addEventListener("click", saveConfig);
|
|
$("loadBtn").addEventListener("click", loadConfig);
|
|
|
|
$("usePsk").addEventListener("change", () => {
|
|
const psk = $("psk");
|
|
psk.disabled = !$("usePsk").checked;
|
|
if ($("usePsk").checked && !psk.value) psk.value = generatePsk();
|
|
});
|
|
|
|
$("form").addEventListener("submit", e => {
|
|
e.preventDefault();
|
|
generateAll();
|
|
});
|
|
|
|
document.addEventListener("click", e => {
|
|
const btn = e.target.closest("button[data-action]");
|
|
if (!btn) return;
|
|
const target = $(btn.dataset.target);
|
|
if (!target) return;
|
|
const text = (target.tagName === "TEXTAREA" || target.tagName === "INPUT")
|
|
? target.value : target.textContent;
|
|
if (btn.dataset.action === "copy") {
|
|
navigator.clipboard.writeText(text).then(
|
|
() => toast("Kopiert"),
|
|
() => toast("Kopieren fehlgeschlagen")
|
|
);
|
|
} else if (btn.dataset.action === "download") {
|
|
downloadText(btn.dataset.filename, text);
|
|
}
|
|
});
|
|
|
|
// Auto-generate on first load so the user has something to see/edit.
|
|
try {
|
|
const win = await generateWgKeyPair();
|
|
const fb = await generateWgKeyPair();
|
|
setKeyPair("winPriv", "winPub", win);
|
|
setKeyPair("fbPriv", "fbPub", fb);
|
|
$("psk").value = generatePsk();
|
|
} catch (e) {
|
|
showErr("keyErr", e.message);
|
|
}
|
|
}
|
|
|
|
/* Felder, die im Token gespeichert werden. Public Keys lassen wir weg —
|
|
die werden beim Laden aus den Private Keys neu abgeleitet. */
|
|
const SAVE_FIELDS = [
|
|
"winPriv", "fbPriv", "psk",
|
|
"endpoint", "port",
|
|
"tunnelNet", "winTunnelIp", "fbTunnelIp",
|
|
"fbLan", "keepalive"
|
|
];
|
|
const SAVE_BOOL_FIELDS = ["usePsk", "fbExisting"];
|
|
|
|
async function saveConfig() {
|
|
clearErr("saveErr");
|
|
const pw = $("savePassword").value;
|
|
if (!pw) { showErr("saveErr", "Passwort fehlt."); return; }
|
|
if (pw.length < 6) { showErr("saveErr", "Passwort sollte mindestens 6 Zeichen haben."); return; }
|
|
|
|
const data = { v: 1 };
|
|
for (const f of SAVE_FIELDS) data[f] = $(f).value;
|
|
for (const f of SAVE_BOOL_FIELDS) data[f] = $(f).checked;
|
|
|
|
try {
|
|
const token = await encryptConfig(data, pw);
|
|
$("saveToken").value = token;
|
|
toast("Token erzeugt — sicher aufbewahren");
|
|
} catch (e) {
|
|
showErr("saveErr", "Verschlüsselung fehlgeschlagen: " + e.message);
|
|
}
|
|
}
|
|
|
|
async function loadConfig() {
|
|
clearErr("saveErr");
|
|
const pw = $("savePassword").value;
|
|
const token = $("saveToken").value.trim();
|
|
if (!pw) { showErr("saveErr", "Passwort fehlt."); return; }
|
|
if (!token) { showErr("saveErr", "Token fehlt."); return; }
|
|
|
|
let data;
|
|
try {
|
|
data = await decryptConfig(token, pw);
|
|
} catch (e) {
|
|
showErr("saveErr", "Entschlüsselung fehlgeschlagen — Passwort falsch oder Token defekt.");
|
|
return;
|
|
}
|
|
|
|
for (const f of SAVE_FIELDS) {
|
|
if (typeof data[f] !== "undefined") $(f).value = data[f];
|
|
}
|
|
for (const f of SAVE_BOOL_FIELDS) {
|
|
if (typeof data[f] !== "undefined") $(f).checked = !!data[f];
|
|
}
|
|
|
|
// Public Keys neu ableiten (waren nicht im Token).
|
|
if (data.winPriv) await refreshPubFromPriv("winPriv", "winPub");
|
|
if (data.fbPriv) await refreshPubFromPriv("fbPriv", "fbPub");
|
|
|
|
// PSK-Feld aktivieren/deaktivieren passend zum geladenen Stand.
|
|
$("psk").disabled = !$("usePsk").checked;
|
|
applyFbExistingState();
|
|
|
|
toast("Konfiguration geladen");
|
|
}
|
|
|
|
function downloadText(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();
|
|
setTimeout(() => { document.body.removeChild(a); URL.revokeObjectURL(url); }, 100);
|
|
}
|
|
|
|
function renderQr(containerId, text) {
|
|
const container = $(containerId);
|
|
container.innerHTML = "";
|
|
// typeNumber=0 → auto, errorCorrectionLevel L (most capacity)
|
|
const qr = qrcode(0, "L");
|
|
qr.addData(text);
|
|
try { qr.make(); }
|
|
catch (e) {
|
|
container.innerHTML = "<p class=\"hint\">QR-Code zu lang für eine einzelne Code-Matrix. Nutze stattdessen den Datei-Download oder die Zwischenablage.</p>";
|
|
return;
|
|
}
|
|
// cellSize 4, margin 8 = passable size
|
|
container.innerHTML = qr.createImgTag(4, 8);
|
|
}
|
|
|
|
function firstHostOfCidr(cidrStr) {
|
|
const c = parseCidr(cidrStr);
|
|
if (!c) return cidrStr;
|
|
// network IP + 1 → typically gateway, but we want a sample host. Keep .1 if already typical, else +1.
|
|
const ip = c.ip.slice();
|
|
ip[3] = (ip[3] | 0) + 1;
|
|
if (ip[3] > 254) ip[3] = 1;
|
|
return ip.join(".");
|
|
}
|
|
|
|
function generateAll() {
|
|
clearErr("netErr"); clearErr("keyErr");
|
|
|
|
const winPriv = $("winPriv").value.trim();
|
|
const winPub = $("winPub").value.trim();
|
|
const fbPriv = $("fbPriv").value.trim();
|
|
const fbPub = $("fbPub").value.trim();
|
|
const usePsk = $("usePsk").checked;
|
|
const psk = usePsk ? $("psk").value.trim() : "";
|
|
|
|
for (const [k, v] of [["Windows-Private", winPriv], ["Windows-Public", winPub], ["Fritzbox-Private", fbPriv], ["Fritzbox-Public", fbPub]]) {
|
|
if (!isValidWgKey(v)) { showErr("keyErr", "Ungültiger Schlüssel: " + k); return; }
|
|
}
|
|
if (usePsk && !isValidWgKey(psk)) { showErr("keyErr", "Ungültiger Pre-Shared Key (Base64, 32 Bytes erwartet)."); return; }
|
|
|
|
const tunnelCidr = parseCidr($("tunnelNet").value);
|
|
const winTunnelIp = parseIp($("winTunnelIp").value);
|
|
const fbTunnelIp = parseIp($("fbTunnelIp").value);
|
|
const fbLan = parseCidr($("fbLan").value);
|
|
const port = Number($("port").value);
|
|
const keepalive = Number($("keepalive").value);
|
|
const endpoint = $("endpoint").value.trim();
|
|
|
|
if (!tunnelCidr) { showErr("netErr", "Tunnel-Subnetz ist kein gültiges CIDR."); return; }
|
|
if (!winTunnelIp) { showErr("netErr", "Tunnel-IP Windows ungültig."); return; }
|
|
if (!fbTunnelIp) { showErr("netErr", "Tunnel-IP Fritzbox ungültig."); return; }
|
|
if (!fbLan) { showErr("netErr", "Fritzbox-LAN ist kein gültiges CIDR."); return; }
|
|
if (!endpoint) { showErr("netErr", "Endpoint fehlt."); return; }
|
|
if (!(port >= 1 && port <= 65535)) { showErr("netErr", "Port ungültig."); return; }
|
|
|
|
const params = {
|
|
winPriv, winPub, fbPriv, fbPub, psk,
|
|
endpoint, port, keepalive,
|
|
tunnelPrefix: tunnelCidr.prefix,
|
|
winTunnelIp: winTunnelIp.join("."),
|
|
fbTunnelIp: fbTunnelIp.join("."),
|
|
fbLan: $("fbLan").value.trim(),
|
|
fbLanFirstHost: firstHostOfCidr($("fbLan").value),
|
|
fbExisting: fbExistingActive()
|
|
};
|
|
|
|
const winConf = buildWindowsConfig(params);
|
|
const fbConf = buildFritzboxConfig(params);
|
|
|
|
$("winConf").textContent = winConf;
|
|
$("fbConf").textContent = fbConf;
|
|
$("instructions").innerHTML = buildInstructions(params);
|
|
|
|
// Toggle Fritzbox-Output-Modus.
|
|
$("fbManualBox").hidden = !params.fbExisting;
|
|
$("fbSubtitle").textContent = params.fbExisting
|
|
? "— bestehende WireGuard-Verbindung um diesen Peer erweitern"
|
|
: "— in der Fritzbox-GUI als WireGuard-Verbindung importieren";
|
|
if (params.fbExisting) {
|
|
$("fbManualValues").textContent = buildFritzboxManualValues(params);
|
|
}
|
|
|
|
renderQr("winQr", winConf);
|
|
renderQr("fbQr", fbConf);
|
|
|
|
$("output").hidden = false;
|
|
$("output").scrollIntoView({ behavior: "smooth", block: "start" });
|
|
}
|
|
|
|
function buildFritzboxManualValues(p) {
|
|
const lines = [];
|
|
lines.push("[Peer]");
|
|
lines.push("# Windows-Server");
|
|
lines.push("PublicKey = " + p.winPub);
|
|
if (p.psk) lines.push("PresharedKey = " + p.psk);
|
|
lines.push("Endpoint = " + p.endpoint + ":" + p.port);
|
|
lines.push("AllowedIPs = " + p.winTunnelIp + "/32");
|
|
if (p.keepalive > 0) lines.push("PersistentKeepalive = " + p.keepalive);
|
|
return lines.join("\n");
|
|
}
|
|
|
|
init();
|
|
</script>
|
|
|
|
</body>
|
|
</html>
|