generated from knso/webklar-preview-template
Demo-Seite: - Ampel in Kalendertagen, naechstes kommendes Datum, "Termin am"/"Frist bis" - nie "Deutsch | Deutsch"; Beraterin bekommt immer Deutsch (d=1), das Handy erkennt seine Sprache selbst und speichert die PC-Vorgabe nicht dauerhaft - Code-Feld ausserhalb von #eingabe; tolerante Code-Eingabe; Ablaufdatum im Banner; leeres/abgelaufenes Einrichtungs-Kontingent ohne Kaufangebot, mit Telefonnummer; nach Aufladen reicht ein Neuladen - Handy-Modus: endet nach dem ersten Brief, uebersteht ein Neuladen (sessionStorage), keine Sackgasse ohne Zugang, ehrlicher Hinweis + "Noch einmal senden", wenn das Ergebnis den PC nicht erreicht - PC wartet geduldig, solange verbunden; meldet langsame Analysen, fehlende Verbindung, fehlenden Zugang; Warnung beim Verlassen - Einwilligung pro Brief, nennt Gesundheit/Herkunft/Aufenthalt - localStorage nur ueber abgesicherten Helfer; Dateifeld wird geleert - RTL (bdi, dir=auto), Sprachliste mit deutschem Zusatz, groesserer QR Worker: KI-Aufruf und Router-catch liefern immer JSON mit CORS; Logs ohne Briefinhalt; [observability]; Nutzungszaehler je Analyse (Datum, Sprache, 390 Tage); normCode + 30 Fehlversuche; Uebergabe-TTL gleitend. Werkzeug: Auswertung pro Woche/Sprache, Eingabepruefung, --bis in der Hilfe. Datenschutz: SCC statt DPF, "in der Regel 30 Tage", Server-Protokolle, Nutzungszaehler. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
124 lines
4.7 KiB
JavaScript
124 lines
4.7 KiB
JavaScript
/**
|
|
* Übergabe Handy ↔ PC — Ende-zu-Ende verschlüsselt.
|
|
*
|
|
* Ein Gerät erzeugt eine Übergabe (zufällige ID + AES-256-Schlüssel) und
|
|
* zeigt sie als QR-Code. Beides steht hinter dem `#` der Adresse; dieser
|
|
* Teil wird von Browsern NIE an einen Server geschickt. Der Worker sieht nur
|
|
* verschlüsselte Päckchen und löscht sie nach 15 Minuten.
|
|
*
|
|
* Päckchen-Format: base64url( IV[12 Byte] || AES-GCM-Chiffrat ) eines JSON.
|
|
* Die App (src/utils/uebergabe.ts) benutzt exakt dasselbe Format.
|
|
*/
|
|
(function (w) {
|
|
'use strict';
|
|
|
|
var LOKAL = /^(localhost|127\.0\.0\.1)$/.test(w.location.hostname);
|
|
var BASIS = (LOKAL ? 'http://localhost:8799' : 'https://behoerdenklar-proxy.behoerdenbrief.workers.dev') + '/uebergabe/';
|
|
|
|
function b64u(bytes) {
|
|
var s = '';
|
|
for (var i = 0; i < bytes.length; i++) s += String.fromCharCode(bytes[i]);
|
|
return btoa(s).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
}
|
|
function ausB64u(text) {
|
|
var s = atob(text.replace(/-/g, '+').replace(/_/g, '/'));
|
|
var b = new Uint8Array(s.length);
|
|
for (var i = 0; i < s.length; i++) b[i] = s.charCodeAt(i);
|
|
return b;
|
|
}
|
|
function zufall(n) { return w.crypto.getRandomValues(new Uint8Array(n)); }
|
|
function schluessel(roh) {
|
|
return w.crypto.subtle.importKey('raw', roh, { name: 'AES-GCM' }, false, ['encrypt', 'decrypt']);
|
|
}
|
|
|
|
/** Neue Übergabe: { id, k, key } — k ist der Schlüssel als Text für den QR-Code. */
|
|
async function neu() {
|
|
var roh = zufall(32);
|
|
return { id: b64u(zufall(16)), k: b64u(roh), key: await schluessel(roh) };
|
|
}
|
|
|
|
/** Übergabe aus ID + Schlüsseltext (aus dem QR-Code) öffnen. */
|
|
async function oeffne(id, k) {
|
|
if (!/^[A-Za-z0-9_-]{22}$/.test(id || '') || !/^[A-Za-z0-9_-]{43}$/.test(k || '')) return null;
|
|
return { id: id, k: k, key: await schluessel(ausB64u(k)) };
|
|
}
|
|
|
|
async function verschluesseln(ue, daten) {
|
|
var iv = zufall(12);
|
|
var klar = new TextEncoder().encode(JSON.stringify(daten));
|
|
var ct = new Uint8Array(await w.crypto.subtle.encrypt({ name: 'AES-GCM', iv: iv }, ue.key, klar));
|
|
var alles = new Uint8Array(iv.length + ct.length);
|
|
alles.set(iv, 0); alles.set(ct, iv.length);
|
|
return b64u(alles);
|
|
}
|
|
|
|
/** Entschlüsseln; null, wenn das Päckchen nicht von dieser Übergabe stammt. */
|
|
async function entschluesseln(ue, text) {
|
|
try {
|
|
var b = ausB64u(text);
|
|
var klar = await w.crypto.subtle.decrypt({ name: 'AES-GCM', iv: b.slice(0, 12) }, ue.key, b.slice(12));
|
|
return JSON.parse(new TextDecoder().decode(klar));
|
|
} catch (e) { return null; }
|
|
}
|
|
|
|
/** Päckchen in ein Fach legen (z. B. „status", „ergebnis"). */
|
|
async function senden(ue, fach, daten) {
|
|
var r = await fetch(BASIS + ue.id + '/' + fach, { method: 'PUT', body: await verschluesseln(ue, daten) });
|
|
if (!r.ok) throw new Error('Übergabe fehlgeschlagen (' + r.status + ')');
|
|
}
|
|
|
|
/** Alle Fächer holen und entschlüsseln: { status: {...}, ergebnis: {...} }. */
|
|
async function holen(ue) {
|
|
// Zeitlimit, damit eine hängende Anfrage die Abfrage am PC nicht blockiert
|
|
var signal = (w.AbortSignal && w.AbortSignal.timeout) ? w.AbortSignal.timeout(10000) : undefined;
|
|
var r = await fetch(BASIS + ue.id, { cache: 'no-store', signal: signal });
|
|
if (!r.ok) throw new Error('Übergabe nicht erreichbar (' + r.status + ')');
|
|
var faecher = (await r.json()).faecher || {};
|
|
var aus = {};
|
|
for (var f in faecher) {
|
|
var d = await entschluesseln(ue, faecher[f]);
|
|
if (d) aus[f] = d;
|
|
}
|
|
return aus;
|
|
}
|
|
|
|
function loeschen(ue) {
|
|
return fetch(BASIS + ue.id, { method: 'DELETE' }).catch(function () {});
|
|
}
|
|
|
|
/** Adresse für den QR-Code. `werte` landet komplett hinter dem `#`. */
|
|
function adresse(werte) {
|
|
var teile = [];
|
|
for (var k in werte) if (werte[k] !== undefined && werte[k] !== null && werte[k] !== '') {
|
|
teile.push(k + '=' + encodeURIComponent(werte[k]));
|
|
}
|
|
return w.location.origin + w.location.pathname + '#' + teile.join('&');
|
|
}
|
|
|
|
/** Werte hinter dem `#` lesen (oder null, wenn keine da sind). */
|
|
function ausAdresse() {
|
|
var h = w.location.hash.replace(/^#/, '');
|
|
if (!h) return null;
|
|
var werte = {};
|
|
h.split('&').forEach(function (t) {
|
|
var i = t.indexOf('=');
|
|
if (i > 0) werte[t.slice(0, i)] = decodeURIComponent(t.slice(i + 1));
|
|
});
|
|
return werte;
|
|
}
|
|
|
|
/** QR-Code als SVG-Text (braucht qrcode.js). */
|
|
function qrSvg(text) {
|
|
var qr = w.qrcode(0, 'M');
|
|
qr.addData(text);
|
|
qr.make();
|
|
return qr.createSvgTag({ cellSize: 4, margin: 2, scalable: true });
|
|
}
|
|
|
|
w.BKUebergabe = {
|
|
neu: neu, oeffne: oeffne, senden: senden, holen: holen, loeschen: loeschen,
|
|
adresse: adresse, ausAdresse: ausAdresse, qrSvg: qrSvg,
|
|
LEBENSDAUER_MIN: 15,
|
|
};
|
|
})(window);
|