Spaces:
Sleeping
Sleeping
| // 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"); | |
| } | |
| }; |