Dateien nach "frontend/src" hochladen
This commit is contained in:
216
frontend/src/crypto.js
Normal file
216
frontend/src/crypto.js
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user