let vault = null; let isInitialized = false; let currentSession = null; let keepAliveInterval = null; let sessionTimeoutDuration = 15; // Default 15 seconds let sessionTimeoutId = null; // Background timer let popupPort = null; // Track popup connection // Utility function to convert Uint8Array to hex function toHex(uint8Array) { return Array.from(uint8Array) .map(b => b.toString(16).padStart(2, '0')) .join(''); } // Background session timeout management async function loadTimeoutSetting() { const result = await chrome.storage.local.get(['sessionTimeout']); sessionTimeoutDuration = result.sessionTimeout || 15; } function startSessionTimeout() { clearSessionTimeout(); if (currentSession && sessionTimeoutDuration > 0) { sessionTimeoutId = setTimeout(async () => { if (vault && currentSession) { // Lock the session vault.lock_session(); await sessionManager.clear(); // Notify popup if it's open if (popupPort) { popupPort.postMessage({ type: 'sessionTimeout', message: 'Session timed out due to inactivity' }); } } }, sessionTimeoutDuration * 1000); } } function clearSessionTimeout() { if (sessionTimeoutId) { clearTimeout(sessionTimeoutId); sessionTimeoutId = null; } } function resetSessionTimeout() { if (currentSession) { startSessionTimeout(); } } // Session persistence functions async function saveSession(keyspace) { currentSession = { keyspace, timestamp: Date.now() }; // Save to both session and local storage for better persistence await chrome.storage.session.set({ cryptoVaultSession: currentSession }); await chrome.storage.local.set({ cryptoVaultSessionBackup: currentSession }); } async function loadSession() { // Try session storage first let result = await chrome.storage.session.get(['cryptoVaultSession']); if (result.cryptoVaultSession) { currentSession = result.cryptoVaultSession; return currentSession; } // Fallback to local storage result = await chrome.storage.local.get(['cryptoVaultSessionBackup']); if (result.cryptoVaultSessionBackup) { currentSession = result.cryptoVaultSessionBackup; // Restore to session storage await chrome.storage.session.set({ cryptoVaultSession: currentSession }); return currentSession; } return null; } async function clearSession() { currentSession = null; await chrome.storage.session.remove(['cryptoVaultSession']); await chrome.storage.local.remove(['cryptoVaultSessionBackup']); } // Keep service worker alive function startKeepAlive() { if (keepAliveInterval) { clearInterval(keepAliveInterval); } keepAliveInterval = setInterval(() => { chrome.storage.session.get(['keepAlive']).catch(() => {}); }, 20000); } function stopKeepAlive() { if (keepAliveInterval) { clearInterval(keepAliveInterval); keepAliveInterval = null; } } // Consolidated session management const sessionManager = { async save(keyspace) { await saveSession(keyspace); startKeepAlive(); await loadTimeoutSetting(); startSessionTimeout(); }, async clear() { await clearSession(); stopKeepAlive(); clearSessionTimeout(); } }; async function restoreSession() { const session = await loadSession(); if (session && vault) { // Check if the session is still valid by testing if vault is unlocked const isUnlocked = vault.is_unlocked(); if (isUnlocked) { // Restart keep-alive for restored session startKeepAlive(); return session; } else { await sessionManager.clear(); } } return null; } // Import WASM module functions import init, * as wasmFunctions from './wasm/wasm_app.js'; // Initialize WASM module async function initVault() { try { if (vault && isInitialized) return vault; // Initialize with the WASM file const wasmUrl = chrome.runtime.getURL('wasm/wasm_app_bg.wasm'); await init(wasmUrl); // Use imported functions directly vault = wasmFunctions; isInitialized = true; // Try to restore previous session await restoreSession(); return vault; } catch (error) { console.error('Failed to initialize CryptoVault:', error); throw error; } } // Consolidated message handlers const messageHandlers = { createKeyspace: async (request) => { await vault.create_keyspace(request.keyspace, request.password); return { success: true }; }, initSession: async (request) => { await vault.init_session(request.keyspace, request.password); await sessionManager.save(request.keyspace); return { success: true }; }, isUnlocked: () => ({ success: true, unlocked: vault.is_unlocked() }), addKeypair: async (request) => { const result = await vault.add_keypair(request.keyType, request.metadata); return { success: true, result }; }, listKeypairs: async () => { if (!vault.is_unlocked()) { return { success: false, error: 'Session is not unlocked' }; } const keypairsRaw = await vault.list_keypairs(); const keypairs = typeof keypairsRaw === 'string' ? JSON.parse(keypairsRaw) : keypairsRaw; return { success: true, keypairs }; }, selectKeypair: (request) => { vault.select_keypair(request.keyId); return { success: true }; }, getCurrentKeypairMetadata: () => ({ success: true, metadata: vault.current_keypair_metadata() }), getCurrentKeypairPublicKey: () => ({ success: true, publicKey: toHex(vault.current_keypair_public_key()) }), sign: async (request) => { const signature = await vault.sign(new Uint8Array(request.message)); return { success: true, signature }; }, encrypt: async (request) => { if (!vault.is_unlocked()) { return { success: false, error: 'Session is not unlocked' }; } const messageBytes = new TextEncoder().encode(request.message); const encryptedData = await vault.encrypt_data(messageBytes); const encryptedMessage = btoa(String.fromCharCode(...new Uint8Array(encryptedData))); return { success: true, encryptedMessage }; }, decrypt: async (request) => { if (!vault.is_unlocked()) { return { success: false, error: 'Session is not unlocked' }; } const encryptedBytes = new Uint8Array(atob(request.encryptedMessage).split('').map(c => c.charCodeAt(0))); const decryptedData = await vault.decrypt_data(encryptedBytes); const decryptedMessage = new TextDecoder().decode(new Uint8Array(decryptedData)); return { success: true, decryptedMessage }; }, verify: async (request) => { const metadata = vault.current_keypair_metadata(); if (!metadata) { return { success: false, error: 'No keypair selected' }; } const isValid = await vault.verify(new Uint8Array(request.message), request.signature); return { success: true, isValid }; }, lockSession: async () => { vault.lock_session(); await sessionManager.clear(); return { success: true }; }, getStatus: async () => { const status = vault ? vault.is_unlocked() : false; const session = await loadSession(); return { success: true, status, session: session ? { keyspace: session.keyspace } : null }; }, // Timeout management handlers resetTimeout: async () => { resetSessionTimeout(); return { success: true }; }, updateTimeout: async (request) => { sessionTimeoutDuration = request.timeout; await chrome.storage.local.set({ sessionTimeout: request.timeout }); resetSessionTimeout(); // Restart with new duration return { success: true }; } }; // Handle messages from popup and content scripts chrome.runtime.onMessage.addListener((request, _sender, sendResponse) => { const handleRequest = async () => { try { if (!vault) { await initVault(); } const handler = messageHandlers[request.action]; if (handler) { return await handler(request); } else { throw new Error('Unknown action: ' + request.action); } } catch (error) { return { success: false, error: error.message }; } }; handleRequest().then(sendResponse); return true; // Keep the message channel open for async response }); // Initialize vault when extension starts chrome.runtime.onStartup.addListener(() => { initVault(); }); chrome.runtime.onInstalled.addListener(() => { initVault(); }); // Handle popup connection for keep-alive and timeout notifications chrome.runtime.onConnect.addListener((port) => { if (port.name === 'popup') { // Track popup connection popupPort = port; // If we have an active session, ensure keep-alive is running if (currentSession) { startKeepAlive(); } port.onDisconnect.addListener(() => { // Popup closed, clear reference and stop keep-alive popupPort = null; stopKeepAlive(); }); } });