sal-modular/extension/popup/WasmHelper.js

668 lines
23 KiB
JavaScript

/**
* 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<void>}
*/
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<object>} 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<Array>} 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<boolean>} 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<Array>} 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<void>}
*/
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<void>}
*/
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<string>} 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 });
});
});
}