Dateien nach "frontend/src" hochladen

This commit is contained in:
2026-05-28 00:22:44 +02:00
parent 71c9061e33
commit 17917e2997
2 changed files with 431 additions and 4 deletions

216
frontend/src/crypto.js Normal file
View File

@@ -0,0 +1,216 @@
/**
* 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);
}