Remove E2E encryption crypto library
This commit is contained in:
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user