diff --git a/.tmp/Chrome Offer1 v 1 id.txt b/.tmp/Chrome Offer1 v 1 id.txt new file mode 100644 index 0000000..f64269a --- /dev/null +++ b/.tmp/Chrome Offer1 v 1 id.txt @@ -0,0 +1,40 @@ +Chrome: +Offer1 +{ + "v": 1, + "id": "21c5a474249cbbda", + "s": { + "type": "offer", + "sdp": "v=0\r\no=- 4727015153900916462 2 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\na=group:BUNDLE 0\r\na=extmap-allow-mixed\r\na=msid-semantic: WMS\r\nm=application 9 UDP/DTLS/SCTP webrtc-datachannel\r\nc=IN IP4 0.0.0.0\r\na=candidate:1600657949 1 udp 2113937151 19e60f27-7a6b-420b-b44b-80fb16b10d96.local 32922 typ host generation 0 network-cost 999\r\na=candidate:3858062446 1 udp 2113939711 5aa40abb-b5c3-46d4-8b1c-335f00f9d532.local 48316 typ host generation 0 network-cost 999\r\na=ice-ufrag:HJ3p\r\na=ice-pwd:8fF3fJMxF3Yci2HvNZOmd+6r\r\na=fingerprint:sha-256 0D:80:05:75:F9:5A:B6:CD:26:26:92:D8:04:1B:18:72:FA:AD:20:E6:AD:6F:B3:7F:F7:8D:2B:F1:36:8E:50:29\r\na=setup:actpass\r\na=mid:0\r\na=sctp-port:5000\r\na=max-message-size:262144\r\n" + } +} +Answer1 +{ + "v": 1, + "id": "4c79fdc6b5e20f4f", + "s": { + "type": "answer", + "sdp": "v=0\r\no=- 2426412120773230660 2 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\na=group:BUNDLE 0\r\na=extmap-allow-mixed\r\na=msid-semantic: WMS\r\nm=application 45330 UDP/DTLS/SCTP webrtc-datachannel\r\nc=IN IP4 192.168.0.99\r\na=candidate:1040060411 1 tcp 1518214911 192.168.0.99 9 typ host tcptype active generation 0 network-id 1 network-cost 10\r\na=candidate:2271431048 1 tcp 1518283007 2a02:8108:310d:b000:66c4:ed1c:57b0:194f 9 typ host tcptype active generation 0 network-id 2 network-cost 10\r\na=candidate:3277118319 1 udp 2122194687 192.168.0.99 45330 typ host generation 0 network-id 1 network-cost 10\r\na=candidate:2043256092 1 udp 2122262783 2a02:8108:310d:b000:66c4:ed1c:57b0:194f 50135 typ host generation 0 network-id 2 network-cost 10\r\na=ice-ufrag:jTj4\r\na=ice-pwd:JTgImK6tSV4XrFETX4d1NeWo\r\na=fingerprint:sha-256 10:FF:A7:A1:4D:C4:E6:C8:58:80:BE:DE:89:35:64:0F:08:46:5F:62:E4:9A:26:F8:35:6F:9D:23:9E:19:0A:5B\r\na=setup:active\r\na=mid:0\r\na=sctp-port:5000\r\na=max-message-size:262144\r\n" + }, + "to": "21c5a474249cbbda" +} + +Offer2 +{ + "v": 1, + "id": "ea8513afc96a66c4", + "s": { + "type": "offer", + "sdp": "v=0\r\no=- 7345681585991935661 2 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\na=group:BUNDLE 0\r\na=extmap-allow-mixed\r\na=msid-semantic: WMS\r\nm=application 9 UDP/DTLS/SCTP webrtc-datachannel\r\nc=IN IP4 0.0.0.0\r\na=candidate:3682889654 1 udp 2113937151 2fbe1617-5bef-4389-8b86-0dc8d27f01fe.local 52239 typ host generation 0 network-cost 999\r\na=candidate:2532579650 1 udp 2113939711 ba3d54c5-0b01-4df0-b677-212617ca1994.local 38447 typ host generation 0 network-cost 999\r\na=ice-ufrag:DWlh\r\na=ice-pwd:bG1hiDVlwbvtyDoC/l5edXh1\r\na=fingerprint:sha-256 DF:A7:0F:3E:00:AA:7A:CB:E9:84:65:2F:8B:9D:E8:C1:39:A7:47:C3:DE:B8:72:CF:80:4A:4A:FA:1D:C5:EB:E5\r\na=setup:actpass\r\na=mid:0\r\na=sctp-port:5000\r\na=max-message-size:262144\r\n" + } +} +Answer2 +{ + "v": 1, + "id": "2d22b7a296f92ab9", + "s": { + "type": "answer", + "sdp": "v=0\r\no=- 628465681931269525 2 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\na=group:BUNDLE 0\r\na=extmap-allow-mixed\r\na=msid-semantic: WMS\r\nm=application 59397 UDP/DTLS/SCTP webrtc-datachannel\r\nc=IN IP4 192.168.0.99\r\na=candidate:811532240 1 tcp 1518214911 192.168.0.99 9 typ host tcptype active generation 0 network-id 1 network-cost 10\r\na=candidate:2328082851 1 tcp 1518283007 2a02:8108:310d:b000:66c4:ed1c:57b0:194f 9 typ host tcptype active generation 0 network-id 2 network-cost 10\r\na=candidate:3472108356 1 udp 2122194687 192.168.0.99 59397 typ host generation 0 network-id 1 network-cost 10\r\na=candidate:1953066295 1 udp 2122262783 2a02:8108:310d:b000:66c4:ed1c:57b0:194f 34097 typ host generation 0 network-id 2 network-cost 10\r\na=ice-ufrag:W4rw\r\na=ice-pwd:asUEGNATk7NDaiAl/qAa6lHX\r\na=fingerprint:sha-256 A8:78:40:79:2D:D9:F7:37:58:FD:1B:18:41:12:D2:2E:49:6D:1E:26:F3:6A:EC:E3:E5:9C:F7:BC:33:F6:81:79\r\na=setup:active\r\na=mid:0\r\na=sctp-port:5000\r\na=max-message-size:262144\r\n" + }, + "to": "ea8513afc96a66c4" +} diff --git a/.tmp/Firefox Chrome.txt b/.tmp/Firefox Chrome.txt new file mode 100644 index 0000000..9a4a243 --- /dev/null +++ b/.tmp/Firefox Chrome.txt @@ -0,0 +1,21 @@ +Firefox <> Chrome: + +Firefox Offer1 +{ + "v": 1, + "id": "6b6433490bb88159", + "s": { + "type": "offer", + "sdp": "v=0\r\no=mozilla...THIS_IS_SDPARTA-99.0 5855950397394494929 0 IN IP4 0.0.0.0\r\ns=-\r\nt=0 0\r\na=sendrecv\r\na=fingerprint:sha-256 BC:D0:57:5A:7B:44:AA:CF:E0:D7:05:C2:CD:D8:C2:F9:26:D7:8D:82:D5:99:CC:54:51:A8:61:64:3C:67:53:A9\r\na=group:BUNDLE 0\r\na=msid-semantic:WMS *\r\nm=application 38115 UDP/DTLS/SCTP webrtc-datachannel\r\nc=IN IP4 192.168.0.99\r\na=candidate:0 1 UDP 2122187007 192.168.0.99 38115 typ host\r\na=candidate:1 1 UDP 2122252543 2a02:8108:310d:b000:66c4:ed1c:57b0:194f 50751 typ host\r\na=candidate:2 1 TCP 2105458943 192.168.0.99 9 typ host tcptype active\r\na=candidate:3 1 TCP 2105524479 2a02:8108:310d:b000:66c4:ed1c:57b0:194f 9 typ host tcptype active\r\na=sendrecv\r\na=end-of-candidates\r\na=ice-pwd:e71f0120d84ac191b57936d7c2c67bbd\r\na=ice-ufrag:3391cd3b\r\na=mid:0\r\na=setup:actpass\r\na=sctp-port:5000\r\na=max-message-size:1073741823\r\n" + } +} +Chrome Answer1 +{ + "v": 1, + "id": "ab830b376f46683b", + "s": { + "type": "answer", + "sdp": "v=0\r\no=- 7803168747385674361 2 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\na=group:BUNDLE 0\r\na=msid-semantic: WMS\r\nm=application 52093 UDP/DTLS/SCTP webrtc-datachannel\r\nc=IN IP4 192.168.0.99\r\na=candidate:480058806 1 tcp 1518214911 192.168.0.99 9 typ host tcptype active generation 0 network-id 1 network-cost 10\r\na=candidate:2785041349 1 tcp 1518283007 2a02:8108:310d:b000:66c4:ed1c:57b0:194f 9 typ host tcptype active generation 0 network-id 2 network-cost 10\r\na=candidate:3795306786 1 udp 2122194687 192.168.0.99 52093 typ host generation 0 network-id 1 network-cost 10\r\na=candidate:1487572817 1 udp 2122262783 2a02:8108:310d:b000:66c4:ed1c:57b0:194f 42761 typ host generation 0 network-id 2 network-cost 10\r\na=ice-ufrag:6Z2W\r\na=ice-pwd:mduInWDq1TEQPV0mKnVivq7l\r\na=fingerprint:sha-256 F6:A4:42:BB:8E:5F:85:CF:A1:AB:E1:E7:4E:3E:71:49:4C:FA:03:68:1B:E2:93:B6:25:09:50:B4:22:FB:5B:20\r\na=setup:active\r\na=mid:0\r\na=sctp-port:5000\r\na=max-message-size:262144\r\n" + }, + "to": "6b6433490bb88159" +} diff --git a/.tmp/Firefox Offer 1 v 1 id.txt b/.tmp/Firefox Offer 1 v 1 id.txt new file mode 100644 index 0000000..2890d4a --- /dev/null +++ b/.tmp/Firefox Offer 1 v 1 id.txt @@ -0,0 +1,43 @@ +Firefox: +Offer 1 +{ + "v": 1, + "id": "6b6433490bb88159", + "s": { + "type": "offer", + "sdp": "v=0\r\no=mozilla...THIS_IS_SDPARTA-99.0 5855950397394494929 0 IN IP4 0.0.0.0\r\ns=-\r\nt=0 0\r\na=sendrecv\r\na=fingerprint:sha-256 BC:D0:57:5A:7B:44:AA:CF:E0:D7:05:C2:CD:D8:C2:F9:26:D7:8D:82:D5:99:CC:54:51:A8:61:64:3C:67:53:A9\r\na=group:BUNDLE 0\r\na=msid-semantic:WMS *\r\nm=application 38115 UDP/DTLS/SCTP webrtc-datachannel\r\nc=IN IP4 192.168.0.99\r\na=candidate:0 1 UDP 2122187007 192.168.0.99 38115 typ host\r\na=candidate:1 1 UDP 2122252543 2a02:8108:310d:b000:66c4:ed1c:57b0:194f 50751 typ host\r\na=candidate:2 1 TCP 2105458943 192.168.0.99 9 typ host tcptype active\r\na=candidate:3 1 TCP 2105524479 2a02:8108:310d:b000:66c4:ed1c:57b0:194f 9 typ host tcptype active\r\na=sendrecv\r\na=end-of-candidates\r\na=ice-pwd:e71f0120d84ac191b57936d7c2c67bbd\r\na=ice-ufrag:3391cd3b\r\na=mid:0\r\na=setup:actpass\r\na=sctp-port:5000\r\na=max-message-size:1073741823\r\n" + } +} + +Offer2 +{ + "v": 1, + "id": "fbb6303b01f9e18d", + "s": { + "type": "offer", + "sdp": "v=0\r\no=mozilla...THIS_IS_SDPARTA-99.0 9222699254471428255 0 IN IP4 0.0.0.0\r\ns=-\r\nt=0 0\r\na=sendrecv\r\na=fingerprint:sha-256 19:2B:49:DB:EB:18:A6:8E:4D:EF:86:73:50:EC:AB:59:96:BB:26:EE:C4:34:D5:0B:5D:1A:2E:12:18:3B:34:E8\r\na=group:BUNDLE 0\r\na=msid-semantic:WMS *\r\nm=application 49437 UDP/DTLS/SCTP webrtc-datachannel\r\nc=IN IP4 192.168.0.99\r\na=candidate:0 1 UDP 2122187007 192.168.0.99 49437 typ host\r\na=candidate:1 1 UDP 2122252543 2a02:8108:310d:b000:66c4:ed1c:57b0:194f 33795 typ host\r\na=candidate:2 1 TCP 2105458943 192.168.0.99 9 typ host tcptype active\r\na=candidate:3 1 TCP 2105524479 2a02:8108:310d:b000:66c4:ed1c:57b0:194f 9 typ host tcptype active\r\na=sendrecv\r\na=end-of-candidates\r\na=ice-pwd:c3415db7920f14a330919e7239e0d353\r\na=ice-ufrag:685a689c\r\na=mid:0\r\na=setup:actpass\r\na=sctp-port:5000\r\na=max-message-size:1073741823\r\n" + } +} + +Answer1 +{ + "v": 1, + "id": "6ccd17a4b797155d", + "s": { + "type": "answer", + "sdp": "v=0\r\no=mozilla...THIS_IS_SDPARTA-99.0 1563643215392536921 0 IN IP4 0.0.0.0\r\ns=-\r\nt=0 0\r\na=sendrecv\r\na=fingerprint:sha-256 75:EC:8B:02:86:7C:C8:04:C7:DF:B3:CE:D5:ED:5B:29:8A:2B:33:7D:B2:B0:FF:C3:3F:89:B2:14:B7:FF:98:5B\r\na=group:BUNDLE 0\r\na=msid-semantic:WMS *\r\nm=application 33301 UDP/DTLS/SCTP webrtc-datachannel\r\nc=IN IP4 192.168.0.99\r\na=candidate:0 1 UDP 2122187007 192.168.0.99 33301 typ host\r\na=candidate:1 1 UDP 2122252543 2a02:8108:310d:b000:66c4:ed1c:57b0:194f 55772 typ host\r\na=candidate:2 1 TCP 2105458943 192.168.0.99 9 typ host tcptype active\r\na=candidate:3 1 TCP 2105524479 2a02:8108:310d:b000:66c4:ed1c:57b0:194f 9 typ host tcptype active\r\na=sendrecv\r\na=end-of-candidates\r\na=ice-pwd:126c1cb120924857d2414c3a5ef3ace9\r\na=ice-ufrag:ef47e5c9\r\na=mid:0\r\na=setup:active\r\na=sctp-port:5000\r\na=max-message-size:1073741823\r\n" + }, + "to": "6b6433490bb88159" +} + +Answer2 +{ + "v": 1, + "id": "f71f0fae072d8329", + "s": { + "type": "answer", + "sdp": "v=0\r\no=mozilla...THIS_IS_SDPARTA-99.0 7701077249990011923 0 IN IP4 0.0.0.0\r\ns=-\r\nt=0 0\r\na=sendrecv\r\na=fingerprint:sha-256 0C:B2:E6:EC:61:95:2A:52:2C:C4:61:0B:81:FE:90:93:46:E8:1C:46:D2:0D:49:98:D2:61:02:98:48:C0:27:44\r\na=group:BUNDLE 0\r\na=msid-semantic:WMS *\r\nm=application 37049 UDP/DTLS/SCTP webrtc-datachannel\r\nc=IN IP4 192.168.0.99\r\na=candidate:0 1 UDP 2122187007 192.168.0.99 37049 typ host\r\na=candidate:1 1 UDP 2122252543 2a02:8108:310d:b000:66c4:ed1c:57b0:194f 36699 typ host\r\na=candidate:2 1 TCP 2105458943 192.168.0.99 9 typ host tcptype active\r\na=candidate:3 1 TCP 2105524479 2a02:8108:310d:b000:66c4:ed1c:57b0:194f 9 typ host tcptype active\r\na=sendrecv\r\na=end-of-candidates\r\na=ice-pwd:f28050b1b31bc209b3034cbd8844da15\r\na=ice-ufrag:51a0d451\r\na=mid:0\r\na=setup:active\r\na=sctp-port:5000\r\na=max-message-size:1073741823\r\n" + }, + "to": "fbb6303b01f9e18d" +} + diff --git a/CRYPTO-UPDATE.md b/CRYPTO-UPDATE.md deleted file mode 100644 index 32cfc84..0000000 --- a/CRYPTO-UPDATE.md +++ /dev/null @@ -1,48 +0,0 @@ -KONTEXT -Vanilla-ECMAScript-Projekt. Mein aktuelles Protokoll committet Geheimnisse -per H(secret) und teilt einen Schlüssel n-of-n auf. Ich ersetze das durch ein -2-von-3 Threshold-Schema: Pedersen-DKG + Threshold-ElGamal mit verifizierbarer -verteilter Entschlüsselung (Chaum-Pedersen-Beweise statt blankem Hash). -Die fertige npm-Library threshold-elgamal scheidet aus: sie funktioniert -zuverlässig nur mit threshold == Teilnehmerzahl, nicht mit 2-von-3. - -AUFGABE -1. Inspiziere zuerst das Repo: finde den bestehenden Commit-/Keypart-Code, - bestimme die öffentliche Schnittstelle (welche Funktionen rufen Aufrufer - auf), und liste sie mir auf, BEVOR du etwas änderst. - -2. Füge @noble/curves als Fundament hinzu (auditiert, dependency-frei). - Verifiziere die aktuelle Version aus dem npm-Registry, bevor du pinst. - KEINE selbstgebaute Feldarithmetik, KEIN eigenes BigInt-Modular-Inverse – - alles über noble. - -3. Implementiere mit threshold=2, parties=3 auf einer Prime-Order-Gruppe - (Ristretto255 via @noble/curves bevorzugt): - - Pedersen-VSS / Pedersen-DKG: zwei Polynome Grad 1, Koeffizienten- - Commitments C_k = g^a_k · h^b_k, h mit unbekanntem log_g(h). - - Verteilte Schlüsselerzeugung: jede Partei dealt einen Beitrag, jede - verifiziert jeden empfangenen Share gegen die Commitments. - - Threshold-ElGamal als KEM: Payload NICHT direkt mit ElGamal. Rekonstruiere - per 2 partiellen Entschlüsselungen das gemeinsame Gruppenelement, leite - daraus per HKDF-SHA256 einen 256-bit Key ab, verschlüssele das Payload mit - AES-256-GCM (WebCrypto subtle, nicht selbst gebaut). - - Jede partielle Entschlüsselung trägt einen Chaum-Pedersen-Beweis der - DL-Gleichheit; verifiziere VOR der Lagrange-Kombination. - -4. Behalte die bestehende öffentliche Schnittstelle bei (gleiche Signaturen), - damit Aufrufer unverändert bleiben. Markiere den alten Code deprecated statt - ihn zu löschen. - -5. Tests: (a) 2 von 3 entschlüsseln erfolgreich, (b) 1 allein scheitert, - (c) manipulierte partielle Entschlüsselung wird durch die Verifikation - abgelehnt, (d) manipulierter VSS-Share fliegt bei der DKG-Verifikation auf, - (e) echtes Payload Round-Trip. - -EINSCHRÄNKUNGEN / WARNUNG -- Das ist selbstgebaute Threshold-Krypto. Setze einen unübersehbaren Kommentar - an den Kern: nicht auditiert, Komposition über noble ist ungeprüft. -- Nutze ausschließlich noble + WebCrypto für alle Primitiven. Erfinde keine - Kurven-, Hash- oder Symmetrie-Operationen selbst. -- Konstante-Zeit-Verhalten kannst du in reinem JS nicht garantieren – halte das - im Kommentar fest. -- Ändere nichts an meiner Geheimnis-/Key-Persistenz, ohne vorher zu fragen. diff --git a/app.js b/app.js index c4143a9..619e69a 100644 --- a/app.js +++ b/app.js @@ -75,6 +75,17 @@ const c = $(containerId); if (!c) return; c.innerHTML = ''; + c.dataset.qrdata = data; + c.style.cursor = 'pointer'; + c.title = 'QR kopieren'; + c.onclick = async () => { + try { + await navigator.clipboard.writeText(data); + const orig = c.innerHTML; + c.textContent = 'Kopiert!'; + setTimeout(() => { c.innerHTML = orig; }, 1200); + } catch (e) {} + }; const size = Math.min(window.innerWidth - 64, 360); qrHost = new QRCode(c, { text: data, width: size, height: size, correctLevel: QRCode.CorrectLevel.L }); } @@ -144,8 +155,7 @@ try { const raw = await detect(); if (raw) { - const bytes = decode45(raw); - const p = await unpack(bytes); + const p = await unpack(raw); if (p.v === 1) { callback(p, raw); return; } } } catch (_) {} @@ -291,8 +301,19 @@ }; mesh.onqrupdate = d => { showQR('qr-host', d); - const raw = $('host-qr-raw'); - if (raw) raw.textContent = d; + if (typeof d !== 'string') { + const raw = $('host-qr-raw'); + if (raw) raw.textContent = 'Kein String: ' + typeof d; + return; + } + unpack(d).then(obj => { + const raw = $('host-qr-raw'); + if (raw) raw.textContent = JSON.stringify(obj, null, 2); + }).catch(err => { + const raw = $('host-qr-raw'); + if (raw) raw.textContent = 'Fehler: ' + (err && err.stack ? err.stack : String(err)); + console.error('unpack error:', err); + }); }; mesh.onreadyupdate = (ready, total) => { $('btn-start-game').disabled = ready < total || total === 0; @@ -331,7 +352,7 @@ if (!raw) return; if (!mesh) return; try { - await unpack(decode45(raw)); + await unpack(raw); } catch (e) { $('host-scan-status').textContent = 'Ungültige Daten'; return; @@ -357,8 +378,10 @@ mesh.onanswerready = answerData => { $('join-answer-section').hidden = false; showQR('qr-answer', answerData); - const raw = $('answer-raw'); - if (raw) raw.textContent = answerData; + unpack(answerData).then(obj => { + const raw = $('answer-raw'); + if (raw) raw.textContent = JSON.stringify(obj, null, 2); + }).catch(() => {}); $('join-scan-status').textContent = 'Antwort-QR dem Host zeigen'; camStop(); }; @@ -394,7 +417,7 @@ if (!raw) return; if (!mesh) { show('start'); return; } try { - await unpack(decode45(raw)); + await unpack(raw); } catch (e) { return; } await mesh.joinFromQR(raw); $('join-qr-paste').value = ''; diff --git a/crypto.js b/crypto.js index fb36749..14a8d05 100644 --- a/crypto.js +++ b/crypto.js @@ -61,77 +61,20 @@ function base64ToBuf(base64) { return bytes; } -function hexToBuf(hex) { - const bytes = new Uint8Array(hex.length / 2); - for (let i = 0; i < hex.length; i += 2) { - bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16); - } - return bytes; -} +/* ─── QR compression: pako.deflateRaw + base64 + SDP dictionary ─── */ -/* ─── QR compression: deflate-raw + base45 ─── */ - -const SDP_ALLOW_A_PREFIXES = ['a=ice-ufrag:', 'a=ice-pwd:', 'a=fingerprint:', 'a=candidate:', 'a=end-of-candidates']; - -function stripSDP(sdp) { - return sdp.split(/\r?\n/).filter(line => { - if (!line) return false; - if (line.startsWith('a=')) { - return SDP_ALLOW_A_PREFIXES.some(p => line.startsWith(p)); - } - return true; - }).map(line => { - if (!line.startsWith('a=candidate:')) return line; - const parts = line.split(' '); - if (parts[2] === 'tcp') return null; - if (parts[7] !== 'host') return null; - if (parts[4] && parts[4].includes(':')) return null; - return line; - }).filter(Boolean).join('\r\n'); -} +const _DICT_B64 = "eyJ2IjoxLCJpZCI6IjIxYzVhNDc0MjQ5Y2JiZGEiLCJzIjp7InR5cGUiOiJvZmZlciIsInNkcCI6InY9MFxyXG5vPS0gNDcyNzAxNTE1MzkwMDkxNjQ2MiAyIElOIElQNCAxMjcuMC4wLjFcclxucz0tXHJcbnQ9MCAwXHJcbmE9Z3JvdXA6QlVORExFIDBcclxuYT1leHRtYXAtYWxsb3ctbWl4ZWRcclxuYT1tc2lkLXNlbWFudGljOiBXTVNcclxubT1hcHBsaWNhdGlvbiA5IFVEUC9EVExTL1NDVFAgd2VicnRjLWRhdGFjaGFubmVsXHJcbmM9SU4gSVA0IDAuMC4wLjBcclxuYT1jYW5kaWRhdGU6MTYwMDY1Nzk0OSAxIHVkcCAyMTEzOTM3MTUxIDE5ZTYwZjI3LTdhNmItNDIwYi1iNDRiLTgwZmIxNmIxMGQ5Ni5sb2NhbCAzMjkyMiB0eXAgaG9zdCBnZW5lcmF0aW9uIDAgbmV0d29yay1jb3N0IDk5OVxyXG5hPWNhbmRpZGF0ZTozODU4MDYyNDQ2IDEgdWRwIDIxMTM5Mzk3MTEgNWFhNDBhYmItYjVjMy00NmQ0LThiMWMtMzM1ZjAwZjlkNTMyLmxvY2FsIDQ4MzE2IHR5cCBob3N0IGdlbmVyYXRpb24gMCBuZXR3b3JrLWNvc3QgOTk5XHJcbmE9aWNlLXVmcmFnOkhKM3BcclxuYT1pY2UtcHdkOjhmRjNmSk14RjNZY2kySHZOWk9tZCs2clxyXG5hPWZpbmdlcnByaW50OnNoYS0yNTYgMEQ6ODA6MDU6NzU6Rjk6NUE6QjY6Q0Q6MjY6MjY6OTI6RDg6MDQ6MUI6MTg6NzI6RkE6QUQ6MjA6RTY6QUQ6NkY6QjM6N0Y6Rjc6OEQ6MkI6RjE6MzY6OEU6NTA6MjlcclxuYT1zZXR1cDphY3RwYXNzXHJcbmE9bWlkOjBcclxuYT1zY3RwLXBvcnQ6NTAwMFxyXG5hPW1heC1tZXNzYWdlLXNpemU6MjYyMTQ0XHJcbiJ9fXsidiI6MSwiaWQiOiI0Yzc5ZmRjNmI1ZTIwZjRmIiwicyI6eyJ0eXBlIjoiYW5zd2VyIiwic2RwIjoidj0wXHJcbm89LSAyNDI2NDEyMTIwNzczMjMwNjYwIDIgSU4gSVA0IDEyNy4wLjAuMVxyXG5zPS1cclxudD0wIDBcclxuYT1ncm91cDpCVU5ETEUgMFxyXG5hPWV4dG1hcC1hbGxvdy1taXhlZFxyXG5hPW1zaWQtc2VtYW50aWM6IFdNU1xyXG5tPWFwcGxpY2F0aW9uIDQ1MzMwIFVEUC9EVExTL1NDVFAgd2VicnRjLWRhdGFjaGFubmVsXHJcbmM9SU4gSVA0IDE5Mi4xNjguMC45OVxyXG5hPWNhbmRpZGF0ZToxMDQwMDYwNDExIDEgdGNwIDE1MTgyMTQ5MTEgMTkyLjE2OC4wLjk5IDkgdHlwIGhvc3QgdGNwdHlwZSBhY3RpdmUgZ2VuZXJhdGlvbiAwIG5ldHdvcmstaWQgMSBuZXR3b3JrLWNvc3QgMTBcclxuYT1jYW5kaWRhdGU6MjI3MTQzMTA0OCAxIHRjcCAxNTE4MjgzMDA3IDJhMDI6ODEwODozMTBkOmIwMDA6NjZjNDplZDFjOjU3YjA6MTk0ZiA5IHR5cCBob3N0IHRjcHR5cGUgYWN0aXZlIGdlbmVyYXRpb24gMCBuZXR3b3JrLWlkIDIgbmV0d29yay1jb3N0IDEwXHJcbmE9Y2FuZGlkYXRlOjMyNzcxMTgzMTkgMSB1ZHAgMjEyMjE5NDY4NyAxOTIuMTY4LjAuOTkgNDUzMzAgdHlwIGhvc3QgZ2VuZXJhdGlvbiAwIG5ldHdvcmstaWQgMSBuZXR3b3JrLWNvc3QgMTBcclxuYT1jYW5kaWRhdGU6MjA0MzI1NjA5MiAxIHVkcCAyMTIyMjYyNzgzIDJhMDI6ODEwODozMTBkOmIwMDA6NjZjNDplZDFjOjU3YjA6MTk0ZiA1MDEzNSB0eXAgaG9zdCBnZW5lcmF0aW9uIDAgbmV0d29yay1pZCAyIG5ldHdvcmstY29zdCAxMFxyXG5hPWljZS11ZnJhZzpqVGo0XHJcbmE9aWNlLXB3ZDpKVGdJbUs2dFNWNFhyRkVUWDRkMU5lV29cclxuYT1maW5nZXJwcmludDpzaGEtMjU2IDEwOkZGOkE3OkExOjREOkM0OkU2OkM4OjU4OjgwOkJFOkRFOjg5OjM1OjY0OjBGOjA4OjQ2OjVGOjYyOkU0OjlBOjI2OkY4OjM1OjZGOjlEOjIzOjlFOjE5OjBBOjVCXHJcbmE9c2V0dXA6YWN0aXZlXHJcbmE9bWlkOjBcclxuYT1zY3RwLXBvcnQ6NTAwMFxyXG5hPW1heC1tZXNzYWdlLXNpemU6MjYyMTQ0XHJcbiJ9LCJ0byI6IjIxYzVhNDc0MjQ5Y2JiZGEifXsidiI6MSwiaWQiOiJlYTg1MTNhZmM5NmE2NmM0IiwicyI6eyJ0eXBlIjoib2ZmZXIiLCJzZHAiOiJ2PTBcclxubz0tIDczNDU2ODE1ODU5OTE5MzU2NjEgMiBJTiBJUDQgMTI3LjAuMC4xXHJcbnM9LVxyXG50PTAgMFxyXG5hPWdyb3VwOkJVTkRMRSAwXHJcbmE9ZXh0bWFwLWFsbG93LW1peGVkXHJcbmE9bXNpZC1zZW1hbnRpYzogV01TXHJcbm09YXBwbGljYXRpb24gOSBVRFAvRFRMUy9TQ1RQIHdlYnJ0Yy1kYXRhY2hhbm5lbFxyXG5jPUlOIElQNCAwLjAuMC4wXHJcbmE9Y2FuZGlkYXRlOjM2ODI4ODk2NTQgMSB1ZHAgMjExMzkzNzE1MSAyZmJlMTYxNy01YmVmLTQzODktOGI4Ni0wZGM4ZDI3ZjAxZmUubG9jYWwgNTIyMzkgdHlwIGhvc3QgZ2VuZXJhdGlvbiAwIG5ldHdvcmstY29zdCA5OTlcclxuYT1jYW5kaWRhdGU6MjUzMjU3OTY1MCAxIHVkcCAyMTEzOTM5NzExIGJhM2Q1NGM1LTBiMDEtNGRmMC1iNjc3LTIxMjYxN2NhMTk5NC5sb2NhbCAzODQ0NyB0eXAgaG9zdCBnZW5lcmF0aW9uIDAgbmV0d29yay1jb3N0IDk5OVxyXG5hPWljZS11ZnJhZzpEV2xoXHJcbmE9aWNlLXB3ZDpiRzFoaURWbHdidnR5RG9DL2w1ZWRYaDFcclxuYT1maW5nZXJwcmludDpzaGEtMjU2IERGOkE3OjBGOjNFOjAwOkFBOjdBOkNCOkU5Ojg0OjY1OjJGOjhCOjlEOkU4OkMxOjM5OkE3OjQ3OkMzOkRFOkI4OjcyOkNGOjgwOjRBOjRBOkZBOjFEOkM1OkVCOkU1XHJcbmE9c2V0dXA6YWN0cGFzc1xyXG5hPW1pZDowXHJcbmE9c2N0cC1wb3J0OjUwMDBcclxuYT1tYXgtbWVzc2FnZS1zaXplOjI2MjE0NFxyXG4ifX17InYiOjEsImlkIjoiMmQyMmI3YTI5NmY5MmFiOSIsInMiOnsidHlwZSI6ImFuc3dlciIsInNkcCI6InY9MFxyXG5vPS0gNjI4NDY1NjgxOTMxMjY5NTI1IDIgSU4gSVA0IDEyNy4wLjAuMVxyXG5zPS1cclxudD0wIDBcclxuYT1ncm91cDpCVU5ETEUgMFxyXG5hPWV4dG1hcC1hbGxvdy1taXhlZFxyXG5hPW1zaWQtc2VtYW50aWM6IFdNU1xyXG5tPWFwcGxpY2F0aW9uIDU5Mzk3IFVEUC9EVExTL1NDVFAgd2VicnRjLWRhdGFjaGFubmVsXHJcbmM9SU4gSVA0IDE5Mi4xNjguMC45OVxyXG5hPWNhbmRpZGF0ZTo4MTE1MzIyNDAgMSB0Y3AgMTUxODIxNDkxMSAxOTIuMTY4LjAuOTkgOSB0eXAgaG9zdCB0Y3B0eXBlIGFjdGl2ZSBnZW5lcmF0aW9uIDAgbmV0d29yay1pZCAxIG5ldHdvcmstY29zdCAxMFxyXG5hPWNhbmRpZGF0ZToyMzI4MDgyODUxIDEgdGNwIDE1MTgyODMwMDcgMmEwMjo4MTA4OjMxMGQ6YjAwMDo2NmM0OmVkMWM6NTdiMDoxOTRmIDkgdHlwIGhvc3QgdGNwdHlwZSBhY3RpdmUgZ2VuZXJhdGlvbiAwIG5ldHdvcmstaWQgMiBuZXR3b3JrLWNvc3QgMTBcclxuYT1jYW5kaWRhdGU6MzQ3MjEwODM1NiAxIHVkcCAyMTIyMTk0Njg3IDE5Mi4xNjguMC45OSA1OTM5NyB0eXAgaG9zdCBnZW5lcmF0aW9uIDAgbmV0d29yay1pZCAxIG5ldHdvcmstY29zdCAxMFxyXG5hPWNhbmRpZGF0ZToxOTUzMDY2Mjk1IDEgdWRwIDIxMjIyNjI3ODMgMmEwMjo4MTA4OjMxMGQ6YjAwMDo2NmM0OmVkMWM6NTdiMDoxOTRmIDM0MDk3IHR5cCBob3N0IGdlbmVyYXRpb24gMCBuZXR3b3JrLWlkIDIgbmV0d29yay1jb3N0IDEwXHJcbmE9aWNlLXVmcmFnOlc0cndcclxuYT1pY2UtcHdkOmFzVUVHTkFUazdORGFpQWwvcUFhNmxIWFxyXG5hPWZpbmdlcnByaW50OnNoYS0yNTYgQTg6Nzg6NDA6Nzk6MkQ6RDk6Rjc6Mzc6NTg6RkQ6MUI6MTg6NDE6MTI6RDI6MkU6NDk6NkQ6MUU6MjY6RjM6NkE6RUM6RTM6RTU6OUM6Rjc6QkM6MzM6RjY6ODE6NzlcclxuYT1zZXR1cDphY3RpdmVcclxuYT1taWQ6MFxyXG5hPXNjdHAtcG9ydDo1MDAwXHJcbmE9bWF4LW1lc3NhZ2Utc2l6ZToyNjIxNDRcclxuIn0sInRvIjoiZWE4NTEzYWZjOTZhNjZjNCJ9eyJ2IjoxLCJpZCI6IjZiNjQzMzQ5MGJiODgxNTkiLCJzIjp7InR5cGUiOiJvZmZlciIsInNkcCI6InY9MFxyXG5vPW1vemlsbGEuLi5USElTX0lTX1NEUEFSVEEtOTkuMCA1ODU1OTUwMzk3Mzk0NDk0OTI5IDAgSU4gSVA0IDAuMC4wLjBcclxucz0tXHJcbnQ9MCAwXHJcbmE9c2VuZHJlY3ZcclxuYT1maW5nZXJwcmludDpzaGEtMjU2IEJDOkQwOjU3OjVBOjdCOjQ0OkFBOkNGOkUwOkQ3OjA1OkMyOkNEOkQ4OkMyOkY5OjI2OkQ3OjhEOjgyOkQ1Ojk5OkNDOjU0OjUxOkE4OjYxOjY0OjNDOjY3OjUzOkE5XHJcbmE9Z3JvdXA6QlVORExFIDBcclxuYT1tc2lkLXNlbWFudGljOldNUyAqXHJcbm09YXBwbGljYXRpb24gMzgxMTUgVURQL0RUTFMvU0NUUCB3ZWJydGMtZGF0YWNoYW5uZWxcclxuYz1JTiBJUDQgMTkyLjE2OC4wLjk5XHJcbmE9Y2FuZGlkYXRlOjAgMSBVRFAgMjEyMjE4NzAwNyAxOTIuMTY4LjAuOTkgMzgxMTUgdHlwIGhvc3RcclxuYT1jYW5kaWRhdGU6MSAxIFVEUCAyMTIyMjUyNTQzIDJhMDI6ODEwODozMTBkOmIwMDA6NjZjNDplZDFjOjU3YjA6MTk0ZiA1MDc1MSB0eXAgaG9zdFxyXG5hPWNhbmRpZGF0ZToyIDEgVENQIDIxMDU0NTg5NDMgMTkyLjE2OC4wLjk5IDkgdHlwIGhvc3QgdGNwdHlwZSBhY3RpdmVcclxuYT1jYW5kaWRhdGU6MyAxIFRDUCAyMTA1NTI0NDc5IDJhMDI6ODEwODozMTBkOmIwMDA6NjZjNDplZDFjOjU3YjA6MTk0ZiA5IHR5cCBob3N0IHRjcHR5cGUgYWN0aXZlXHJcbmE9c2VuZHJlY3ZcclxuYT1lbmQtb2YtY2FuZGlkYXRlc1xyXG5hPWljZS1wd2Q6ZTcxZjAxMjBkODRhYzE5MWI1NzkzNmQ3YzJjNjdiYmRcclxuYT1pY2UtdWZyYWc6MzM5MWNkM2JcclxuYT1taWQ6MFxyXG5hPXNldHVwOmFjdHBhc3NcclxuYT1zY3RwLXBvcnQ6NTAwMFxyXG5hPW1heC1tZXNzYWdlLXNpemU6MTA3Mzc0MTgyM1xyXG4ifX17InYiOjEsImlkIjoiYWI4MzBiMzc2ZjQ2NjgzYiIsInMiOnsidHlwZSI6ImFuc3dlciIsInNkcCI6InY9MFxyXG5vPS0gNzgwMzE2ODc0NzM4NTY3NDM2MSAyIElOIElQNCAxMjcuMC4wLjFcclxucz0tXHJcbnQ9MCAwXHJcbmE9Z3JvdXA6QlVORExFIDBcclxuYT1tc2lkLXNlbWFudGljOiBXTVNcclxubT1hcHBsaWNhdGlvbiA1MjA5MyBVRFAvRFRMUy9TQ1RQIHdlYnJ0Yy1kYXRhY2hhbm5lbFxyXG5jPUlOIElQNCAxOTIuMTY4LjAuOTlcclxuYT1jYW5kaWRhdGU6NDgwMDU4ODA2IDEgdGNwIDE1MTgyMTQ5MTEgMTkyLjE2OC4wLjk5IDkgdHlwIGhvc3QgdGNwdHlwZSBhY3RpdmUgZ2VuZXJhdGlvbiAwIG5ldHdvcmstaWQgMSBuZXR3b3JrLWNvc3QgMTBcclxuYT1jYW5kaWRhdGU6Mjc4NTA0MTM0OSAxIHRjcCAxNTE4MjgzMDA3IDJhMDI6ODEwODozMTBkOmIwMDA6NjZjNDplZDFjOjU3YjA6MTk0ZiA5IHR5cCBob3N0IHRjcHR5cGUgYWN0aXZlIGdlbmVyYXRpb24gMCBuZXR3b3JrLWlkIDIgbmV0d29yay1jb3N0IDEwXHJcbmE9Y2FuZGlkYXRlOjM3OTUzMDY3ODYgMSB1ZHAgMjEyMjE5NDY4NyAxOTIuMTY4LjAuOTkgNTIwOTMgdHlwIGhvc3QgZ2VuZXJhdGlvbiAwIG5ldHdvcmstaWQgMSBuZXR3b3JrLWNvc3QgMTBcclxuYT1jYW5kaWRhdGU6MTQ4NzU3MjgxNyAxIHVkcCAyMTIyMjYyNzgzIDJhMDI6ODEwODozMTBkOmIwMDA6NjZjNDplZDFjOjU3YjA6MTk0ZiA0Mjc2MSB0eXAgaG9zdCBnZW5lcmF0aW9uIDAgbmV0d29yay1pZCAyIG5ldHdvcmstY29zdCAxMFxyXG5hPWljZS11ZnJhZzo2WjJXXHJcbmE9aWNlLXB3ZDptZHVJbldEcTFURVFQVjBtS25WaXZxN2xcclxuYT1maW5nZXJwcmludDpzaGEtMjU2IEY2OkE0OjQyOkJCOjhFOjVGOjg1OkNGOkExOkFCOkUxOkU3OjRFOjNFOjcxOjQ5OjRDOkZBOjAzOjY4OjFCOkUyOjkzOkI2OjI1OjA5OjUwOkI0OjIyOkZCOjVCOjIwXHJcbmE9c2V0dXA6YWN0aXZlXHJcbmE9bWlkOjBcclxuYT1zY3RwLXBvcnQ6NTAwMFxyXG5hPW1heC1tZXNzYWdlLXNpemU6MjYyMTQ0XHJcbiJ9LCJ0byI6IjZiNjQzMzQ5MGJiODgxNTkifXsidiI6MSwiaWQiOiI2YjY0MzM0OTBiYjg4MTU5IiwicyI6eyJ0eXBlIjoib2ZmZXIiLCJzZHAiOiJ2PTBcclxubz1tb3ppbGxhLi4uVEhJU19JU19TRFBBUlRBLTk5LjAgNTg1NTk1MDM5NzM5NDQ5NDkyOSAwIElOIElQNCAwLjAuMC4wXHJcbnM9LVxyXG50PTAgMFxyXG5hPXNlbmRyZWN2XHJcbmE9ZmluZ2VycHJpbnQ6c2hhLTI1NiBCQzpEMDo1Nzo1QTo3Qjo0NDpBQTpDRjpFMDpENzowNTpDMjpDRDpEODpDMjpGOToyNjpENzo4RDo4MjpENTo5OTpDQzo1NDo1MTpBODo2MTo2NDozQzo2Nzo1MzpBOVxyXG5hPWdyb3VwOkJVTkRMRSAwXHJcbmE9bXNpZC1zZW1hbnRpYzpXTVMgKlxyXG5tPWFwcGxpY2F0aW9uIDM4MTE1IFVEUC9EVExTL1NDVFAgd2VicnRjLWRhdGFjaGFubmVsXHJcbmM9SU4gSVA0IDE5Mi4xNjguMC45OVxyXG5hPWNhbmRpZGF0ZTowIDEgVURQIDIxMjIxODcwMDcgMTkyLjE2OC4wLjk5IDM4MTE1IHR5cCBob3N0XHJcbmE9Y2FuZGlkYXRlOjEgMSBVRFAgMjEyMjI1MjU0MyAyYTAyOjgxMDg6MzEwZDpiMDAwOjY2YzQ6ZWQxYzo1N2IwOjE5NGYgNTA3NTEgdHlwIGhvc3RcclxuYT1jYW5kaWRhdGU6MiAxIFRDUCAyMTA1NDU4OTQzIDE5Mi4xNjguMC45OSA5IHR5cCBob3N0IHRjcHR5cGUgYWN0aXZlXHJcbmE9Y2FuZGlkYXRlOjMgMSBUQ1AgMjEwNTUyNDQ3OSAyYTAyOjgxMDg6MzEwZDpiMDAwOjY2YzQ6ZWQxYzo1N2IwOjE5NGYgOSB0eXAgaG9zdCB0Y3B0eXBlIGFjdGl2ZVxyXG5hPXNlbmRyZWN2XHJcbmE9ZW5kLW9mLWNhbmRpZGF0ZXNcclxuYT1pY2UtcHdkOmU3MWYwMTIwZDg0YWMxOTFiNTc5MzZkN2MyYzY3YmJkXHJcbmE9aWNlLXVmcmFnOjMzOTFjZDNiXHJcbmE9bWlkOjBcclxuYT1zZXR1cDphY3RwYXNzXHJcbmE9c2N0cC1wb3J0OjUwMDBcclxuYT1tYXgtbWVzc2FnZS1zaXplOjEwNzM3NDE4MjNcclxuIn19eyJ2IjoxLCJpZCI6ImZiYjYzMDNiMDFmOWUxOGQiLCJzIjp7InR5cGUiOiJvZmZlciIsInNkcCI6InY9MFxyXG5vPW1vemlsbGEuLi5USElTX0lTX1NEUEFSVEEtOTkuMCA5MjIyNjk5MjU0NDcxNDI4MjU1IDAgSU4gSVA0IDAuMC4wLjBcclxucz0tXHJcbnQ9MCAwXHJcbmE9c2VuZHJlY3ZcclxuYT1maW5nZXJwcmludDpzaGEtMjU2IDE5OjJCOjQ5OkRCOkVCOjE4OkE2OjhFOjREOkVGOjg2OjczOjUwOkVDOkFCOjU5Ojk2OkJCOjI2OkVFOkM0OjM0OkQ1OjBCOjVEOjFBOjJFOjEyOjE4OjNCOjM0OkU4XHJcbmE9Z3JvdXA6QlVORExFIDBcclxuYT1tc2lkLXNlbWFudGljOldNUyAqXHJcbm09YXBwbGljYXRpb24gNDk0MzcgVURQL0RUTFMvU0NUUCB3ZWJydGMtZGF0YWNoYW5uZWxcclxuYz1JTiBJUDQgMTkyLjE2OC4wLjk5XHJcbmE9Y2FuZGlkYXRlOjAgMSBVRFAgMjEyMjE4NzAwNyAxOTIuMTY4LjAuOTkgNDk0MzcgdHlwIGhvc3RcclxuYT1jYW5kaWRhdGU6MSAxIFVEUCAyMTIyMjUyNTQzIDJhMDI6ODEwODozMTBkOmIwMDA6NjZjNDplZDFjOjU3YjA6MTk0ZiAzMzc5NSB0eXAgaG9zdFxyXG5hPWNhbmRpZGF0ZToyIDEgVENQIDIxMDU0NTg5NDMgMTkyLjE2OC4wLjk5IDkgdHlwIGhvc3QgdGNwdHlwZSBhY3RpdmVcclxuYT1jYW5kaWRhdGU6MyAxIFRDUCAyMTA1NTI0NDc5IDJhMDI6ODEwODozMTBkOmIwMDA6NjZjNDplZDFjOjU3YjA6MTk0ZiA5IHR5cCBob3N0IHRjcHR5cGUgYWN0aXZlXHJcbmE9c2VuZHJlY3ZcclxuYT1lbmQtb2YtY2FuZGlkYXRlc1xyXG5hPWljZS1wd2Q6YzM0MTVkYjc5MjBmMTRhMzMwOTE5ZTcyMzllMGQzNTNcclxuYT1pY2UtdWZyYWc6Njg1YTY4OWNcclxuYT1taWQ6MFxyXG5hPXNldHVwOmFjdHBhc3NcclxuYT1zY3RwLXBvcnQ6NTAwMFxyXG5hPW1heC1tZXNzYWdlLXNpemU6MTA3Mzc0MTgyM1xyXG4ifX17InYiOjEsImlkIjoiNmNjZDE3YTRiNzk3MTU1ZCIsInMiOnsidHlwZSI6ImFuc3dlciIsInNkcCI6InY9MFxyXG5vPW1vemlsbGEuLi5USElTX0lTX1NEUEFSVEEtOTkuMCAxNTYzNjQzMjE1MzkyNTM2OTIxIDAgSU4gSVA0IDAuMC4wLjBcclxucz0tXHJcbnQ9MCAwXHJcbmE9c2VuZHJlY3ZcclxuYT1maW5nZXJwcmludDpzaGEtMjU2IDc1OkVDOjhCOjAyOjg2OjdDOkM4OjA0OkM3OkRGOkIzOkNFOkQ1OkVEOjVCOjI5OjhBOjJCOjMzOjdEOkIyOkIwOkZGOkMzOjNGOjg5OkIyOjE0OkI3OkZGOjk4OjVCXHJcbmE9Z3JvdXA6QlVORExFIDBcclxuYT1tc2lkLXNlbWFudGljOldNUyAqXHJcbm09YXBwbGljYXRpb24gMzMzMDEgVURQL0RUTFMvU0NUUCB3ZWJydGMtZGF0YWNoYW5uZWxcclxuYz1JTiBJUDQgMTkyLjE2OC4wLjk5XHJcbmE9Y2FuZGlkYXRlOjAgMSBVRFAgMjEyMjE4NzAwNyAxOTIuMTY4LjAuOTkgMzMzMDEgdHlwIGhvc3RcclxuYT1jYW5kaWRhdGU6MSAxIFVEUCAyMTIyMjUyNTQzIDJhMDI6ODEwODozMTBkOmIwMDA6NjZjNDplZDFjOjU3YjA6MTk0ZiA1NTc3MiB0eXAgaG9zdFxyXG5hPWNhbmRpZGF0ZToyIDEgVENQIDIxMDU0NTg5NDMgMTkyLjE2OC4wLjk5IDkgdHlwIGhvc3QgdGNwdHlwZSBhY3RpdmVcclxuYT1jYW5kaWRhdGU6MyAxIFRDUCAyMTA1NTI0NDc5IDJhMDI6ODEwODozMTBkOmIwMDA6NjZjNDplZDFjOjU3YjA6MTk0ZiA5IHR5cCBob3N0IHRjcHR5cGUgYWN0aXZlXHJcbmE9c2VuZHJlY3ZcclxuYT1lbmQtb2YtY2FuZGlkYXRlc1xyXG5hPWljZS1wd2Q6MTI2YzFjYjEyMDkyNDg1N2QyNDE0YzNhNWVmM2FjZTlcclxuYT1pY2UtdWZyYWc6ZWY0N2U1YzlcclxuYT1taWQ6MFxyXG5hPXNldHVwOmFjdGl2ZVxyXG5hPXNjdHAtcG9ydDo1MDAwXHJcbmE9bWF4LW1lc3NhZ2Utc2l6ZToxMDczNzQxODIzXHJcbiJ9LCJ0byI6IjZiNjQzMzQ5MGJiODgxNTkifXsidiI6MSwiaWQiOiJmNzFmMGZhZTA3MmQ4MzI5IiwicyI6eyJ0eXBlIjoiYW5zd2VyIiwic2RwIjoidj0wXHJcbm89bW96aWxsYS4uLlRISVNfSVNfU0RQQVJUQS05OS4wIDc3MDEwNzcyNDk5OTAwMTE5MjMgMCBJTiBJUDQgMC4wLjAuMFxyXG5zPS1cclxudD0wIDBcclxuYT1zZW5kcmVjdlxyXG5hPWZpbmdlcnByaW50OnNoYS0yNTYgMEM6QjI6RTY6RUM6NjE6OTU6MkE6NTI6MkM6QzQ6NjE6MEI6ODE6RkU6OTA6OTM6NDY6RTg6MUM6NDY6RDI6MEQ6NDk6OTg6RDI6NjE6MDI6OTg6NDg6QzA6Mjc6NDRcclxuYT1ncm91cDpCVU5ETEUgMFxyXG5hPW1zaWQtc2VtYW50aWM6V01TICpcclxubT1hcHBsaWNhdGlvbiAzNzA0OSBVRFAvRFRMUy9TQ1RQIHdlYnJ0Yy1kYXRhY2hhbm5lbFxyXG5jPUlOIElQNCAxOTIuMTY4LjAuOTlcclxuYT1jYW5kaWRhdGU6MCAxIFVEUCAyMTIyMTg3MDA3IDE5Mi4xNjguMC45OSAzNzA0OSB0eXAgaG9zdFxyXG5hPWNhbmRpZGF0ZToxIDEgVURQIDIxMjIyNTI1NDMgMmEwMjo4MTA4OjMxMGQ6YjAwMDo2NmM0OmVkMWM6NTdiMDoxOTRmIDM2Njk5IHR5cCBob3N0XHJcbmE9Y2FuZGlkYXRlOjIgMSBUQ1AgMjEwNTQ1ODk0MyAxOTIuMTY4LjAuOTkgOSB0eXAgaG9zdCB0Y3B0eXBlIGFjdGl2ZVxyXG5hPWNhbmRpZGF0ZTozIDEgVENQIDIxMDU1MjQ0NzkgMmEwMjo4MTA4OjMxMGQ6YjAwMDo2NmM0OmVkMWM6NTdiMDoxOTRmIDkgdHlwIGhvc3QgdGNwdHlwZSBhY3RpdmVcclxuYT1zZW5kcmVjdlxyXG5hPWVuZC1vZi1jYW5kaWRhdGVzXHJcbmE9aWNlLXB3ZDpmMjgwNTBiMWIzMWJjMjA5YjMwMzRjYmQ4ODQ0ZGExNVxyXG5hPWljZS11ZnJhZzo1MWEwZDQ1MVxyXG5hPW1pZDowXHJcbmE9c2V0dXA6YWN0aXZlXHJcbmE9c2N0cC1wb3J0OjUwMDBcclxuYT1tYXgtbWVzc2FnZS1zaXplOjEwNzM3NDE4MjNcclxuIn0sInRvIjoiZmJiNjMwM2IwMWY5ZTE4ZCJ9"; +const SDP_DICT = base64ToBuf(_DICT_B64); async function pack(obj) { const json = JSON.stringify(obj); - const blob = new Blob([json]); - const cs = new CompressionStream('deflate-raw'); - const ab = await new Response(blob.stream().pipeThrough(cs)).arrayBuffer(); - return new Uint8Array(ab); + const bytes = new TextEncoder().encode(json); + const compressed = pako.deflateRaw(bytes, {dictionary: SDP_DICT}); + return bufToBase64(compressed); } -async function unpack(bytes) { - const blob = new Blob([bytes]); - const ds = new DecompressionStream('deflate-raw'); - const ab = await new Response(blob.stream().pipeThrough(ds)).arrayBuffer(); - return JSON.parse(new TextDecoder().decode(ab)); -} - -const B45 = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:'; - -function encode45(buf) { - const bytes = new Uint8Array(buf); - let s = ''; - for (let i = 0; i < bytes.length; i += 2) { - if (i + 1 < bytes.length) { - const v = bytes[i] * 256 + bytes[i + 1]; - s += B45[v % 45] + B45[Math.floor(v / 45) % 45] + B45[Math.floor(v / 2025)]; - } else { - const v = bytes[i]; - s += B45[v % 45] + B45[Math.floor(v / 45)]; - } - } - return s; -} - -function decode45(str) { - const bytes = []; - for (let i = 0; i < str.length; i += 3) { - if (i + 2 < str.length) { - const v = B45.indexOf(str[i]) + B45.indexOf(str[i + 1]) * 45 + B45.indexOf(str[i + 2]) * 2025; - bytes.push(Math.floor(v / 256), v % 256); - } else { - const v = B45.indexOf(str[i]) + B45.indexOf(str[i + 1]) * 45; - bytes.push(v); - } - } - return Uint8Array.from(bytes); +async function unpack(str) { + const compressed = base64ToBuf(str); + const bytes = pako.inflateRaw(compressed, {dictionary: SDP_DICT}); + return JSON.parse(new TextDecoder().decode(bytes)); } diff --git a/index.html b/index.html index e75712f..160eab1 100644 --- a/index.html +++ b/index.html @@ -136,6 +136,7 @@
© by neunzig.wtf · commit HASH
+ diff --git a/p2p.js b/p2p.js index 7a39a20..f8f14c0 100644 --- a/p2p.js +++ b/p2p.js @@ -5,7 +5,6 @@ class MeshNet { this._pendingMeshPeers = new Map(); this._pendingPeer = null; this._currentQRData = null; - this._fullSDPData = null; this.isHost = false; this.hostPeerId = null; this.roster = []; @@ -47,9 +46,7 @@ class MeshNet { signalSent = true; this._log('SDP-Signal (Offer) empfangen'); - const packed = await pack({ v: 1, id: this.myPeerId, s: signal }); - this._currentQRData = encode45(packed); - this._fullSDPData = this._currentQRData; + this._currentQRData = await pack({ v: 1, id: this.myPeerId, s: signal }); if (this.onqrupdate) this.onqrupdate(this._currentQRData); this._log('Offer QR erstellt'); @@ -81,7 +78,7 @@ class MeshNet { return; } try { - const data = await unpack(decode45(answerStr)); + const data = await unpack(answerStr); this._pendingPeer.peerId = data.id; this._pendingPeer.peer.signal(data.s); this._pendingPeer.signalReceived = true; @@ -95,7 +92,7 @@ class MeshNet { /* ─── Peer: join ─── */ async joinFromQR(qrContent) { - const data = await unpack(decode45(qrContent)); + const data = await unpack(qrContent); const remoteId = data.id; this.hostPeerId = remoteId; const offer = data.s; @@ -107,8 +104,7 @@ class MeshNet { peer.on('signal', async signal => { if (answerSent) return; answerSent = true; - const packed = await pack({ v: 1, id: this.myPeerId, s: signal, to: remoteId }); - const answerData = encode45(packed); + const answerData = await pack({ v: 1, id: this.myPeerId, s: signal, to: remoteId }); if (this.onanswerready) this.onanswerready(answerData); this._log('Answer QR erstellt'); }); @@ -393,10 +389,6 @@ class MeshNet { return this.names.get(peerId) || peerId.slice(0, 8); } - getFullSDPData() { - return this._fullSDPData; - } - destroy() { this._log('MeshNet wird zerstört'); for (const peer of this.connections.values()) { diff --git a/test-pairing.html b/test-pairing.html index 20b9662..a60f9b4 100644 --- a/test-pairing.html +++ b/test-pairing.html @@ -168,19 +168,17 @@ return new Promise(r => setTimeout(r, ms)); } - /* ─── Test: QR-Daten format (encode45/decode45 roundtrip) ─── */ + /* ─── Test: pack/unpack Roundtrip ─── */ async function testQRRoundtrip() { const testData = { v: 1, id: 'aabbccdd', s: { type: 'offer', sdp: 'mock-sdp' } }; const packed = await pack(testData); - const encoded = encode45(packed); - const decoded = decode45(encoded); - const unpacked = await unpack(decoded); + const unpacked = await unpack(packed); const pass = unpacked.v === 1 && unpacked.id === 'aabbccdd' && unpacked.s.type === 'offer'; return { pass, detail: pass - ? `pack → encode45 → decode45 → unpack OK\nEncoded: ${encoded.slice(0, 40)}…` + ? `pack → unpack OK\nBase64: ${packed.slice(0, 40)}…` : `Roundtrip fehlgeschlagen: ${JSON.stringify(unpacked)}` }; } @@ -198,7 +196,7 @@ let parsed; try { - parsed = await unpack(decode45(host._qrData)); + parsed = await unpack(host._qrData); } catch (e) { return { pass: false, detail: 'Host QR parsing failed: ' + e.message }; } @@ -211,7 +209,7 @@ if (!peer._answerData) return { pass: false, detail: 'Peer Answer QR wurde nicht generiert' }; try { - parsed = await unpack(decode45(peer._answerData)); + parsed = await unpack(peer._answerData); } catch (e) { return { pass: false, detail: 'Peer Answer QR parsing failed: ' + e.message }; } @@ -425,7 +423,7 @@ $('summary').style.display = 'none'; const tests = [ - { id: 't-qr-roundtrip', label: 'QR-Format: pack/encode45/decode45/unpack', fn: testQRRoundtrip }, + { id: 't-qr-roundtrip', label: 'QR-Format: pack/unpack (pako+SDP dict)', fn: testQRRoundtrip }, { id: 't-basic-pairing', label: 'Basis-Pairing: Host → QR → Peer → Answer → Feed', fn: testBasicPairing }, { id: 't-chat-pairing', label: 'Chat via Pairing: Host ↔ Peer', fn: testChatAfterPairing }, { id: 't-onchat', label: 'onchat Callback bei Chat-Empfang', fn: testOnchatCallback }, diff --git a/vendor/pako.min.js b/vendor/pako.min.js new file mode 100644 index 0000000..2535eaf --- /dev/null +++ b/vendor/pako.min.js @@ -0,0 +1,2 @@ +/*! pako 2.1.0 https://github.com/nodeca/pako @license (MIT AND Zlib) */ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).pako={})}(this,(function(t){"use strict";function e(t){let e=t.length;for(;--e>=0;)t[e]=0}const a=256,i=286,n=30,s=15,r=new Uint8Array([0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0]),o=new Uint8Array([0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13]),l=new Uint8Array([0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,3,7]),h=new Uint8Array([16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15]),d=new Array(576);e(d);const _=new Array(60);e(_);const f=new Array(512);e(f);const c=new Array(256);e(c);const u=new Array(29);e(u);const w=new Array(n);function m(t,e,a,i,n){this.static_tree=t,this.extra_bits=e,this.extra_base=a,this.elems=i,this.max_length=n,this.has_stree=t&&t.length}let b,g,p;function k(t,e){this.dyn_tree=t,this.max_code=0,this.stat_desc=e}e(w);const v=t=>t<256?f[t]:f[256+(t>>>7)],y=(t,e)=>{t.pending_buf[t.pending++]=255&e,t.pending_buf[t.pending++]=e>>>8&255},x=(t,e,a)=>{t.bi_valid>16-a?(t.bi_buf|=e<>>=p,c-=p),c<15&&(f+=z[a++]< o){t.msg="invalid distance too far back",E.mode=he;break t}if(f>>>=p,c-=p,p=n-s,v>p){if(p=v-p,p>h&&E.sane){t.msg="invalid distance too far back",E.mode=he;break t}if(y=0,x=_,0===d){if(y+=l-p,p