/** * Browser extension-friendly WebAssembly loader and helper functions * This handles loading the WebAssembly module without relying on ES modules */ // Global reference to the loaded WebAssembly module let wasmModule = null; // Initialization state const state = { loading: false, initialized: false, error: null }; /** * Load the WebAssembly module * @returns {Promise} */ export async function loadWasmModule() { if (state.initialized || state.loading) { return; } state.loading = true; try { // Get paths to WebAssembly files const wasmJsPath = chrome.runtime.getURL('wasm/wasm_app.js'); const wasmBinaryPath = chrome.runtime.getURL('wasm/wasm_app_bg.wasm'); console.log('Loading WASM JS from:', wasmJsPath); console.log('Loading WASM binary from:', wasmBinaryPath); // Create a container for our temporary WebAssembly globals window.__wasmApp = {}; // Create a script element to load the JS file const script = document.createElement('script'); script.src = wasmJsPath; // Wait for the script to load await new Promise((resolve, reject) => { script.onload = resolve; script.onerror = () => reject(new Error('Failed to load WASM JavaScript file')); document.head.appendChild(script); }); // Check if the wasm_app global was created if (!window.wasm_app && !window.__wbg_init) { throw new Error('WASM module did not export expected functions'); } // Get the initialization function const init = window.__wbg_init || (window.wasm_app && window.wasm_app.default); if (!init || typeof init !== 'function') { throw new Error('WASM init function not found'); } // Fetch the WASM binary file const response = await fetch(wasmBinaryPath); if (!response.ok) { throw new Error(`Failed to fetch WASM binary: ${response.status} ${response.statusText}`); } // Get the binary data const wasmBinary = await response.arrayBuffer(); // Initialize the WASM module await init(wasmBinary); // Debug logging for available functions in the WebAssembly module console.log('Available WebAssembly functions:'); console.log('init_rhai_env:', typeof window.init_rhai_env, typeof (window.wasm_app && window.wasm_app.init_rhai_env)); console.log('init_session:', typeof window.init_session, typeof (window.wasm_app && window.wasm_app.init_session)); console.log('lock_session:', typeof window.lock_session, typeof (window.wasm_app && window.wasm_app.lock_session)); console.log('add_keypair:', typeof window.add_keypair, typeof (window.wasm_app && window.wasm_app.add_keypair)); console.log('select_keypair:', typeof window.select_keypair, typeof (window.wasm_app && window.wasm_app.select_keypair)); console.log('sign:', typeof window.sign, typeof (window.wasm_app && window.wasm_app.sign)); console.log('run_rhai:', typeof window.run_rhai, typeof (window.wasm_app && window.wasm_app.run_rhai)); console.log('list_keypairs:', typeof window.list_keypairs, typeof (window.wasm_app && window.wasm_app.list_keypairs)); // Store reference to all the exported functions wasmModule = { init_rhai_env: window.init_rhai_env || (window.wasm_app && window.wasm_app.init_rhai_env), init_session: window.init_session || (window.wasm_app && window.wasm_app.init_session), lock_session: window.lock_session || (window.wasm_app && window.wasm_app.lock_session), add_keypair: window.add_keypair || (window.wasm_app && window.wasm_app.add_keypair), select_keypair: window.select_keypair || (window.wasm_app && window.wasm_app.select_keypair), sign: window.sign || (window.wasm_app && window.wasm_app.sign), run_rhai: window.run_rhai || (window.wasm_app && window.wasm_app.run_rhai), list_keypairs: window.list_keypairs || (window.wasm_app && window.wasm_app.list_keypairs), list_keypairs_debug: window.list_keypairs_debug || (window.wasm_app && window.wasm_app.list_keypairs_debug), check_indexeddb: window.check_indexeddb || (window.wasm_app && window.wasm_app.check_indexeddb) }; // Log what was actually registered console.log('Registered WebAssembly module functions:'); for (const [key, value] of Object.entries(wasmModule)) { console.log(`${key}: ${typeof value}`, value ? 'Available' : 'Missing'); } // Initialize the WASM environment if (typeof wasmModule.init_rhai_env === 'function') { wasmModule.init_rhai_env(); } state.initialized = true; console.log('WASM module loaded and initialized successfully'); } catch (error) { console.error('Failed to load WASM module:', error); state.error = error.message || 'Unknown error loading WebAssembly module'; } finally { state.loading = false; } } /** * Get the current state of the WebAssembly module * @returns {{loading: boolean, initialized: boolean, error: string|null}} */ export function getWasmState() { return { ...state }; } /** * Get the WebAssembly module * @returns {object|null} The WebAssembly module or null if not loaded */ export function getWasmModule() { return wasmModule; } /** * Debug function to check the vault state * @returns {Promise} State information */ export async function debugVaultState() { const module = getWasmModule(); if (!module) { throw new Error('WebAssembly module not loaded'); } try { console.log('🔍 Debugging vault state...'); // Check if we have a valid session using Rhai script const sessionCheck = ` let has_session = vault::has_active_session(); let keyspace = ""; if has_session { keyspace = vault::get_current_keyspace(); } // Return info about the session { "has_session": has_session, "keyspace": keyspace } `; console.log('Checking session status...'); const sessionStatus = await module.run_rhai(sessionCheck); console.log('Session status:', sessionStatus); // Get keypair info if we have a session if (sessionStatus && sessionStatus.has_session) { const keypairsScript = ` // Get all keypairs for the current keyspace let keypairs = vault::list_keypairs(); // Add diagnostic information let diagnostic = { "keypair_count": keypairs.len(), "keyspace": vault::get_current_keyspace(), "keypairs": keypairs }; diagnostic `; console.log('Fetching keypair details...'); const keypairDiagnostic = await module.run_rhai(keypairsScript); console.log('Keypair diagnostic:', keypairDiagnostic); return keypairDiagnostic; } return sessionStatus; } catch (error) { console.error('Error in debug function:', error); return { error: error.toString() }; } } /** * Get keypairs from the vault * @returns {Promise} List of keypairs */ export async function getKeypairsFromVault() { console.log('==============================================='); console.log('Starting getKeypairsFromVault...'); const module = getWasmModule(); if (!module) { console.error('WebAssembly module not loaded!'); throw new Error('WebAssembly module not loaded'); } console.log('WebAssembly module:', module); console.log('Module functions available:', Object.keys(module)); // Check if IndexedDB is available and working const isIndexedDBAvailable = await checkIndexedDBAvailability(); if (!isIndexedDBAvailable) { console.warn('IndexedDB is not available or not working properly'); // We'll continue, but this is likely why keypairs aren't persisting } // Force re-initialization of the current session if needed try { // This checks if we have the debug function available if (typeof module.list_keypairs_debug === 'function') { console.log('Using debug function to diagnose keypair loading issues...'); const debugResult = await module.list_keypairs_debug(); console.log('Debug keypair listing result:', debugResult); if (Array.isArray(debugResult) && debugResult.length > 0) { console.log('Debug function returned keypairs:', debugResult); // If debug function worked but regular function doesn't, use its result return debugResult; } else { console.log('Debug function did not return keypairs, continuing with normal flow...'); } } } catch (err) { console.error('Error in debug function:', err); // Continue with normal flow even if the debug function fails } try { console.log('-----------------------------------------------'); console.log('Running diagnostics to check vault state...'); // Run diagnostic first to log vault state await debugVaultState(); console.log('Diagnostics complete'); console.log('-----------------------------------------------'); console.log('Checking if list_keypairs function is available:', typeof module.list_keypairs); for (const key in module) { console.log(`Module function: ${key} = ${typeof module[key]}`); } if (typeof module.list_keypairs !== 'function') { console.error('list_keypairs function is not available in the WebAssembly module!'); console.log('Available functions:', Object.keys(module)); // Fall back to Rhai script console.log('Falling back to using Rhai script for listing keypairs...'); const script = ` // Get all keypairs from the current keyspace let keypairs = vault::list_keypairs(); keypairs `; const keypairList = await module.run_rhai(script); console.log('Retrieved keypairs from vault using Rhai:', keypairList); return keypairList; } console.log('Calling WebAssembly list_keypairs function...'); // Use the direct list_keypairs function from WebAssembly instead of Rhai script const keypairList = await module.list_keypairs(); console.log('Retrieved keypairs from vault:', keypairList); console.log('Raw keypair list type:', typeof keypairList); console.log('Is array?', Array.isArray(keypairList)); console.log('Raw keypair list:', keypairList); // Format keypairs for UI const formattedKeypairs = Array.isArray(keypairList) ? keypairList.map(kp => { // Parse metadata if available let metadata = {}; if (kp.metadata) { try { if (typeof kp.metadata === 'string') { metadata = JSON.parse(kp.metadata); } else { metadata = kp.metadata; } } catch (e) { console.warn('Failed to parse keypair metadata:', e); } } return { id: kp.id, label: metadata.label || `Key-${kp.id.substring(0, 4)}` }; }) : []; console.log('Formatted keypairs for UI:', formattedKeypairs); // Update background service worker return new Promise((resolve) => { chrome.runtime.sendMessage({ action: 'update_session', type: 'keypairs_loaded', data: formattedKeypairs }, (response) => { console.log('Background response to keypairs update:', response); resolve(formattedKeypairs); }); }); } catch (error) { console.error('Error fetching keypairs from vault:', error); return []; } } /** * Check if IndexedDB is available and working * @returns {Promise} True if IndexedDB is working */ export async function checkIndexedDBAvailability() { console.log('Checking IndexedDB availability...'); // First check if IndexedDB is available in the browser if (!window.indexedDB) { console.error('IndexedDB is not available in this browser'); return false; } const module = getWasmModule(); if (!module || typeof module.check_indexeddb !== 'function') { console.error('WebAssembly module or check_indexeddb function not available'); return false; } try { const result = await module.check_indexeddb(); console.log('IndexedDB check result:', result); return true; } catch (error) { console.error('IndexedDB check failed:', error); return false; } } /** * Initialize a session with the given keyspace and password * @param {string} keyspace * @param {string} password * @returns {Promise} List of keypairs after initialization */ export async function initSession(keyspace, password) { const module = getWasmModule(); if (!module) { throw new Error('WebAssembly module not loaded'); } try { console.log(`Initializing session for keyspace: ${keyspace}`); // Check if IndexedDB is working const isIndexedDBAvailable = await checkIndexedDBAvailability(); if (!isIndexedDBAvailable) { console.warn('IndexedDB is not available or not working properly. Keypairs might not persist.'); // Continue anyway as we might fall back to memory storage } // Initialize the session using the WASM module await module.init_session(keyspace, password); console.log('Session initialized successfully'); // Check if we have stored keypairs for this keyspace in Chrome storage const storedKeypairs = await new Promise(resolve => { chrome.storage.local.get([`keypairs:${keyspace}`], result => { resolve(result[`keypairs:${keyspace}`] || []); }); }); console.log(`Found ${storedKeypairs.length} stored keypairs for keyspace ${keyspace}`); // Import stored keypairs into the WebAssembly session if they don't exist already if (storedKeypairs.length > 0) { console.log('Importing stored keypairs into WebAssembly session...'); // First get current keypairs from the vault directly const wasmKeypairs = await module.list_keypairs(); console.log('Current keypairs in WebAssembly vault:', wasmKeypairs); // Get the IDs of existing keypairs in the vault const existingIds = new Set(wasmKeypairs.map(kp => kp.id)); // Import keypairs that don't already exist in the vault for (const keypair of storedKeypairs) { if (!existingIds.has(keypair.id)) { console.log(`Importing keypair ${keypair.id} into WebAssembly vault...`); // Create metadata for the keypair const metadata = JSON.stringify({ label: keypair.label || `Key-${keypair.id.substring(0, 8)}`, imported: true, importDate: new Date().toISOString() }); // For adding existing keypairs, we'd normally need the private key // Since we can't retrieve it, we'll create a new one with the same label // This is a placeholder - in a real implementation, you'd need to use the actual keys try { const keyType = keypair.type || 'Secp256k1'; await module.add_keypair(keyType, metadata); console.log(`Created keypair of type ${keyType} with label ${keypair.label}`); } catch (err) { console.warn(`Failed to import keypair ${keypair.id}:`, err); // Continue with other keypairs even if one fails } } else { console.log(`Keypair ${keypair.id} already exists in vault, skipping import`); } } } // Initialize session using WASM (await the async function) await module.init_session(keyspace, password); // Get keypairs from the vault after session is ready const currentKeypairs = await getKeypairsFromVault(); // Update keypairs in background service worker await new Promise(resolve => { chrome.runtime.sendMessage({ action: 'update_session', type: 'keypairs_loaded', data: currentKeypairs }, response => { console.log('Updated keypairs in background service worker'); resolve(); }); }); return currentKeypairs; } catch (error) { console.error('Failed to initialize session:', error); throw error; } } /** * Lock the current session * @returns {Promise} */ export async function lockSession() { const module = getWasmModule(); if (!module) { throw new Error('WebAssembly module not loaded'); } try { console.log('Locking session...'); // First run diagnostics to see what we have before locking await debugVaultState(); // Call the WASM lock_session function module.lock_session(); console.log('Session locked in WebAssembly module'); // Update session state in background await new Promise((resolve, reject) => { chrome.runtime.sendMessage({ action: 'update_session', type: 'session_locked' }, (response) => { if (response && response.success) { console.log('Background service worker updated for locked session'); resolve(); } else { console.error('Failed to update session state in background:', response?.error); reject(new Error(response?.error || 'Failed to update session state')); } }); }); // Verify session is locked properly const sessionStatus = await debugVaultState(); console.log('Session status after locking:', sessionStatus); } catch (error) { console.error('Error locking session:', error); throw error; } } /** * Add a new keypair * @param {string} keyType The type of key to create (default: 'Secp256k1') * @param {string} label Optional custom label for the keypair * @returns {Promise<{id: string, label: string}>} The created keypair info */ export async function addKeypair(keyType = 'Secp256k1', label = null) { const module = getWasmModule(); if (!module) { throw new Error('WebAssembly module not loaded'); } try { // Get current keyspace const sessionState = await getSessionState(); const keyspace = sessionState.currentKeyspace; if (!keyspace) { throw new Error('No active keyspace'); } // Generate default label if not provided const keyLabel = label || `${keyType}-Key-${Date.now().toString(16).slice(-4)}`; // Create metadata JSON const metadata = JSON.stringify({ label: keyLabel, created: new Date().toISOString(), type: keyType }); console.log(`Adding new keypair of type ${keyType} with label ${keyLabel}`); console.log('Keypair metadata:', metadata); // Call the WASM add_keypair function with metadata // This will add the keypair to the WebAssembly vault const keyId = await module.add_keypair(keyType, metadata); console.log(`Keypair created with ID: ${keyId} in WebAssembly vault`); // Create keypair object for UI and storage const newKeypair = { id: keyId, label: keyLabel, type: keyType, created: new Date().toISOString() }; // Get the latest keypairs from the WebAssembly vault to ensure consistency const vaultKeypairs = await module.list_keypairs(); console.log('Current keypairs in vault after addition:', vaultKeypairs); // Format the vault keypairs for storage const formattedVaultKeypairs = vaultKeypairs.map(kp => { // Parse metadata if available let metadata = {}; if (kp.metadata) { try { if (typeof kp.metadata === 'string') { metadata = JSON.parse(kp.metadata); } else { metadata = kp.metadata; } } catch (e) { console.warn('Failed to parse keypair metadata:', e); } } return { id: kp.id, label: metadata.label || `Key-${kp.id.substring(0, 8)}`, type: kp.type || 'Secp256k1', created: metadata.created || new Date().toISOString() }; }); // Save the formatted keypairs to Chrome storage await new Promise(resolve => { chrome.storage.local.set({ [`keypairs:${keyspace}`]: formattedVaultKeypairs }, () => { console.log(`Saved ${formattedVaultKeypairs.length} keypairs to Chrome storage for keyspace ${keyspace}`); resolve(); }); }); // Update session state in background with the new keypair information await new Promise((resolve, reject) => { chrome.runtime.sendMessage({ action: 'update_session', type: 'keypair_added', data: newKeypair }, async (response) => { if (response && response.success) { console.log('Background service worker updated with new keypair'); resolve(newKeypair); } else { const error = response?.error || 'Failed to update session state'; console.error('Error updating background state:', error); reject(new Error(error)); } }); }); // Also update the complete keypair list in background with the current vault state await new Promise(resolve => { chrome.runtime.sendMessage({ action: 'update_session', type: 'keypairs_loaded', data: formattedVaultKeypairs }, () => { console.log('Updated complete keypair list in background with vault state'); resolve(); }); }); return newKeypair; } catch (error) { console.error('Error adding keypair:', error); throw error; } } /** * Select a keypair * @param {string} keyId The ID of the keypair to select * @returns {Promise} */ export async function selectKeypair(keyId) { if (!wasmModule || !wasmModule.select_keypair) { throw new Error('WASM module not loaded'); } // Call the WASM select_keypair function await wasmModule.select_keypair(keyId); // Update session state in background await new Promise((resolve, reject) => { chrome.runtime.sendMessage({ action: 'update_session', type: 'keypair_selected', data: keyId }, (response) => { if (response && response.success) { resolve(); } else { reject(response && response.error ? response.error : 'Failed to update session state'); } }); }); } /** * Sign a message with the selected keypair * @param {string} message The message to sign * @returns {Promise} The signature as a hex string */ export async function sign(message) { if (!wasmModule || !wasmModule.sign) { throw new Error('WASM module not loaded'); } // Convert message to Uint8Array const encoder = new TextEncoder(); const messageBytes = encoder.encode(message); // Call the WASM sign function return await wasmModule.sign(messageBytes); } /** * Get the current session state * @returns {Promise<{currentKeyspace: string|null, keypairs: Array, selectedKeypair: string|null}>} */ export async function getSessionState() { return new Promise((resolve) => { chrome.runtime.sendMessage({ action: 'get_session' }, (response) => { resolve(response || { currentKeyspace: null, keypairs: [], selectedKeypair: null }); }); }); }