/** * DickenDock E2E-Verschlüsselung * * Algorithmen: * X25519 → Schlüsselaustausch (WebCrypto; Chrome 113+/Firefox 130+/Safari 17.4+) * AES-256-GCM → Nachrichtenverschlüsselung * PBKDF2-SHA256 → Export-Passwortverschlüsselung (200k Iterationen) * ↳ statt Argon2id: kein WebCrypto-Support ohne WASM-Lib → keine externen Abhängigkeiten * * Private Key: ausschließlich in IndexedDB, niemals an den Server übertragen. */ const IDB_NAME = 'dd_e2e_v1'; const IDB_STORE = 'keys'; const KEY_ID = 'identity'; // ── IndexedDB helpers ───────────────────────────────────────────────────────── function _openDB() { return new Promise((res, rej) => { const r = indexedDB.open(IDB_NAME, 1); r.onupgradeneeded = e => e.target.result.createObjectStore(IDB_STORE); r.onsuccess = e => res(e.target.result); r.onerror = () => rej(r.error); }); } async function _idbGet(k) { const db = await _openDB(); return new Promise((res, rej) => { const r = db.transaction(IDB_STORE, 'readonly').objectStore(IDB_STORE).get(k); r.onsuccess = () => res(r.result ?? null); r.onerror = () => rej(r.error); }); } async function _idbSet(k, v) { const db = await _openDB(); return new Promise((res, rej) => { const tx = db.transaction(IDB_STORE, 'readwrite'); tx.objectStore(IDB_STORE).put(v, k); tx.oncomplete = () => res(); tx.onerror = () => rej(tx.error); }); } async function _idbDel(k) { const db = await _openDB(); return new Promise((res, rej) => { const tx = db.transaction(IDB_STORE, 'readwrite'); tx.objectStore(IDB_STORE).delete(k); tx.oncomplete = () => res(); tx.onerror = () => rej(tx.error); }); } // ── Base64 helpers ──────────────────────────────────────────────────────────── const b64 = u8 => btoa(String.fromCharCode(...u8)); const ub64 = s => Uint8Array.from(atob(s), c => c.charCodeAt(0)); // ── Internal ────────────────────────────────────────────────────────────────── async function _importJwkPair(pub, priv) { const publicKey = await crypto.subtle.importKey('jwk', pub, { name:'X25519' }, true, []); const privateKey = await crypto.subtle.importKey('jwk', priv, { name:'X25519' }, true, ['deriveKey']); return { publicKey, privateKey }; } async function _pbkdf2Key(password, salt) { const raw = await crypto.subtle.importKey( 'raw', new TextEncoder().encode(password), 'PBKDF2', false, ['deriveKey'] ); return crypto.subtle.deriveKey( { name:'PBKDF2', salt, iterations:200_000, hash:'SHA-256' }, raw, { name:'AES-GCM', length:256 }, false, ['encrypt','decrypt'] ); } // ── Keypair Management ──────────────────────────────────────────────────────── /** * Lädt Keypair aus IndexedDB. * Migriert altes localStorage-Format (P-256) → löscht es, erzeugt neues X25519-Keypair. * Erstellt neues Keypair falls keins vorhanden. */ export async function getOrCreateKeyPair() { // Migration: altes P-256-Keypair aus localStorage entfernen if (localStorage.getItem('dd_msg_keypair')) { localStorage.removeItem('dd_msg_keypair'); // Kein Import möglich (Algorithmuswechsel P-256→X25519), neues Keypair wird unten erzeugt } const stored = await _idbGet(KEY_ID); if (stored) return stored; // Neues X25519-Keypair erzeugen const kp = await crypto.subtle.generateKey({ name:'X25519' }, true, ['deriveKey']); await _idbSet(KEY_ID, kp); return kp; } /** Neues Keypair erzeugen und in IndexedDB speichern (überschreibt vorhandenes). */ export async function generateNewKeyPair() { const kp = await crypto.subtle.generateKey({ name:'X25519' }, true, ['deriveKey']); await _idbSet(KEY_ID, kp); return kp; } /** Keypair aus IndexedDB löschen. */ export async function clearStoredKeyPair() { await _idbDel(KEY_ID); } /** Public Key als JWK exportieren (für Server-Registrierung). */ export async function getPublicKeyJwk(kp) { return crypto.subtle.exportKey('jwk', kp.publicKey); } // ── Fingerprint ─────────────────────────────────────────────────────────────── /** * SHA-256-Hash des Public Key, angezeigt als Kurzformat: ABCD-EF12-3456-CDEF * Kann manuell verglichen werden um Man-in-the-Middle zu erkennen. */ export async function getFingerprint(kp) { const jwk = await getPublicKeyJwk(kp); const enc = new TextEncoder().encode(JSON.stringify(jwk)); const hash = await crypto.subtle.digest('SHA-256', enc); const hex = [...new Uint8Array(hash)] .map(b => b.toString(16).padStart(2, '0')).join('').toUpperCase(); return `${hex.slice(0,4)}-${hex.slice(4,8)}-${hex.slice(8,12)}-${hex.slice(12,16)}`; } // ── Export / Import ─────────────────────────────────────────────────────────── /** * Keypair verschlüsselt als JSON-String exportieren. * Verschlüsselung: PBKDF2-SHA256 (200k) → AES-256-GCM */ export async function exportEncrypted(kp, password) { const pub = await crypto.subtle.exportKey('jwk', kp.publicKey); const priv = await crypto.subtle.exportKey('jwk', kp.privateKey); const salt = crypto.getRandomValues(new Uint8Array(16)); const iv = crypto.getRandomValues(new Uint8Array(12)); const enc = await crypto.subtle.encrypt( { name:'AES-GCM', iv }, await _pbkdf2Key(password, salt), new TextEncoder().encode(JSON.stringify({ pub, priv })) ); return JSON.stringify({ v: 1, alg: 'X25519+PBKDF2-SHA256-200k+AES-256-GCM', salt: b64(salt), iv: b64(iv), data: b64(new Uint8Array(enc)), }); } /** * Verschlüsseltes Keypair importieren, entschlüsseln und in IndexedDB speichern. * Wirft bei falschem Passwort oder korrupter Datei einen Fehler. */ export async function importEncrypted(jsonStr, password) { const obj = JSON.parse(jsonStr); if (obj.v !== 1) throw new Error('Unbekanntes Export-Format'); let dec; try { dec = await crypto.subtle.decrypt( { name:'AES-GCM', iv: ub64(obj.iv) }, await _pbkdf2Key(password, ub64(obj.salt)), ub64(obj.data) ); } catch { throw new Error('Entschlüsselung fehlgeschlagen – falsches Passwort?'); } const { pub, priv } = JSON.parse(new TextDecoder().decode(dec)); const kp = await _importJwkPair(pub, priv); await _idbSet(KEY_ID, kp); return kp; } // ── Message Encryption ──────────────────────────────────────────────────────── /** * Gemeinsamen AES-256-GCM-Schlüssel aus eigenem Private Key + Public Key des Partners ableiten. * Beide Seiten erhalten denselben Schlüssel (X25519-Eigenschaft). */ export async function deriveSharedKey(myPrivateKey, theirPubJwk) { const theirKey = await crypto.subtle.importKey('jwk', theirPubJwk, { name:'X25519' }, false, []); return crypto.subtle.deriveKey( { name:'X25519', public: theirKey }, myPrivateKey, { name:'AES-GCM', length:256 }, false, ['encrypt','decrypt'] ); } /** Nachricht mit AES-256-GCM verschlüsseln. */ export async function encryptMsg(sharedKey, text) { const iv = crypto.getRandomValues(new Uint8Array(12)); const enc = await crypto.subtle.encrypt( { name:'AES-GCM', iv }, sharedKey, new TextEncoder().encode(text) ); return { encrypted_content: b64(new Uint8Array(enc)), iv: b64(iv), }; } /** Nachricht entschlüsseln. Wirft bei falschem Schlüssel/korrupten Daten. */ export async function decryptMsg(sharedKey, ecB64, ivB64) { const dec = await crypto.subtle.decrypt( { name:'AES-GCM', iv: ub64(ivB64) }, sharedKey, ub64(ecB64) ); return new TextDecoder().decode(dec); }