feat: implement browser extension UI with WebAssembly integration
This commit is contained in:
		
							
								
								
									
										30
									
								
								wasm_app/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								wasm_app/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | ||||
| [package] | ||||
| name = "wasm_app" | ||||
| version = "0.1.0" | ||||
| edition = "2021" | ||||
|  | ||||
| [lib] | ||||
| crate-type = ["cdylib"] | ||||
|  | ||||
| [dependencies] | ||||
| web-sys = { version = "0.3", features = ["console"] } | ||||
| kvstore = { path = "../kvstore" } | ||||
| hex = "0.4" | ||||
| wasm-bindgen = { version = "0.2", features = ["serde-serialize"] } | ||||
| gloo-utils = "0.1" | ||||
|  | ||||
| #  | ||||
| serde = { version = "1.0", features = ["derive"] } | ||||
| serde_json = "1.0" | ||||
| rhai = { version = "1.16", features = ["serde"] } | ||||
| wasm-bindgen-futures = "0.4" | ||||
| once_cell = "1.21" | ||||
| vault = { path = "../vault" } | ||||
| evm_client = { path = "../evm_client" } | ||||
|  | ||||
| [dev-dependencies] | ||||
| wasm-bindgen-test = "0.3" | ||||
|  | ||||
| [target.'cfg(target_arch = "wasm32")'.dependencies] | ||||
| getrandom = { version = "0.3", features = ["wasm_js"] } | ||||
| getrandom_02 = { package = "getrandom", version = "0.2.16", features = ["js"] } | ||||
							
								
								
									
										172
									
								
								wasm_app/src/debug_bindings.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										172
									
								
								wasm_app/src/debug_bindings.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,172 @@ | ||||
| //! WASM-only debug bindings for the vault extension | ||||
| #![cfg(target_arch = "wasm32")] | ||||
|  | ||||
| use wasm_bindgen::prelude::*; | ||||
| use crate::{SESSION_MANAGER, SESSION_PASSWORD}; | ||||
|  | ||||
| /// Debugging function to check if keypairs can be listed | ||||
| #[wasm_bindgen] | ||||
| pub async fn list_keypairs_debug() -> Result<JsValue, JsValue> { | ||||
|     use js_sys::{Array, Object}; | ||||
|     use web_sys::console; | ||||
|     console::log_1(&"Debug listing keypairs...".into()); | ||||
|     let session_ptr = SESSION_MANAGER.with(|cell| { | ||||
|         let has_session = cell.borrow().is_some(); | ||||
|         console::log_1(&format!("Has session: {}", has_session).into()); | ||||
|         cell.borrow().as_ref().map(|s| s as *const _) | ||||
|     }); | ||||
|     let password_opt = SESSION_PASSWORD.with(|pw| { | ||||
|         let has_pw = pw.borrow().is_some(); | ||||
|         console::log_1(&format!("Has password: {}", has_pw).into()); | ||||
|         pw.borrow().clone() | ||||
|     }); | ||||
|     if session_ptr.is_none() { | ||||
|         return Err(JsValue::from_str("Session not initialized in debug function")); | ||||
|     } | ||||
|     if password_opt.is_none() { | ||||
|         return Err(JsValue::from_str("Session password not set in debug function")); | ||||
|     } | ||||
|     let session: &vault::session::SessionManager<kvstore::wasm::WasmStore> = unsafe { &*session_ptr.unwrap() }; | ||||
|     let password = password_opt.unwrap(); | ||||
|     match session.current_keyspace_name() { | ||||
|         Some(ks) => { | ||||
|             let vault = session.get_vault(); | ||||
|             match vault.list_keypairs(ks, &password).await { | ||||
|                 Ok(keypairs) => { | ||||
|                     console::log_1(&format!("Found {} keypairs", keypairs.len()).into()); | ||||
|                     let array = Array::new(); | ||||
|                     for (id, key_type) in keypairs { | ||||
|                         let obj = Object::new(); | ||||
|                         js_sys::Reflect::set(&obj, &JsValue::from_str("id"), &JsValue::from_str(&id)).unwrap(); | ||||
|                         js_sys::Reflect::set(&obj, &JsValue::from_str("type"), &JsValue::from_str(&format!("{:?}", key_type))).unwrap(); | ||||
|                         array.push(&obj); | ||||
|                     } | ||||
|                     return Ok(array.into()); | ||||
|                 } | ||||
|                 Err(e) => { | ||||
|                     console::error_1(&format!("Error listing keypairs in debug function: {}", e).into()); | ||||
|                     return Err(JsValue::from_str(&format!("Error listing keypairs: {}", e))); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         None => { | ||||
|             console::error_1(&"No keyspace selected in debug function".into()); | ||||
|             return Err(JsValue::from_str("No keyspace selected")); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[wasm_bindgen] | ||||
| pub async fn check_indexeddb() -> Result<JsValue, JsValue> { | ||||
|     #[cfg(not(target_arch = "wasm32"))] | ||||
|     { | ||||
|         return Err(JsValue::from_str( | ||||
|             "IndexedDB check only available in browser context", | ||||
|         )); | ||||
|     } | ||||
|  | ||||
|     { | ||||
|         use js_sys::Object; | ||||
|         use kvstore::traits::KVStore; | ||||
|         use kvstore::wasm::WasmStore; | ||||
|         use web_sys::console; // Import the trait so we can use its methods | ||||
|  | ||||
|         console::log_1(&"Checking IndexedDB availability...".into()); | ||||
|  | ||||
|         // Check if window.indexedDB is available | ||||
|         if js_sys::eval("typeof window.indexedDB") | ||||
|             .map_err(|e| { | ||||
|                 console::error_1(&format!("Error checking IndexedDB: {:?}", e).into()); | ||||
|                 JsValue::from_str(&format!("Error checking IndexedDB: {:?}", e)) | ||||
|             })? | ||||
|             .as_string() | ||||
|             .unwrap_or_default() | ||||
|             == "undefined" | ||||
|         { | ||||
|             console::error_1(&"IndexedDB is not available in this browser".into()); | ||||
|             return Err(JsValue::from_str( | ||||
|                 "IndexedDB is not available in this browser", | ||||
|             )); | ||||
|         } | ||||
|  | ||||
|         // Try to create a test database | ||||
|         match WasmStore::open("db_test").await { | ||||
|             Ok(store) => { | ||||
|                 console::log_1(&"Successfully opened test database".into()); | ||||
|  | ||||
|                 // Try to write and read a value to ensure it works | ||||
|                 let test_key = "test_key"; | ||||
|                 let test_value = "test_value"; | ||||
|  | ||||
|                 // Use the KVStore trait methods | ||||
|                 if let Err(e) = store.set(test_key, test_value.as_bytes()).await { | ||||
|                     console::error_1(&format!("Failed to write to IndexedDB: {}", e).into()); | ||||
|                     return Err(JsValue::from_str(&format!( | ||||
|                         "Failed to write to IndexedDB: {}", | ||||
|                         e | ||||
|                     ))); | ||||
|                 } | ||||
|  | ||||
|                 // Get the value and handle the Option<Vec<u8>> properly | ||||
|                 match store.get(test_key).await { | ||||
|                     Ok(maybe_value) => match maybe_value { | ||||
|                         Some(value) => { | ||||
|                             let value_str = String::from_utf8_lossy(&value); | ||||
|                             if value_str == test_value { | ||||
|                                 console::log_1( | ||||
|                                     &"Successfully read test value from IndexedDB".into(), | ||||
|                                 ); | ||||
|                             } else { | ||||
|                                 console::error_1( | ||||
|                                     &format!( | ||||
|                                         "IndexedDB test value mismatch: expected {}, got {}", | ||||
|                                         test_value, value_str | ||||
|                                     ) | ||||
|                                     .into(), | ||||
|                                 ); | ||||
|                                 return Err(JsValue::from_str("IndexedDB test value mismatch")); | ||||
|                             } | ||||
|                         } | ||||
|                         None => { | ||||
|                             console::error_1(&"IndexedDB test key not found after writing".into()); | ||||
|                             return Err(JsValue::from_str( | ||||
|                                 "IndexedDB test key not found after writing", | ||||
|                             )); | ||||
|                         } | ||||
|                     }, | ||||
|                     Err(e) => { | ||||
|                         console::error_1(&format!("Failed to read from IndexedDB: {}", e).into()); | ||||
|                         return Err(JsValue::from_str(&format!( | ||||
|                             "Failed to read from IndexedDB: {}", | ||||
|                             e | ||||
|                         ))); | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 // Return success with the available database names | ||||
|                 let result = Object::new(); | ||||
|                 js_sys::Reflect::set( | ||||
|                     &result, | ||||
|                     &JsValue::from_str("status"), | ||||
|                     &JsValue::from_str("success"), | ||||
|                 ) | ||||
|                 .unwrap(); | ||||
|                 js_sys::Reflect::set( | ||||
|                     &result, | ||||
|                     &JsValue::from_str("message"), | ||||
|                     &JsValue::from_str("IndexedDB is working properly"), | ||||
|                 ) | ||||
|                 .unwrap(); | ||||
|  | ||||
|                 return Ok(result.into()); | ||||
|             } | ||||
|             Err(e) => { | ||||
|                 console::error_1(&format!("Failed to open IndexedDB test database: {}", e).into()); | ||||
|                 return Err(JsValue::from_str(&format!( | ||||
|                     "Failed to open test database: {}", | ||||
|                     e | ||||
|                 ))); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										77
									
								
								wasm_app/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								wasm_app/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,77 @@ | ||||
| //! WASM entrypoint for Rhai scripting integration for the extension. | ||||
| //! Composes vault and evm_client Rhai bindings and exposes a secure run_rhai API. | ||||
| #![cfg(target_arch = "wasm32")] | ||||
|  | ||||
| use once_cell::unsync::Lazy; | ||||
| use std::cell::RefCell; | ||||
| use wasm_bindgen::prelude::*; | ||||
|  | ||||
| use wasm_bindgen::JsValue; | ||||
|  | ||||
| use rhai::Engine; | ||||
| use vault::rhai_bindings as vault_rhai_bindings; | ||||
| use vault::session::SessionManager; | ||||
|  | ||||
| use kvstore::wasm::WasmStore; | ||||
|  | ||||
| // Global singleton engine/session/client (for demonstration; production should scope per user/session) | ||||
| thread_local! { | ||||
|     static ENGINE: Lazy<RefCell<Engine>> = Lazy::new(|| RefCell::new(Engine::new())); | ||||
|     static SESSION_PASSWORD: RefCell<Option<Vec<u8>>> = RefCell::new(None); | ||||
| } | ||||
|  | ||||
| pub use vault::session_singleton::SESSION_MANAGER; | ||||
|  | ||||
| // Include the keypair bindings module | ||||
| mod vault_bindings; | ||||
| pub use vault_bindings::*; | ||||
|  | ||||
| /// Initialize the scripting environment (must be called before run_rhai) | ||||
| #[wasm_bindgen] | ||||
| pub fn init_rhai_env() { | ||||
|     ENGINE.with(|engine_cell| { | ||||
|         let mut engine = engine_cell.borrow_mut(); | ||||
|         // Register APIs with dummy session; will be replaced by real session after init | ||||
|         SESSION_MANAGER.with(|cell| { | ||||
|             if let Some(ref session) = cell.borrow().as_ref() { | ||||
|                 vault_rhai_bindings::register_rhai_api::<WasmStore>(&mut engine); | ||||
|             } | ||||
|         }); | ||||
|     }); | ||||
| } | ||||
|  | ||||
| /// Securely run a Rhai script in the extension context (must be called only after user approval) | ||||
| #[wasm_bindgen] | ||||
| pub fn run_rhai(script: &str) -> Result<JsValue, JsValue> { | ||||
|     ENGINE.with(|engine_cell| { | ||||
|         let mut engine = engine_cell.borrow_mut(); | ||||
|         SESSION_MANAGER.with(|cell| { | ||||
|             if let Some(ref mut session) = cell.borrow_mut().as_mut() { | ||||
|                 let mut scope = rhai::Scope::new(); | ||||
|                 engine | ||||
|                     .eval_with_scope::<rhai::Dynamic>(&mut scope, script) | ||||
|                     .map(|res| JsValue::from_str(&format!("{:?}", res))) | ||||
|                     .map_err(|e| JsValue::from_str(&format!("{}", e))) | ||||
|             } else { | ||||
|                 Err(JsValue::from_str("Session not initialized")) | ||||
|             } | ||||
|         }) | ||||
|     }) | ||||
| } | ||||
|  | ||||
| pub mod wasm_helpers { | ||||
|     use super::*; | ||||
|  | ||||
|     /// Global function to select keypair (used in Rhai) | ||||
|     pub fn select_keypair_global(key_id: &str) -> Result<(), String> { | ||||
|         SESSION_MANAGER.with(|cell| { | ||||
|             if let Some(session) = cell.borrow_mut().as_mut() { | ||||
|                 session | ||||
|                     .select_keypair(key_id) | ||||
|                     .map_err(|e| format!("select_keypair error: {e}")) | ||||
|             } else { | ||||
|                 Err("Session not initialized".to_string()) | ||||
|             } | ||||
|         }) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										185
									
								
								wasm_app/src/vault_bindings.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										185
									
								
								wasm_app/src/vault_bindings.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,185 @@ | ||||
| //! WebAssembly bindings for accessing vault operations (session, keypairs, signing, scripting, etc) | ||||
| #![cfg(target_arch = "wasm32")] | ||||
|  | ||||
| use kvstore::wasm::WasmStore; | ||||
| use once_cell::unsync::Lazy; | ||||
| use rhai::Engine; | ||||
| use std::cell::RefCell; | ||||
| use vault::rhai_bindings as vault_rhai_bindings; | ||||
| use vault::session::SessionManager; | ||||
| use wasm_bindgen::prelude::*; | ||||
| use wasm_bindgen::JsValue; | ||||
|  | ||||
| thread_local! { | ||||
|     static ENGINE: Lazy<RefCell<Engine>> = Lazy::new(|| RefCell::new(Engine::new())); | ||||
|     static SESSION_PASSWORD: RefCell<Option<Vec<u8>>> = RefCell::new(None); | ||||
| } | ||||
|  | ||||
| pub use vault::session_singleton::SESSION_MANAGER; | ||||
|  | ||||
| // ===================== | ||||
| // Session Lifecycle | ||||
| // ===================== | ||||
|  | ||||
| /// Initialize session with keyspace and password | ||||
| #[wasm_bindgen] | ||||
| pub async fn init_session(keyspace: &str, password: &str) -> Result<(), JsValue> { | ||||
|     let keyspace = keyspace.to_string(); | ||||
|     let password_vec = password.as_bytes().to_vec(); | ||||
|     match WasmStore::open(&keyspace).await { | ||||
|         Ok(store) => { | ||||
|             let vault = vault::Vault::new(store); | ||||
|             let mut manager = SessionManager::new(vault); | ||||
|             match manager.unlock_keyspace(&keyspace, &password_vec).await { | ||||
|                 Ok(_) => { | ||||
|                     SESSION_MANAGER.with(|cell| cell.replace(Some(manager))); | ||||
|                 } | ||||
|                 Err(e) => { | ||||
|                     web_sys::console::error_1(&format!("Failed to unlock keyspace: {e}").into()); | ||||
|                     return Err(JsValue::from_str(&format!("Failed to unlock keyspace: {e}"))); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         Err(e) => { | ||||
|             web_sys::console::error_1(&format!("Failed to open WasmStore: {e}").into()); | ||||
|             return Err(JsValue::from_str(&format!("Failed to open WasmStore: {e}"))); | ||||
|         } | ||||
|     } | ||||
|     SESSION_PASSWORD.with(|cell| cell.replace(Some(password.as_bytes().to_vec()))); | ||||
|     Ok(()) | ||||
| } | ||||
|  | ||||
|  | ||||
| /// Lock the session (zeroize password and session) | ||||
| #[wasm_bindgen] | ||||
| pub fn lock_session() { | ||||
|     SESSION_MANAGER.with(|cell| *cell.borrow_mut() = None); | ||||
|     SESSION_PASSWORD.with(|cell| *cell.borrow_mut() = None); | ||||
| } | ||||
|  | ||||
| // ===================== | ||||
| // Keypair Management | ||||
| // ===================== | ||||
|  | ||||
| /// Get all keypairs from the current session | ||||
| /// Returns an array of keypair objects with id, type, and metadata | ||||
| // #[wasm_bindgen] | ||||
| // pub async fn list_keypairs() -> Result<JsValue, JsValue> { | ||||
| //     // [Function body commented out to resolve duplicate symbol error] | ||||
| //     // (Original implementation moved to keypair_bindings.rs) | ||||
| //     unreachable!("This function is disabled. Use the export from keypair_bindings.rs."); | ||||
| // } | ||||
|  | ||||
| // [Function body commented out to resolve duplicate symbol error] | ||||
| // } | ||||
|  | ||||
|  | ||||
| /// Select keypair for the session | ||||
| #[wasm_bindgen] | ||||
| pub fn select_keypair(key_id: &str) -> Result<(), JsValue> { | ||||
|     let mut result = Err(JsValue::from_str("Session not initialized")); | ||||
|     SESSION_MANAGER.with(|cell| { | ||||
|         if let Some(session) = cell.borrow_mut().as_mut() { | ||||
|             result = session | ||||
|                 .select_keypair(key_id) | ||||
|                 .map_err(|e| JsValue::from_str(&format!("select_keypair error: {e}"))); | ||||
|         } | ||||
|     }); | ||||
|     result | ||||
| } | ||||
|  | ||||
|  | ||||
| /// List keypairs in the current session's keyspace | ||||
| #[wasm_bindgen] | ||||
| pub async fn list_keypairs() -> Result<JsValue, JsValue> { | ||||
|     SESSION_MANAGER.with(|cell| { | ||||
|         if let Some(session) = cell.borrow().as_ref() { | ||||
|             if let Some(keyspace) = session.current_keyspace() { | ||||
|                 let keypairs = &keyspace.keypairs; | ||||
|                 serde_json::to_string(keypairs) | ||||
|                     .map(|s| JsValue::from_str(&s)) | ||||
|                     .map_err(|e| JsValue::from_str(&format!("Serialization error: {e}"))) | ||||
|             } else { | ||||
|                 Err(JsValue::from_str("No keyspace unlocked")) | ||||
|             } | ||||
|         } else { | ||||
|             Err(JsValue::from_str("Session not initialized")) | ||||
|         } | ||||
|     }) | ||||
| } | ||||
|  | ||||
| /// Add a keypair to the current keyspace | ||||
| #[wasm_bindgen] | ||||
| pub async fn add_keypair( | ||||
|     key_type: Option<String>, | ||||
|     metadata: Option<String>, | ||||
| ) -> Result<JsValue, JsValue> { | ||||
|     use vault::{KeyMetadata, KeyType}; | ||||
|     let password = SESSION_PASSWORD | ||||
|         .with(|pw| pw.borrow().clone()) | ||||
|         .ok_or_else(|| JsValue::from_str("Session password not set"))?; | ||||
|     let (keyspace_name, session_exists) = SESSION_MANAGER.with(|cell| { | ||||
|         if let Some(ref session) = cell.borrow().as_ref() { | ||||
|             let keyspace_name = session.current_keyspace().map(|_| "".to_string()); // TODO: replace with actual keyspace name if available; | ||||
|             (keyspace_name, true) | ||||
|         } else { | ||||
|             (None, false) | ||||
|         } | ||||
|     }); | ||||
|     let keyspace_name = keyspace_name.ok_or_else(|| JsValue::from_str("No keyspace selected"))?; | ||||
|     if !session_exists { | ||||
|         return Err(JsValue::from_str("Session not initialized")); | ||||
|     } | ||||
|     let key_type = key_type | ||||
|         .as_deref() | ||||
|         .map(|s| match s { | ||||
|             "Ed25519" => KeyType::Ed25519, | ||||
|             "Secp256k1" => KeyType::Secp256k1, | ||||
|             _ => KeyType::Secp256k1, | ||||
|         }) | ||||
|         .unwrap_or(KeyType::Secp256k1); | ||||
|     let metadata = match metadata { | ||||
|         Some(ref meta_str) => Some( | ||||
|             serde_json::from_str::<KeyMetadata>(meta_str) | ||||
|                 .map_err(|e| JsValue::from_str(&format!("Invalid metadata: {e}")))?, | ||||
|         ), | ||||
|         None => None, | ||||
|     }; | ||||
|     // Take session out, do async work, then put it back | ||||
|     let mut session_opt = SESSION_MANAGER.with(|cell| cell.borrow_mut().take()); | ||||
|     let session = session_opt.as_mut().ok_or_else(|| JsValue::from_str("Session not initialized"))?; | ||||
|     let key_id = session | ||||
|         .get_vault_mut() | ||||
|         .add_keypair(&keyspace_name, &password, Some(key_type), metadata) | ||||
|         .await | ||||
|         .map_err(|e| JsValue::from_str(&format!("add_keypair error: {e}")))?; | ||||
|     // Put session back | ||||
|     SESSION_MANAGER.with(|cell| *cell.borrow_mut() = Some(session_opt.take().unwrap())); | ||||
|     Ok(JsValue::from_str(&key_id)) | ||||
| } | ||||
|  | ||||
| /// Sign message with current session | ||||
| #[wasm_bindgen] | ||||
| pub async fn sign(message: &[u8]) -> Result<JsValue, JsValue> { | ||||
|     { | ||||
|         // SAFETY: We only use this pointer synchronously within this function, and SESSION_MANAGER outlives this scope. | ||||
|         let session_ptr = | ||||
|             SESSION_MANAGER.with(|cell| cell.borrow().as_ref().map(|s| s as *const _)); | ||||
|         let password_opt = SESSION_PASSWORD.with(|pw| pw.borrow().clone()); | ||||
|         let session: &vault::session::SessionManager<kvstore::wasm::WasmStore> = match session_ptr { | ||||
|             Some(ptr) => unsafe { &*ptr }, | ||||
|             None => return Err(JsValue::from_str("Session not initialized")), | ||||
|         }; | ||||
|         let password = match password_opt { | ||||
|             Some(p) => p, | ||||
|             None => return Err(JsValue::from_str("Session password not set")), | ||||
|         }; | ||||
|         match session.sign(message).await { | ||||
|             Ok(sig_bytes) => { | ||||
|                 let hex_sig = hex::encode(&sig_bytes); | ||||
|                 Ok(JsValue::from_str(&hex_sig)) | ||||
|             } | ||||
|             Err(e) => Err(JsValue::from_str(&format!("Sign error: {e}"))), | ||||
|         } | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user