Remove E2E encryption crypto library

This commit is contained in:
2026-03-11 21:15:17 -07:00
parent eab77d8cd2
commit 7eee3fefe5
-264
View File
@@ -1,264 +0,0 @@
import * as chatApi from '@/api/chat';
const DB_NAME = 'movie-loop-crypto';
const DB_VERSION = 1;
const STORE_NAME = 'keys';
// Cache derived shared keys per friend
const sharedKeyCache = new Map<string, CryptoKey>();
// ─── IndexedDB helpers ─────────────────────────────────────────────
function openDB(): Promise<IDBDatabase> {
return new Promise((resolve, reject) => {
const request = indexedDB.open(DB_NAME, DB_VERSION);
request.onupgradeneeded = () => {
request.result.createObjectStore(STORE_NAME);
};
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async function dbGet(key: string): Promise<Record<string, unknown> | undefined> {
const db = await openDB();
return new Promise((resolve, reject) => {
const tx = db.transaction(STORE_NAME, 'readonly');
const req = tx.objectStore(STORE_NAME).get(key);
req.onsuccess = () => resolve(req.result);
req.onerror = () => reject(req.error);
});
}
async function dbPut(key: string, value: Record<string, unknown>): Promise<void> {
const db = await openDB();
return new Promise((resolve, reject) => {
const tx = db.transaction(STORE_NAME, 'readwrite');
tx.objectStore(STORE_NAME).put(value, key);
tx.oncomplete = () => resolve();
tx.onerror = () => reject(tx.error);
});
}
async function dbDelete(key: string): Promise<void> {
const db = await openDB();
return new Promise((resolve, reject) => {
const tx = db.transaction(STORE_NAME, 'readwrite');
tx.objectStore(STORE_NAME).delete(key);
tx.oncomplete = () => resolve();
tx.onerror = () => reject(tx.error);
});
}
// ─── Key Generation (ECDH P-256 as fallback for X25519 compatibility) ──
export async function generateKeyPair(): Promise<CryptoKeyPair> {
return crypto.subtle.generateKey(
{ name: 'ECDH', namedCurve: 'P-256' },
true,
['deriveKey', 'deriveBits'],
);
}
export async function exportPublicKey(key: CryptoKey): Promise<string> {
const exported = await crypto.subtle.exportKey('raw', key);
return btoa(String.fromCharCode(...new Uint8Array(exported)));
}
export async function importPublicKey(base64: string): Promise<CryptoKey> {
const raw = Uint8Array.from(atob(base64), (c) => c.charCodeAt(0));
return crypto.subtle.importKey(
'raw',
raw,
{ name: 'ECDH', namedCurve: 'P-256' },
true,
[],
);
}
// ─── Private key storage (wrapped with PBKDF2) ────────────────────
async function deriveWrappingKey(password: string, salt: Uint8Array): Promise<CryptoKey> {
const enc = new TextEncoder();
const keyMaterial = await crypto.subtle.importKey(
'raw',
enc.encode(password),
'PBKDF2',
false,
['deriveKey'],
);
return crypto.subtle.deriveKey(
{ name: 'PBKDF2', salt: salt.buffer as ArrayBuffer, iterations: 100_000, hash: 'SHA-256' },
keyMaterial,
{ name: 'AES-GCM', length: 256 },
false,
['wrapKey', 'unwrapKey'],
);
}
export async function storePrivateKey(privateKey: CryptoKey, password: string): Promise<void> {
const salt = crypto.getRandomValues(new Uint8Array(16));
const iv = crypto.getRandomValues(new Uint8Array(12));
const wrappingKey = await deriveWrappingKey(password, salt);
const wrapped = await crypto.subtle.wrapKey('pkcs8', privateKey, wrappingKey, {
name: 'AES-GCM',
iv,
});
await dbPut('privateKey', {
wrapped: new Uint8Array(wrapped),
salt,
iv,
});
}
export async function loadPrivateKey(password: string): Promise<CryptoKey | null> {
const stored = await dbGet('privateKey');
if (!stored) return null;
try {
const salt = stored.salt as Uint8Array;
const iv = stored.iv as Uint8Array;
const wrapped = stored.wrapped as ArrayBuffer;
const wrappingKey = await deriveWrappingKey(password, salt);
return crypto.subtle.unwrapKey(
'pkcs8',
wrapped,
wrappingKey,
{ name: 'AES-GCM', iv: iv.buffer as ArrayBuffer },
{ name: 'ECDH', namedCurve: 'P-256' },
true,
['deriveKey', 'deriveBits'],
);
} catch {
return null;
}
}
export async function hasStoredKey(): Promise<boolean> {
const stored = await dbGet('privateKey');
return !!stored;
}
export async function clearStoredKeys(): Promise<void> {
await dbDelete('privateKey');
sharedKeyCache.clear();
}
// ─── Shared secret derivation ─────────────────────────────────────
async function deriveSharedKey(
privateKey: CryptoKey,
publicKey: CryptoKey,
): Promise<CryptoKey> {
return crypto.subtle.deriveKey(
{ name: 'ECDH', public: publicKey },
privateKey,
{ name: 'AES-GCM', length: 256 },
false,
['encrypt', 'decrypt'],
);
}
// Store the private key in memory (set after login/unlock)
let currentPrivateKey: CryptoKey | null = null;
export function setCurrentPrivateKey(key: CryptoKey | null) {
currentPrivateKey = key;
sharedKeyCache.clear();
}
export function getCurrentPrivateKey(): CryptoKey | null {
return currentPrivateKey;
}
// Public key cache
const publicKeyCache = new Map<string, CryptoKey>();
async function getFriendPublicKey(friendId: string): Promise<CryptoKey> {
const cached = publicKeyCache.get(friendId);
if (cached) return cached;
const { chatPublicKey } = await chatApi.getUserPublicKey(friendId);
if (!chatPublicKey) {
throw new Error('Friend has not set up encryption keys');
}
const key = await importPublicKey(chatPublicKey);
publicKeyCache.set(friendId, key);
return key;
}
async function getSharedKey(friendId: string): Promise<CryptoKey> {
const cached = sharedKeyCache.get(friendId);
if (cached) return cached;
if (!currentPrivateKey) {
throw new Error('Encryption key not loaded');
}
const friendPubKey = await getFriendPublicKey(friendId);
const shared = await deriveSharedKey(currentPrivateKey, friendPubKey);
sharedKeyCache.set(friendId, shared);
return shared;
}
// ─── Encrypt / Decrypt ────────────────────────────────────────────
export async function encryptForFriend(
friendId: string,
plaintext: string,
): Promise<{ ciphertext: string; iv: string }> {
const sharedKey = await getSharedKey(friendId);
const iv = crypto.getRandomValues(new Uint8Array(12));
const enc = new TextEncoder();
const encrypted = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv },
sharedKey,
enc.encode(plaintext),
);
return {
ciphertext: btoa(String.fromCharCode(...new Uint8Array(encrypted))),
iv: btoa(String.fromCharCode(...iv)),
};
}
export async function decryptFromFriend(
friendId: string,
ciphertext: string,
ivBase64: string,
): Promise<string> {
const sharedKey = await getSharedKey(friendId);
const encrypted = Uint8Array.from(atob(ciphertext), (c) => c.charCodeAt(0));
const iv = Uint8Array.from(atob(ivBase64), (c) => c.charCodeAt(0));
const decrypted = await crypto.subtle.decrypt(
{ name: 'AES-GCM', iv },
sharedKey,
encrypted,
);
return new TextDecoder().decode(decrypted);
}
// ─── Setup flow ───────────────────────────────────────────────────
export async function setupEncryption(password: string): Promise<string> {
const keyPair = await generateKeyPair();
await storePrivateKey(keyPair.privateKey, password);
setCurrentPrivateKey(keyPair.privateKey);
const publicKeyBase64 = await exportPublicKey(keyPair.publicKey);
await chatApi.updateMyPublicKey(publicKeyBase64);
return publicKeyBase64;
}
export async function unlockEncryption(password: string): Promise<boolean> {
const privateKey = await loadPrivateKey(password);
if (!privateKey) return false;
setCurrentPrivateKey(privateKey);
return true;
}