/** * extension/shared/crypto.js — Zero-knowledge cryptography for the extension. * * Mirrors the web app's crypto.js but: * - Uses `crypto.subtle` (no `window.`) so it works in both popup and service worker. * - deriveVaultKey uses extractable:true so the key can be serialised to * chrome.storage.session (exportVaultKey / importVaultKey). */ const ExtCrypto = (() => { const subtle = crypto.subtle; // ── Helpers ───────────────────────────────────────────────────────────────── function strToBytes(str) { return new TextEncoder().encode(str); } function base64ToBytes(b64) { const bin = atob(b64); const out = new Uint8Array(bin.length); for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i); return out; } function bytesToBase64(bytes) { let bin = ''; bytes.forEach(b => (bin += String.fromCharCode(b))); return btoa(bin); } // ── PBKDF2 base key ───────────────────────────────────────────────────────── async function importPbkdf2Key(password) { return subtle.importKey('raw', strToBytes(password), 'PBKDF2', false, ['deriveBits', 'deriveKey']); } // ── Auth hash (sent to server for login/register) ─────────────────────────── async function deriveAuthHash(password, email) { const baseKey = await importPbkdf2Key(password); const bits = await subtle.deriveBits( { name: 'PBKDF2', salt: strToBytes(email.toLowerCase()), iterations: 100_000, hash: 'SHA-256' }, baseKey, 256 ); return bytesToBase64(new Uint8Array(bits)); } // ── Vault key (AES-256-GCM, extractable for session storage) ──────────────── async function deriveVaultKey(password, enc_key_salt) { const baseKey = await importPbkdf2Key(password); return subtle.deriveKey( { name: 'PBKDF2', salt: base64ToBytes(enc_key_salt), iterations: 600_000, hash: 'SHA-256' }, baseKey, { name: 'AES-GCM', length: 256 }, true, // extractable — needed to serialise into chrome.storage.session ['encrypt', 'decrypt'] ); } /** Serialise a CryptoKey to a JSON string for chrome.storage.session. */ async function exportVaultKey(vaultKey) { const jwk = await subtle.exportKey('jwk', vaultKey); return JSON.stringify(jwk); } /** Deserialise a CryptoKey from chrome.storage.session. */ async function importVaultKey(jwkStr) { const jwk = JSON.parse(jwkStr); return subtle.importKey('jwk', jwk, { name: 'AES-GCM', length: 256 }, true, ['encrypt', 'decrypt']); } // ── Encrypt / Decrypt ──────────────────────────────────────────────────────── async function encryptItem(vaultKey, plainObj) { const iv = crypto.getRandomValues(new Uint8Array(12)); const ct = await subtle.encrypt( { name: 'AES-GCM', iv }, vaultKey, strToBytes(JSON.stringify(plainObj)) ); return { enc_data: bytesToBase64(new Uint8Array(ct)), iv: bytesToBase64(iv) }; } async function decryptItem(vaultKey, enc_data, iv) { const pt = await subtle.decrypt( { name: 'AES-GCM', iv: base64ToBytes(iv) }, vaultKey, base64ToBytes(enc_data) ); return JSON.parse(new TextDecoder().decode(pt)); } function generateSalt(byteLength = 16) { return bytesToBase64(crypto.getRandomValues(new Uint8Array(byteLength))); } return { deriveAuthHash, deriveVaultKey, exportVaultKey, importVaultKey, encryptItem, decryptItem, generateSalt }; })();