const STORAGE_PREFIX = 'lendry-e2e-private:'; const ALGORITHM = { name: 'ECDH', namedCurve: 'P-256' } as const; const AES = { name: 'AES-GCM', length: 256 } as const; interface StoredKeyMaterial { privateJwk: JsonWebKey; publicKey: string; } function toBase64(buffer: ArrayBuffer) { const bytes = new Uint8Array(buffer); let binary = ''; bytes.forEach((byte) => { binary += String.fromCharCode(byte); }); return btoa(binary); } function fromBase64(value: string) { const binary = atob(value); const bytes = new Uint8Array(binary.length); for (let index = 0; index < binary.length; index += 1) { bytes[index] = binary.charCodeAt(index); } return bytes.buffer; } async function exportPublicKey(key: CryptoKey) { const raw = await crypto.subtle.exportKey('spki', key); return toBase64(raw); } async function importPublicKey(base64: string) { return crypto.subtle.importKey('spki', fromBase64(base64), ALGORITHM, true, []); } async function loadStoredKeys(userId: string) { const stored = localStorage.getItem(`${STORAGE_PREFIX}${userId}`); if (!stored) return null; const parsed = JSON.parse(stored) as StoredKeyMaterial; const privateKey = await crypto.subtle.importKey('jwk', parsed.privateJwk, ALGORITHM, true, ['deriveKey']); return { privateKey, publicKey: parsed.publicKey }; } async function saveStoredKeys(userId: string, privateKey: CryptoKey, publicKey: string) { const privateJwk = await crypto.subtle.exportKey('jwk', privateKey); localStorage.setItem( `${STORAGE_PREFIX}${userId}`, JSON.stringify({ privateJwk, publicKey } satisfies StoredKeyMaterial) ); } export async function ensureE2EKeyPair(userId: string) { const existing = await loadStoredKeys(userId); if (existing) return existing; const keyPair = await crypto.subtle.generateKey(ALGORITHM, true, ['deriveKey']); const publicKey = await exportPublicKey(keyPair.publicKey); await saveStoredKeys(userId, keyPair.privateKey, publicKey); return { privateKey: keyPair.privateKey, publicKey }; } async function deriveAesKey(privateKey: CryptoKey, peerPublicKeyBase64: string) { const peerPublicKey = await importPublicKey(peerPublicKeyBase64); return crypto.subtle.deriveKey({ name: 'ECDH', public: peerPublicKey }, privateKey, AES, false, ['encrypt', 'decrypt']); } export async function encryptDirectMessage(userId: string, peerPublicKeyBase64: string, plaintext: string) { const keys = await ensureE2EKeyPair(userId); const aesKey = await deriveAesKey(keys.privateKey, peerPublicKeyBase64); const iv = crypto.getRandomValues(new Uint8Array(12)); const encoded = new TextEncoder().encode(plaintext); const ciphertext = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, aesKey, encoded); return JSON.stringify({ v: 1, iv: toBase64(iv.buffer), ciphertext: toBase64(ciphertext) }); } export async function decryptDirectMessage(userId: string, peerPublicKeyBase64: string, payload: string) { const keys = await loadStoredKeys(userId); if (!keys) { return '🔒 Не удалось расшифровать сообщение'; } try { const parsed = JSON.parse(payload) as { iv?: string; ciphertext?: string }; if (!parsed.iv || !parsed.ciphertext) { return payload; } const aesKey = await deriveAesKey(keys.privateKey, peerPublicKeyBase64); const iv = new Uint8Array(fromBase64(parsed.iv)); const decrypted = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, aesKey, fromBase64(parsed.ciphertext)); return new TextDecoder().decode(decrypted); } catch { return '🔒 Не удалось расшифровать сообщение'; } } export async function encryptBinary(userId: string, peerPublicKeyBase64: string, data: ArrayBuffer) { const keys = await ensureE2EKeyPair(userId); const aesKey = await deriveAesKey(keys.privateKey, peerPublicKeyBase64); const iv = crypto.getRandomValues(new Uint8Array(12)); const ciphertext = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, aesKey, data); const packed = new Uint8Array(iv.byteLength + ciphertext.byteLength); packed.set(iv, 0); packed.set(new Uint8Array(ciphertext), iv.byteLength); return packed.buffer; } export async function decryptBinary(userId: string, peerPublicKeyBase64: string, packed: ArrayBuffer) { const keys = await loadStoredKeys(userId); if (!keys) { throw new Error('Локальный E2E-ключ не найден'); } const bytes = new Uint8Array(packed); const iv = bytes.slice(0, 12); const ciphertext = bytes.slice(12); const aesKey = await deriveAesKey(keys.privateKey, peerPublicKeyBase64); return crypto.subtle.decrypt({ name: 'AES-GCM', iv }, aesKey, ciphertext); }