feat: implement browser extension UI with WebAssembly integration

This commit is contained in:
Sameh Abouel-saad
2025-05-22 11:53:32 +03:00
parent 13945a8725
commit ed76ba3d8d
74 changed files with 7054 additions and 577 deletions

219
extension/popup/App.jsx Normal file
View File

@@ -0,0 +1,219 @@
import React, { useState, useEffect } from 'react';
import KeyspaceManager from './KeyspaceManager';
import KeypairManager from './KeypairManager';
import SignMessage from './SignMessage';
import * as wasmHelper from './WasmHelper';
function App() {
const [wasmState, setWasmState] = useState({
loading: false,
initialized: false,
error: null
});
const [locked, setLocked] = useState(true);
const [keyspaces, setKeyspaces] = useState([]);
const [currentKeyspace, setCurrentKeyspace] = useState('');
const [keypairs, setKeypairs] = useState([]); // [{id, label, publicKey}]
const [selectedKeypair, setSelectedKeypair] = useState('');
const [signature, setSignature] = useState('');
const [loading, setLoading] = useState(false);
const [status, setStatus] = useState('');
// Load WebAssembly on component mount
useEffect(() => {
async function initWasm() {
try {
setStatus('Loading WebAssembly module...');
await wasmHelper.loadWasmModule();
setWasmState(wasmHelper.getWasmState());
setStatus('WebAssembly module loaded');
// Load session state
await refreshStatus();
} catch (error) {
console.error('Failed to load WebAssembly:', error);
setStatus('Error loading WebAssembly: ' + (error.message || 'Unknown error'));
}
}
initWasm();
}, []);
// Fetch status from background on mount
async function refreshStatus() {
const state = await wasmHelper.getSessionState();
setCurrentKeyspace(state.currentKeyspace || '');
setKeypairs(state.keypairs || []);
setSelectedKeypair(state.selectedKeypair || '');
setLocked(!state.currentKeyspace);
// For demo: collect all keyspaces from storage
if (state.keypairs && state.keypairs.length > 0) {
setKeyspaces([state.currentKeyspace]);
} else {
setKeyspaces([state.currentKeyspace].filter(Boolean));
}
}
// Session unlock/create
const handleUnlock = async (keyspace, password) => {
if (!wasmState.initialized) {
setStatus('WebAssembly module not loaded');
return;
}
setLoading(true);
setStatus('Unlocking...');
try {
await wasmHelper.initSession(keyspace, password);
setCurrentKeyspace(keyspace);
setLocked(false);
setStatus('Session unlocked!');
await refreshStatus();
} catch (e) {
setStatus('Unlock failed: ' + e);
}
setLoading(false);
};
const handleCreateKeyspace = async (keyspace, password) => {
if (!wasmState.initialized) {
setStatus('WebAssembly module not loaded');
return;
}
setLoading(true);
setStatus('Creating keyspace...');
try {
await wasmHelper.initSession(keyspace, password);
setCurrentKeyspace(keyspace);
setLocked(false);
setStatus('Keyspace created and unlocked!');
await refreshStatus();
} catch (e) {
setStatus('Create failed: ' + e);
}
setLoading(false);
};
const handleLock = async () => {
if (!wasmState.initialized) {
setStatus('WebAssembly module not loaded');
return;
}
setLoading(true);
setStatus('Locking...');
try {
await wasmHelper.lockSession();
setLocked(true);
setCurrentKeyspace('');
setKeypairs([]);
setSelectedKeypair('');
setStatus('Session locked.');
await refreshStatus();
} catch (e) {
setStatus('Lock failed: ' + e);
}
setLoading(false);
};
const handleSelectKeypair = async (id) => {
if (!wasmState.initialized) {
setStatus('WebAssembly module not loaded');
return;
}
setLoading(true);
setStatus('Selecting keypair...');
try {
await wasmHelper.selectKeypair(id);
setSelectedKeypair(id);
setStatus('Keypair selected.');
await refreshStatus();
} catch (e) {
setStatus('Select failed: ' + e);
}
setLoading(false);
};
const handleCreateKeypair = async () => {
if (!wasmState.initialized) {
setStatus('WebAssembly module not loaded');
return;
}
setLoading(true);
setStatus('Creating keypair...');
try {
const keyId = await wasmHelper.addKeypair();
setStatus('Keypair created. ID: ' + keyId);
await refreshStatus();
} catch (e) {
setStatus('Create failed: ' + e);
}
setLoading(false);
};
const handleSign = async (message) => {
if (!wasmState.initialized) {
setStatus('WebAssembly module not loaded');
return;
}
setLoading(true);
setStatus('Signing message...');
try {
if (!selectedKeypair) {
throw new Error('No keypair selected');
}
const sig = await wasmHelper.sign(message);
setSignature(sig);
setStatus('Message signed!');
} catch (e) {
setStatus('Signing failed: ' + e);
setSignature('');
}
setLoading(false);
};
return (
<div className="App">
<h1>Modular Vault Extension</h1>
{wasmState.error && (
<div className="error">
WebAssembly Error: {wasmState.error}
</div>
)}
<KeyspaceManager
keyspaces={keyspaces}
onUnlock={handleUnlock}
onCreate={handleCreateKeyspace}
locked={locked}
onLock={handleLock}
currentKeyspace={currentKeyspace}
/>
{!locked && (
<>
<KeypairManager
keypairs={keypairs}
onSelect={handleSelectKeypair}
onCreate={handleCreateKeypair}
selectedKeypair={selectedKeypair}
/>
{selectedKeypair && (
<SignMessage
onSign={handleSign}
signature={signature}
loading={loading}
/>
)}
</>
)}
<div className="status" style={{marginTop: '1rem', minHeight: 24}}>
{status}
</div>
</div>
);
}
export default App;

View File

@@ -0,0 +1,30 @@
import React, { useState } from 'react';
export default function KeypairManager({ keypairs, onSelect, onCreate, selectedKeypair }) {
const [creating, setCreating] = useState(false);
return (
<div className="keypair-manager">
<label>Keypair:</label>
<select value={selectedKeypair || ''} onChange={e => onSelect(e.target.value)}>
<option value="" disabled>Select keypair</option>
{keypairs.map(kp => (
<option key={kp.id} value={kp.id}>{kp.label}</option>
))}
</select>
<button onClick={() => setCreating(true)} style={{marginLeft: 8}}>Create New</button>
{creating && (
<div style={{marginTop: '0.5rem'}}>
<button onClick={() => { onCreate(); setCreating(false); }}>Create Secp256k1 Keypair</button>
<button onClick={() => setCreating(false)} style={{marginLeft: 8}}>Cancel</button>
</div>
)}
{selectedKeypair && (
<div style={{marginTop: '0.5rem'}}>
<span>Public Key: <code>{keypairs.find(kp => kp.id === selectedKeypair)?.publicKey}</code></span>
<button onClick={() => navigator.clipboard.writeText(keypairs.find(kp => kp.id === selectedKeypair)?.publicKey)} style={{marginLeft: 8}}>Copy</button>
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,30 @@
import React, { useState } from 'react';
export default function KeyspaceManager({ keyspaces, onUnlock, onCreate, locked, onLock, currentKeyspace }) {
const [selected, setSelected] = useState(keyspaces[0] || '');
const [password, setPassword] = useState('');
const [newKeyspace, setNewKeyspace] = useState('');
if (locked) {
return (
<div className="keyspace-manager">
<label>Keyspace:</label>
<select value={selected} onChange={e => setSelected(e.target.value)}>
{keyspaces.map(k => <option key={k} value={k}>{k}</option>)}
</select>
<button onClick={() => onUnlock(selected, password)} disabled={!selected || !password}>Unlock</button>
<div style={{marginTop: '0.5rem'}}>
<input placeholder="New keyspace name" value={newKeyspace} onChange={e => setNewKeyspace(e.target.value)} />
<input placeholder="Password" type="password" value={password} onChange={e => setPassword(e.target.value)} />
<button onClick={() => onCreate(newKeyspace, password)} disabled={!newKeyspace || !password}>Create</button>
</div>
</div>
);
}
return (
<div className="keyspace-manager">
<span>Keyspace: <b>{currentKeyspace}</b></span>
<button onClick={onLock} style={{marginLeft: 8}}>Lock Session</button>
</div>
);
}

View File

@@ -0,0 +1,27 @@
import React, { useState } from 'react';
export default function SignMessage({ onSign, signature, loading }) {
const [message, setMessage] = useState('');
return (
<div className="sign-message">
<label>Message to sign:</label>
<input
type="text"
placeholder="Enter plaintext message"
value={message}
onChange={e => setMessage(e.target.value)}
style={{width: '100%', marginBottom: 8}}
/>
<button onClick={() => onSign(message)} disabled={!message || loading}>
{loading ? 'Signing...' : 'Sign'}
</button>
{signature && (
<div style={{marginTop: '0.5rem'}}>
<span>Signature: <code>{signature}</code></span>
<button onClick={() => navigator.clipboard.writeText(signature)} style={{marginLeft: 8}}>Copy</button>
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,667 @@
/**
* 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 });
});
});
}

View File

@@ -0,0 +1,88 @@
import React, { useState, useEffect, createContext, useContext } from 'react';
// Create a context to share the WASM module across components
export const WasmContext = createContext(null);
// Hook to access WASM module
export function useWasm() {
return useContext(WasmContext);
}
// Component that loads and initializes the WASM module
export function WasmProvider({ children }) {
const [wasmModule, setWasmModule] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
async function loadWasm() {
try {
setLoading(true);
// Instead of using dynamic imports which require correct MIME types,
// we'll use fetch to load the JavaScript file as text and eval it
const wasmJsPath = chrome.runtime.getURL('wasm/wasm_app.js');
console.log('Loading WASM JS from:', wasmJsPath);
// Load the JavaScript file
const jsResponse = await fetch(wasmJsPath);
if (!jsResponse.ok) {
throw new Error(`Failed to load WASM JS: ${jsResponse.status} ${jsResponse.statusText}`);
}
// Get the JavaScript code as text
const jsCode = await jsResponse.text();
// Create a function to execute the code in an isolated scope
let wasmModuleExports = {};
const moduleFunction = new Function('exports', jsCode + '\nreturn { initSync, default: __wbg_init, init_rhai_env, init_session, lock_session, add_keypair, select_keypair, sign, run_rhai };');
// Execute the function to get the exports
const wasmModule = moduleFunction(wasmModuleExports);
// Initialize WASM with the binary
const wasmBinaryPath = chrome.runtime.getURL('wasm/wasm_app_bg.wasm');
console.log('Initializing WASM with binary:', wasmBinaryPath);
const binaryResponse = await fetch(wasmBinaryPath);
if (!binaryResponse.ok) {
throw new Error(`Failed to load WASM binary: ${binaryResponse.status} ${binaryResponse.statusText}`);
}
const wasmBinary = await binaryResponse.arrayBuffer();
// Initialize the WASM module
await wasmModule.default(wasmBinary);
// Initialize the WASM environment
if (typeof wasmModule.init_rhai_env === 'function') {
wasmModule.init_rhai_env();
}
console.log('WASM module loaded successfully');
setWasmModule(wasmModule);
setLoading(false);
} catch (error) {
console.error('Failed to load WASM module:', error);
setError(error.message || 'Failed to load WebAssembly module');
setLoading(false);
}
}
loadWasm();
}, []);
if (loading) {
return <div className="wasm-loading">Loading WebAssembly module...</div>;
}
if (error) {
return <div className="wasm-error">Error: {error}</div>;
}
return (
<WasmContext.Provider value={wasmModule}>
{children}
</WasmContext.Provider>
);
}

View File

@@ -0,0 +1,88 @@
/**
* Debug helper for WebAssembly Vault with Rhai scripts
*/
// Helper to try various Rhai scripts for debugging
export const RHAI_SCRIPTS = {
// Check if there's an active session
CHECK_SESSION: `
let has_session = false;
let current_keyspace = "";
// Try to access functions expected to exist in the vault namespace
if (isdef(vault) && isdef(vault::has_active_session)) {
has_session = vault::has_active_session();
if (has_session && isdef(vault::get_current_keyspace)) {
current_keyspace = vault::get_current_keyspace();
}
}
{
"has_session": has_session,
"keyspace": current_keyspace,
"available_functions": [
isdef(vault::list_keypairs) ? "list_keypairs" : null,
isdef(vault::add_keypair) ? "add_keypair" : null,
isdef(vault::has_active_session) ? "has_active_session" : null,
isdef(vault::get_current_keyspace) ? "get_current_keyspace" : null
]
}
`,
// Explicitly get keypairs for the current keyspace using session data
LIST_KEYPAIRS: `
let result = {"error": "Not initialized"};
if (isdef(vault) && isdef(vault::has_active_session) && vault::has_active_session()) {
let keyspace = vault::get_current_keyspace();
// Try to list the keypairs from the current session
if (isdef(vault::get_keypairs_from_session)) {
result = {
"keyspace": keyspace,
"keypairs": vault::get_keypairs_from_session()
};
} else {
result = {
"error": "vault::get_keypairs_from_session is not defined",
"keyspace": keyspace
};
}
}
result
`,
// Use Rhai to inspect the Vault storage directly (for advanced debugging)
INSPECT_VAULT_STORAGE: `
let result = {"error": "Not accessible"};
if (isdef(vault) && isdef(vault::inspect_storage)) {
result = vault::inspect_storage();
}
result
`
};
// Run all debug scripts and collect results
export async function runDiagnostics(wasmModule) {
if (!wasmModule || !wasmModule.run_rhai) {
throw new Error('WebAssembly module not loaded or run_rhai not available');
}
const results = {};
for (const [name, script] of Object.entries(RHAI_SCRIPTS)) {
try {
console.log(`Running Rhai diagnostic script: ${name}`);
results[name] = await wasmModule.run_rhai(script);
console.log(`Result from ${name}:`, results[name]);
} catch (error) {
console.error(`Error running script ${name}:`, error);
results[name] = { error: error.toString() };
}
}
return results;
}

View File

@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Modular Vault Extension</title>
<link rel="stylesheet" href="popup.css">
</head>
<body>
<div id="root"></div>
<script src="popup.js"></script>
</body>
</html>

View File

@@ -0,0 +1,8 @@
import React from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
import './style.css';
// Render the React app
const root = createRoot(document.getElementById('root'));
root.render(<App />);

117
extension/popup/popup.css Normal file
View File

@@ -0,0 +1,117 @@
/* Basic styles for the extension popup */
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
margin: 0;
padding: 0;
background-color: #202124;
color: #e8eaed;
}
.container {
width: 350px;
padding: 15px;
}
h1 {
font-size: 18px;
margin: 0 0 15px 0;
border-bottom: 1px solid #3c4043;
padding-bottom: 10px;
}
h2 {
font-size: 16px;
margin: 10px 0;
}
.form-section {
margin-bottom: 20px;
background-color: #292a2d;
border-radius: 8px;
padding: 15px;
}
.form-group {
margin-bottom: 10px;
}
label {
display: block;
margin-bottom: 5px;
font-size: 13px;
color: #9aa0a6;
}
input, textarea {
width: 100%;
padding: 8px;
border: 1px solid #3c4043;
border-radius: 4px;
background-color: #202124;
color: #e8eaed;
box-sizing: border-box;
}
textarea {
min-height: 60px;
resize: vertical;
}
button {
background-color: #8ab4f8;
color: #202124;
border: none;
border-radius: 4px;
padding: 8px 16px;
font-weight: 500;
cursor: pointer;
transition: background-color 0.3s;
}
button:hover {
background-color: #669df6;
}
button.small {
padding: 4px 8px;
font-size: 12px;
}
.button-group {
display: flex;
gap: 10px;
}
.status {
margin: 10px 0;
padding: 8px;
background-color: #292a2d;
border-radius: 4px;
font-size: 13px;
}
.list {
margin-top: 10px;
max-height: 150px;
overflow-y: auto;
}
.list-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px;
border-bottom: 1px solid #3c4043;
}
.list-item.selected {
background-color: rgba(138, 180, 248, 0.1);
}
.hidden {
display: none;
}
.session-info {
margin-top: 15px;
}

306
extension/popup/popup.js Normal file
View File

@@ -0,0 +1,306 @@
// Simple non-module JavaScript for browser extension popup
document.addEventListener('DOMContentLoaded', async function() {
const root = document.getElementById('root');
root.innerHTML = `
<div class="container">
<h1>Modular Vault Extension</h1>
<div id="status" class="status">Loading WASM module...</div>
<div id="session-controls">
<div id="keyspace-form" class="form-section">
<h2>Session</h2>
<div class="form-group">
<label for="keyspace">Keyspace:</label>
<input type="text" id="keyspace" placeholder="Enter keyspace name">
</div>
<div class="form-group">
<label for="password">Password:</label>
<input type="password" id="password" placeholder="Enter password">
</div>
<div class="button-group">
<button id="unlock-btn">Unlock</button>
<button id="create-btn">Create New</button>
</div>
</div>
<div id="session-info" class="session-info hidden">
<h2>Active Session</h2>
<p>Current keyspace: <span id="current-keyspace"></span></p>
<button id="lock-btn">Lock Session</button>
<div id="keypair-section" class="form-section">
<h2>Keypairs</h2>
<button id="create-keypair-btn">Create New Keypair</button>
<div id="keypair-list" class="list"></div>
</div>
<div id="sign-section" class="form-section hidden">
<h2>Sign Message</h2>
<div class="form-group">
<label for="message">Message:</label>
<textarea id="message" placeholder="Enter message to sign"></textarea>
</div>
<button id="sign-btn">Sign</button>
<div class="form-group">
<label for="signature">Signature:</label>
<textarea id="signature" readonly></textarea>
<button id="copy-btn" class="small">Copy</button>
</div>
</div>
</div>
</div>
</div>
`;
// DOM elements
const statusEl = document.getElementById('status');
const keyspaceFormEl = document.getElementById('keyspace-form');
const sessionInfoEl = document.getElementById('session-info');
const currentKeyspaceEl = document.getElementById('current-keyspace');
const keyspaceInput = document.getElementById('keyspace');
const passwordInput = document.getElementById('password');
const unlockBtn = document.getElementById('unlock-btn');
const createBtn = document.getElementById('create-btn');
const lockBtn = document.getElementById('lock-btn');
const createKeypairBtn = document.getElementById('create-keypair-btn');
const keypairListEl = document.getElementById('keypair-list');
const signSectionEl = document.getElementById('sign-section');
const messageInput = document.getElementById('message');
const signBtn = document.getElementById('sign-btn');
const signatureOutput = document.getElementById('signature');
const copyBtn = document.getElementById('copy-btn');
// State
let wasmModule = null;
let currentKeyspace = null;
let keypairs = [];
let selectedKeypairId = null;
// Initialize
init();
async function init() {
try {
// Get session state from background
const sessionState = await getSessionState();
if (sessionState.currentKeyspace) {
// We have an active session
currentKeyspace = sessionState.currentKeyspace;
keypairs = sessionState.keypairs || [];
selectedKeypairId = sessionState.selectedKeypair;
updateUI();
}
statusEl.textContent = 'Ready';
} catch (error) {
statusEl.textContent = 'Error: ' + (error.message || 'Unknown error');
}
}
function updateUI() {
if (currentKeyspace) {
// Show session info
keyspaceFormEl.classList.add('hidden');
sessionInfoEl.classList.remove('hidden');
currentKeyspaceEl.textContent = currentKeyspace;
// Update keypair list
updateKeypairList();
// Show/hide sign section based on selected keypair
if (selectedKeypairId) {
signSectionEl.classList.remove('hidden');
} else {
signSectionEl.classList.add('hidden');
}
} else {
// Show keyspace form
keyspaceFormEl.classList.remove('hidden');
sessionInfoEl.classList.add('hidden');
}
}
function updateKeypairList() {
// Clear list
keypairListEl.innerHTML = '';
// Add each keypair
keypairs.forEach(keypair => {
const item = document.createElement('div');
item.className = 'list-item' + (selectedKeypairId === keypair.id ? ' selected' : '');
item.innerHTML = `
<span>${keypair.label || keypair.id}</span>
<button class="select-btn" data-id="${keypair.id}">Select</button>
`;
keypairListEl.appendChild(item);
// Add select handler
item.querySelector('.select-btn').addEventListener('click', async () => {
try {
statusEl.textContent = 'Selecting keypair...';
// Use background service to select keypair for now
await chrome.runtime.sendMessage({
action: 'update_session',
type: 'keypair_selected',
data: keypair.id
});
selectedKeypairId = keypair.id;
updateUI();
statusEl.textContent = 'Keypair selected: ' + keypair.id;
} catch (error) {
statusEl.textContent = 'Error selecting keypair: ' + (error.message || 'Unknown error');
}
});
});
}
// Get session state from background
async function getSessionState() {
return new Promise((resolve) => {
chrome.runtime.sendMessage({ action: 'get_session' }, (response) => {
resolve(response || { currentKeyspace: null, keypairs: [], selectedKeypair: null });
});
});
}
// Event handlers
unlockBtn.addEventListener('click', async () => {
const keyspace = keyspaceInput.value.trim();
const password = passwordInput.value;
if (!keyspace || !password) {
statusEl.textContent = 'Please enter keyspace and password';
return;
}
statusEl.textContent = 'Unlocking session...';
try {
// For now, use the background service worker mock
await chrome.runtime.sendMessage({
action: 'update_session',
type: 'keyspace',
data: keyspace
});
currentKeyspace = keyspace;
updateUI();
statusEl.textContent = 'Session unlocked!';
// Refresh state
const state = await getSessionState();
keypairs = state.keypairs || [];
selectedKeypairId = state.selectedKeypair;
updateUI();
} catch (error) {
statusEl.textContent = 'Error unlocking session: ' + (error.message || 'Unknown error');
}
});
createBtn.addEventListener('click', async () => {
const keyspace = keyspaceInput.value.trim();
const password = passwordInput.value;
if (!keyspace || !password) {
statusEl.textContent = 'Please enter keyspace and password';
return;
}
statusEl.textContent = 'Creating keyspace...';
try {
// For now, use the background service worker mock
await chrome.runtime.sendMessage({
action: 'update_session',
type: 'keyspace',
data: keyspace
});
currentKeyspace = keyspace;
updateUI();
statusEl.textContent = 'Keyspace created and unlocked!';
} catch (error) {
statusEl.textContent = 'Error creating keyspace: ' + (error.message || 'Unknown error');
}
});
lockBtn.addEventListener('click', async () => {
statusEl.textContent = 'Locking session...';
try {
await chrome.runtime.sendMessage({
action: 'update_session',
type: 'session_locked'
});
currentKeyspace = null;
keypairs = [];
selectedKeypairId = null;
updateUI();
statusEl.textContent = 'Session locked';
} catch (error) {
statusEl.textContent = 'Error locking session: ' + (error.message || 'Unknown error');
}
});
createKeypairBtn.addEventListener('click', async () => {
statusEl.textContent = 'Creating keypair...';
try {
// Generate a mock keypair ID
const keyId = 'key-' + Date.now().toString(16);
const newKeypair = {
id: keyId,
label: `Secp256k1-Key-${keypairs.length + 1}`
};
await chrome.runtime.sendMessage({
action: 'update_session',
type: 'keypair_added',
data: newKeypair
});
// Refresh state
const state = await getSessionState();
keypairs = state.keypairs || [];
updateUI();
statusEl.textContent = 'Keypair created: ' + keyId;
} catch (error) {
statusEl.textContent = 'Error creating keypair: ' + (error.message || 'Unknown error');
}
});
signBtn.addEventListener('click', async () => {
const message = messageInput.value.trim();
if (!message) {
statusEl.textContent = 'Please enter a message to sign';
return;
}
if (!selectedKeypairId) {
statusEl.textContent = 'Please select a keypair first';
return;
}
statusEl.textContent = 'Signing message...';
try {
// For now, generate a mock signature
const mockSignature = Array.from({length: 64}, () => Math.floor(Math.random() * 16).toString(16)).join('');
signatureOutput.value = mockSignature;
statusEl.textContent = 'Message signed!';
} catch (error) {
statusEl.textContent = 'Error signing message: ' + (error.message || 'Unknown error');
}
});
copyBtn.addEventListener('click', () => {
signatureOutput.select();
document.execCommand('copy');
statusEl.textContent = 'Signature copied to clipboard!';
});
});

26
extension/popup/style.css Normal file
View File

@@ -0,0 +1,26 @@
body {
margin: 0;
font-family: 'Inter', Arial, sans-serif;
background: #181c20;
color: #f3f6fa;
}
.App {
padding: 1.5rem;
min-width: 320px;
max-width: 400px;
background: #23272e;
border-radius: 12px;
box-shadow: 0 4px 24px rgba(0,0,0,0.2);
}
h1 {
font-size: 1.5rem;
margin-bottom: 0.5rem;
}
p {
color: #b0bac9;
margin-bottom: 1.5rem;
}
.status {
margin-bottom: 1rem;
}

317
extension/popup/wasm.js Normal file
View File

@@ -0,0 +1,317 @@
// WebAssembly API functions for accessing WASM operations directly
// and synchronizing state with background service worker
// Get session state from the background service worker
export function getStatus() {
return new Promise((resolve) => {
chrome.runtime.sendMessage({ action: 'get_session' }, (response) => {
resolve(response);
});
});
}
// Debug function to examine vault state using Rhai scripts
export async function debugVaultState(wasmModule) {
if (!wasmModule) {
throw new Error('WASM module not loaded');
}
try {
console.log('🔍 Debugging vault state...');
// First check if we have a valid session
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 wasmModule.run_rhai(sessionCheck);
console.log('Session status:', sessionStatus);
// Only try to get keypairs if we have an active session
if (sessionStatus && sessionStatus.has_session) {
// Get information about all keypairs
const keypairsScript = `
// Get all keypairs for the current keyspace
let keypairs = vault::list_keypairs();
// Add more diagnostic information
let diagnostic = {
"keypair_count": keypairs.len(),
"keyspace": vault::get_current_keyspace(),
"keypairs": keypairs
};
diagnostic
`;
console.log('Fetching keypair details...');
const keypairDiagnostic = await wasmModule.run_rhai(keypairsScript);
console.log('Keypair diagnostic:', keypairDiagnostic);
return keypairDiagnostic;
} else {
console.log('No active session, cannot fetch keypairs');
return { error: 'No active session' };
}
} catch (error) {
console.error('Error in debug function:', error);
return { error: error.toString() };
}
}
// Fetch all keypairs from the WebAssembly vault
export async function getKeypairsFromVault(wasmModule) {
if (!wasmModule) {
throw new Error('WASM module not loaded');
}
try {
// First run diagnostics for debugging
await debugVaultState(wasmModule);
console.log('Calling list_keypairs WebAssembly binding...');
// Use our new direct WebAssembly binding instead of Rhai script
const keypairList = await wasmModule.list_keypairs();
console.log('Retrieved keypairs from vault:', keypairList);
// Transform the keypairs into the expected format
// The WebAssembly binding returns an array of objects with id, type, and metadata
const formattedKeypairs = Array.isArray(keypairList) ? keypairList.map(kp => {
// Parse metadata if it's a string
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 || `${kp.type}-Key-${kp.id.substring(0, 4)}`
};
}) : [];
console.log('Formatted keypairs:', formattedKeypairs);
// Update the keypairs in the background service worker
return new Promise((resolve) => {
chrome.runtime.sendMessage({
action: 'update_session',
type: 'keypairs_loaded',
data: formattedKeypairs
}, (response) => {
if (response && response.success) {
console.log('Successfully updated keypairs in background');
resolve(formattedKeypairs);
} else {
console.error('Failed to update keypairs in background:', response?.error);
resolve([]);
}
});
});
} catch (error) {
console.error('Error fetching keypairs from vault:', error);
return [];
}
}
// Initialize session with the WASM module
export function initSession(wasmModule, keyspace, password) {
return new Promise(async (resolve, reject) => {
if (!wasmModule) {
reject('WASM module not loaded');
return;
}
try {
// Call the WASM init_session function
console.log(`Initializing session for keyspace: ${keyspace}`);
await wasmModule.init_session(keyspace, password);
// Update the session state in the background service worker
chrome.runtime.sendMessage({
action: 'update_session',
type: 'keyspace',
data: keyspace
}, async (response) => {
if (response && response.success) {
try {
// After successful session initialization, fetch keypairs from the vault
console.log('Session initialized, fetching keypairs from vault...');
const keypairs = await getKeypairsFromVault(wasmModule);
console.log('Keypairs loaded:', keypairs);
resolve(keypairs);
} catch (fetchError) {
console.error('Error fetching keypairs:', fetchError);
// Even if fetching keypairs fails, the session is initialized
resolve([]);
}
} else {
reject(response && response.error ? response.error : 'Failed to update session state');
}
});
} catch (error) {
console.error('Session initialization error:', error);
reject(error.message || 'Failed to initialize session');
}
});
}
// Lock the session using the WASM module
export function lockSession(wasmModule) {
return new Promise(async (resolve, reject) => {
if (!wasmModule) {
reject('WASM module not loaded');
return;
}
try {
// Call the WASM lock_session function
wasmModule.lock_session();
// Update the session state in the background service worker
chrome.runtime.sendMessage({
action: 'update_session',
type: 'session_locked'
}, (response) => {
if (response && response.success) {
resolve();
} else {
reject(response && response.error ? response.error : 'Failed to update session state');
}
});
} catch (error) {
reject(error.message || 'Failed to lock session');
}
});
}
// Add a keypair using the WASM module
export function addKeypair(wasmModule, keyType = 'Secp256k1', label = null) {
return new Promise(async (resolve, reject) => {
if (!wasmModule) {
reject('WASM module not loaded');
return;
}
try {
// Create a default label if none provided
const keyLabel = label || `${keyType}-Key-${Date.now().toString(16).slice(-4)}`;
// Create metadata JSON for the keypair
const metadata = JSON.stringify({
label: keyLabel,
created: new Date().toISOString(),
type: keyType
});
console.log(`Adding new keypair of type ${keyType} with label ${keyLabel}`);
// Call the WASM add_keypair function with metadata
const keyId = await wasmModule.add_keypair(keyType, metadata);
console.log(`Keypair created with ID: ${keyId}`);
// Create keypair object with ID and label
const newKeypair = {
id: keyId,
label: keyLabel
};
// Update the session state in the background service worker
chrome.runtime.sendMessage({
action: 'update_session',
type: 'keypair_added',
data: newKeypair
}, (response) => {
if (response && response.success) {
// After adding a keypair, refresh the whole list from the vault
getKeypairsFromVault(wasmModule)
.then(() => {
console.log('Keypair list refreshed from vault');
resolve(keyId);
})
.catch(refreshError => {
console.warn('Error refreshing keypair list:', refreshError);
// Still resolve with the key ID since the key was created
resolve(keyId);
});
} else {
reject(response && response.error ? response.error : 'Failed to update session state');
}
});
} catch (error) {
console.error('Error adding keypair:', error);
reject(error.message || 'Failed to add keypair');
}
});
}
// Select a keypair using the WASM module
export function selectKeypair(wasmModule, keyId) {
return new Promise(async (resolve, reject) => {
if (!wasmModule) {
reject('WASM module not loaded');
return;
}
try {
// Call the WASM select_keypair function
await wasmModule.select_keypair(keyId);
// Update the session state in the background service worker
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');
}
});
} catch (error) {
reject(error.message || 'Failed to select keypair');
}
});
}
// Sign a message using the WASM module
export function sign(wasmModule, message) {
return new Promise(async (resolve, reject) => {
if (!wasmModule) {
reject('WASM module not loaded');
return;
}
try {
// Convert message to Uint8Array for WASM
const encoder = new TextEncoder();
const messageBytes = encoder.encode(message);
// Call the WASM sign function
const signature = await wasmModule.sign(messageBytes);
resolve(signature);
} catch (error) {
reject(error.message || 'Failed to sign message');
}
});
}