// SecureChat E2EE Crypto Layer // Uses Web Crypto API: RSA-OAEP for key exchange, AES-GCM for messages const Crypto = { // Generate RSA-OAEP key pair for E2EE async generateKeyPair() { const keyPair = await window.crypto.subtle.generateKey( { name: "RSA-OAEP", modulusLength: 2048, publicExponent: new Uint8Array([1, 0, 1]), hash: "SHA-256" }, true, ["encrypt", "decrypt"] ); const publicKeyJwk = await window.crypto.subtle.exportKey("jwk", keyPair.publicKey); const privateKeyJwk = await window.crypto.subtle.exportKey("jwk", keyPair.privateKey); return { publicKeyJwk, privateKeyJwk, keyPair }; }, // Export public key as JSON string (stored on server) async exportPublicKey(keyPair) { return JSON.stringify(await window.crypto.subtle.exportKey("jwk", keyPair.publicKey)); }, // Import a public key from JWK JSON string async importPublicKey(jwkString) { const jwk = typeof jwkString === "string" ? JSON.parse(jwkString) : jwkString; return window.crypto.subtle.importKey("jwk", jwk, { name: "RSA-OAEP", hash: "SHA-256" }, false, ["encrypt"]); }, // Import a private key from JWK JSON string async importPrivateKey(jwkString) { const jwk = typeof jwkString === "string" ? JSON.parse(jwkString) : jwkString; return window.crypto.subtle.importKey("jwk", jwk, { name: "RSA-OAEP", hash: "SHA-256" }, false, ["decrypt"]); }, // Encrypt a message with a recipient's RSA public key (returns base64) async encryptForRecipient(message, publicKey) { const encoded = new TextEncoder().encode(message); const encrypted = await window.crypto.subtle.encrypt({ name: "RSA-OAEP" }, publicKey, encoded); return btoa(String.fromCharCode(...new Uint8Array(encrypted))); }, // Decrypt a message with own RSA private key async decryptWithPrivateKey(ciphertextB64, privateKey) { const ciphertext = Uint8Array.from(atob(ciphertextB64), c => c.charCodeAt(0)); const decrypted = await window.crypto.subtle.decrypt({ name: "RSA-OAEP" }, privateKey, ciphertext); return new TextDecoder().decode(decrypted); }, // === Group E2EE: AES-GCM + per-member RSA key wrapping === // Generate a random AES-GCM key for a group message async generateAesKey() { return window.crypto.subtle.generateKey({ name: "AES-GCM", length: 256 }, true, ["encrypt", "decrypt"]); }, // Encrypt message with AES-GCM key, returns {ciphertext: base64, iv: base64} async encryptWithAes(message, aesKey) { const iv = window.crypto.getRandomValues(new Uint8Array(12)); const encoded = new TextEncoder().encode(message); const encrypted = await window.crypto.subtle.encrypt({ name: "AES-GCM", iv }, aesKey, encoded); return { ciphertext: btoa(String.fromCharCode(...new Uint8Array(encrypted))), iv: btoa(String.fromCharCode(...iv)) }; }, // Decrypt message with AES-GCM key async decryptWithAes(ciphertextB64, ivB64, aesKey) { const ciphertext = Uint8Array.from(atob(ciphertextB64), c => c.charCodeAt(0)); const iv = Uint8Array.from(atob(ivB64), c => c.charCodeAt(0)); const decrypted = await window.crypto.subtle.decrypt({ name: "AES-GCM", iv }, aesKey, ciphertext); return new TextDecoder().decode(decrypted); }, // Wrap (encrypt) AES key with RSA public key for a member async wrapAesKey(aesKey, recipientPublicKey) { const exportedAes = await window.crypto.subtle.exportKey("raw", aesKey); const wrapped = await window.crypto.subtle.encrypt({ name: "RSA-OAEP" }, recipientPublicKey, exportedAes); return btoa(String.fromCharCode(...new Uint8Array(wrapped))); }, // Unwrap (decrypt) AES key with own RSA private key async unwrapAesKey(wrappedB64, privateKey) { const wrapped = Uint8Array.from(atob(wrappedB64), c => c.charCodeAt(0)); const rawAes = await window.crypto.subtle.decrypt({ name: "RSA-OAEP" }, privateKey, wrapped); return window.crypto.subtle.importKey("raw", rawAes, { name: "AES-GCM", length: 256 }, false, ["decrypt"]); }, // Store private key in localStorage (encrypted with a password-derived key would be ideal; for now, store JWK) storePrivateKey(privateKeyJwk) { localStorage.setItem("sc_private_key", JSON.stringify(privateKeyJwk)); }, loadPrivateKey() { const stored = localStorage.getItem("sc_private_key"); return stored ? JSON.parse(stored) : null; }, clearPrivateKey() { localStorage.removeItem("sc_private_key"); } };