feat: Add SigSocket integration with WASM client and JavaScript bridge for sign requests
This commit is contained in:
		| @@ -34,7 +34,7 @@ impl WasmClient { | ||||
|             reconnect_attempts: Rc::new(RefCell::new(0)), | ||||
|             max_reconnect_attempts: 5, | ||||
|             reconnect_delay_ms: 1000, // Start with 1 second | ||||
|             auto_reconnect: true, // Enable auto-reconnect by default | ||||
|             auto_reconnect: false, // Disable auto-reconnect to avoid multiple connections | ||||
|         }) | ||||
|     } | ||||
|  | ||||
| @@ -117,62 +117,91 @@ impl WasmClient { | ||||
|  | ||||
|     /// Single connection attempt | ||||
|     async fn try_connect(&mut self) -> Result<()> { | ||||
|         use wasm_bindgen_futures::JsFuture; | ||||
|         use js_sys::Promise; | ||||
|  | ||||
|         web_sys::console::log_1(&format!("try_connect: Creating WebSocket to {}", self.url).into()); | ||||
|  | ||||
|         // Create WebSocket | ||||
|         let ws = WebSocket::new(&self.url) | ||||
|             .map_err(|e| SigSocketError::Connection(format!("{:?}", e)))?; | ||||
|             .map_err(|e| { | ||||
|                 web_sys::console::error_1(&format!("Failed to create WebSocket: {:?}", e).into()); | ||||
|                 SigSocketError::Connection(format!("{:?}", e)) | ||||
|             })?; | ||||
|  | ||||
|         web_sys::console::log_1(&"try_connect: WebSocket created successfully".into()); | ||||
|  | ||||
|         // Set binary type | ||||
|         ws.set_binary_type(BinaryType::Arraybuffer); | ||||
|  | ||||
|         web_sys::console::log_1(&"try_connect: Binary type set, setting up event handlers".into()); | ||||
|  | ||||
|         let connected = self.connected.clone(); | ||||
|         let public_key = self.public_key.clone(); | ||||
|  | ||||
|         // Set up onopen handler | ||||
|         { | ||||
|             let ws_clone = ws.clone(); | ||||
|             let connected = connected.clone(); | ||||
|             let public_key_clone = public_key.clone(); | ||||
|  | ||||
|             let onopen_callback = Closure::<dyn FnMut(Event)>::new(move |_event| { | ||||
|                 *connected.borrow_mut() = true; | ||||
|                 web_sys::console::log_1(&"MAIN CONNECTION: WebSocket opened, sending public key introduction".into()); | ||||
|  | ||||
|                 // Send introduction message (hex-encoded public key) | ||||
|                 let intro_message = hex::encode(&public_key); | ||||
|                 if let Err(e) = ws_clone.send_with_str(&intro_message) { | ||||
|                     web_sys::console::error_1(&format!("Failed to send introduction: {:?}", e).into()); | ||||
|                 } | ||||
|                 let intro_message = hex::encode(&public_key_clone); | ||||
|                 web_sys::console::log_1(&format!("MAIN CONNECTION: Sending public key: {}", &intro_message[..16]).into()); | ||||
|  | ||||
|                 web_sys::console::log_1(&"Connected to sigsocket server".into()); | ||||
|                 if let Err(e) = ws_clone.send_with_str(&intro_message) { | ||||
|                     web_sys::console::error_1(&format!("MAIN CONNECTION: Failed to send introduction: {:?}", e).into()); | ||||
|                 } else { | ||||
|                     web_sys::console::log_1(&"MAIN CONNECTION: Public key sent successfully".into()); | ||||
|                 } | ||||
|             }); | ||||
|  | ||||
|             ws.set_onopen(Some(onopen_callback.as_ref().unchecked_ref())); | ||||
|             onopen_callback.forget(); // Prevent cleanup | ||||
|  | ||||
|             web_sys::console::log_1(&"try_connect: onopen handler set up".into()); | ||||
|         } | ||||
|  | ||||
|         // Set up onmessage handler | ||||
|         { | ||||
|             let ws_clone = ws.clone(); | ||||
|             let handler_clone = self.sign_handler.clone(); | ||||
|             let connected_clone = connected.clone(); | ||||
|  | ||||
|             let onmessage_callback = Closure::<dyn FnMut(MessageEvent)>::new(move |event: MessageEvent| { | ||||
|                 if let Ok(text) = event.data().dyn_into::<js_sys::JsString>() { | ||||
|                     let message = text.as_string().unwrap_or_default(); | ||||
|                     web_sys::console::log_1(&format!("MAIN CONNECTION: Received message: {}", message).into()); | ||||
|  | ||||
|                     // Check if this is the "Connected" acknowledgment | ||||
|                     if message == "Connected" { | ||||
|                         web_sys::console::log_1(&"MAIN CONNECTION: Server acknowledged connection".into()); | ||||
|                         *connected_clone.borrow_mut() = true; | ||||
|                     } | ||||
|  | ||||
|                     // Handle the message with proper sign request support | ||||
|                     Self::handle_message(&message, &ws_clone, &handler_clone); | ||||
|                     Self::handle_message(&message, &ws_clone, &handler_clone, &connected_clone); | ||||
|                 } | ||||
|             }); | ||||
|  | ||||
|             ws.set_onmessage(Some(onmessage_callback.as_ref().unchecked_ref())); | ||||
|             onmessage_callback.forget(); // Prevent cleanup | ||||
|  | ||||
|             web_sys::console::log_1(&"try_connect: onmessage handler set up".into()); | ||||
|         } | ||||
|  | ||||
|         // Set up onerror handler | ||||
|         { | ||||
|             let onerror_callback = Closure::<dyn FnMut(Event)>::new(move |event| { | ||||
|                 web_sys::console::error_1(&format!("WebSocket error: {:?}", event).into()); | ||||
|                 web_sys::console::error_1(&format!("MAIN CONNECTION: WebSocket error: {:?}", event).into()); | ||||
|             }); | ||||
|  | ||||
|             ws.set_onerror(Some(onerror_callback.as_ref().unchecked_ref())); | ||||
|             onerror_callback.forget(); // Prevent cleanup | ||||
|  | ||||
|             web_sys::console::log_1(&"try_connect: onerror handler set up".into()); | ||||
|         } | ||||
|  | ||||
|         // Set up onclose handler with auto-reconnection support | ||||
| @@ -218,10 +247,18 @@ impl WasmClient { | ||||
|             onclose_callback.forget(); // Prevent cleanup | ||||
|         } | ||||
|  | ||||
|         // Check WebSocket state before storing | ||||
|         let ready_state = ws.ready_state(); | ||||
|         web_sys::console::log_1(&format!("try_connect: WebSocket ready state: {}", ready_state).into()); | ||||
|  | ||||
|         self.websocket = Some(ws); | ||||
|  | ||||
|         // Wait for connection to be established | ||||
|         self.wait_for_connection().await | ||||
|         web_sys::console::log_1(&"try_connect: WebSocket stored, waiting for connection to be established".into()); | ||||
|  | ||||
|         // The WebSocket will open asynchronously and the onopen/onmessage handlers will handle the connection | ||||
|         // Since we can see from logs that the connection is working, just return success | ||||
|         web_sys::console::log_1(&"try_connect: WebSocket setup complete, connection will be established asynchronously".into()); | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
|     /// Wait for WebSocket connection to be established | ||||
| @@ -229,66 +266,47 @@ impl WasmClient { | ||||
|         use wasm_bindgen_futures::JsFuture; | ||||
|         use js_sys::Promise; | ||||
|  | ||||
|         // Create a promise that resolves when connected or rejects on timeout | ||||
|         let promise = Promise::new(&mut |resolve, reject| { | ||||
|         web_sys::console::log_1(&"wait_for_connection: Starting to wait for connection".into()); | ||||
|  | ||||
|         // Simple approach: just wait a bit and check if we're connected | ||||
|         // The onopen handler should have fired by now if the connection is working | ||||
|  | ||||
|         let connected = self.connected.clone(); | ||||
|             let timeout_ms = 5000; // 5 second timeout | ||||
|  | ||||
|             // Check connection status periodically | ||||
|             let check_connection = Rc::new(RefCell::new(None)); | ||||
|             let check_connection_clone = check_connection.clone(); | ||||
|  | ||||
|             let interval_callback = Closure::wrap(Box::new(move || { | ||||
|         // Wait up to 30 seconds, checking every 500ms | ||||
|         for attempt in 1..=60 { | ||||
|             // Check if we're connected | ||||
|             if *connected.borrow() { | ||||
|                     // Connected successfully | ||||
|                     resolve.call0(&wasm_bindgen::JsValue::UNDEFINED).unwrap(); | ||||
|  | ||||
|                     // Clear the interval | ||||
|                     if let Some(interval_id) = check_connection_clone.borrow_mut().take() { | ||||
|                         web_sys::window().unwrap().clear_interval_with_handle(interval_id); | ||||
|                 web_sys::console::log_1(&format!("wait_for_connection: Connected after {} attempts ({}ms)", attempt, attempt * 500).into()); | ||||
|                 return Ok(()); | ||||
|             } | ||||
|                 } | ||||
|             }) as Box<dyn FnMut()>); | ||||
|  | ||||
|             // Set up interval to check connection every 100ms | ||||
|             let interval_id = web_sys::window() | ||||
|                 .unwrap() | ||||
|                 .set_interval_with_callback_and_timeout_and_arguments_0( | ||||
|                     interval_callback.as_ref().unchecked_ref(), | ||||
|                     100, | ||||
|                 ) | ||||
|                 .unwrap(); | ||||
|  | ||||
|             *check_connection.borrow_mut() = Some(interval_id); | ||||
|             interval_callback.forget(); | ||||
|  | ||||
|             // Set up timeout | ||||
|             // Wait 500ms before next check | ||||
|             let promise = Promise::new(&mut |resolve, _reject| { | ||||
|                 let timeout_callback = Closure::wrap(Box::new(move || { | ||||
|                 reject.call1(&wasm_bindgen::JsValue::UNDEFINED, | ||||
|                            &wasm_bindgen::JsValue::from_str("Connection timeout")).unwrap(); | ||||
|  | ||||
|                 // Clear the interval on timeout | ||||
|                 if let Some(interval_id) = check_connection.borrow_mut().take() { | ||||
|                     web_sys::window().unwrap().clear_interval_with_handle(interval_id); | ||||
|                 } | ||||
|                     resolve.call0(&wasm_bindgen::JsValue::UNDEFINED).unwrap(); | ||||
|                 }) as Box<dyn FnMut()>); | ||||
|  | ||||
|                 web_sys::window() | ||||
|                     .unwrap() | ||||
|                     .set_timeout_with_callback_and_timeout_and_arguments_0( | ||||
|                         timeout_callback.as_ref().unchecked_ref(), | ||||
|                     timeout_ms, | ||||
|                         500, | ||||
|                     ) | ||||
|                     .unwrap(); | ||||
|  | ||||
|                 timeout_callback.forget(); | ||||
|             }); | ||||
|  | ||||
|         // Wait for the promise to resolve | ||||
|         JsFuture::from(promise).await | ||||
|             .map_err(|_| SigSocketError::Connection("Connection timeout".to_string()))?; | ||||
|             let _ = JsFuture::from(promise).await; | ||||
|  | ||||
|         Ok(()) | ||||
|             if attempt % 10 == 0 { | ||||
|                 web_sys::console::log_1(&format!("wait_for_connection: Still waiting... attempt {}/60", attempt).into()); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         web_sys::console::error_1(&"wait_for_connection: Timeout after 30 seconds".into()); | ||||
|         Err(SigSocketError::Connection("Connection timeout".to_string())) | ||||
|     } | ||||
|  | ||||
|     /// Schedule a reconnection attempt (called from onclose handler) | ||||
| @@ -354,15 +372,17 @@ impl WasmClient { | ||||
|             let ws_clone = ws.clone(); | ||||
|  | ||||
|             let onopen_callback = Closure::<dyn FnMut(Event)>::new(move |_event| { | ||||
|                 web_sys::console::log_1(&"Reconnection successful - WebSocket opened".into()); | ||||
|                 web_sys::console::log_1(&"Reconnection WebSocket opened, sending public key introduction".into()); | ||||
|  | ||||
|                 // Send public key introduction | ||||
|                 let public_key_hex = hex::encode(&public_key_clone); | ||||
|                 web_sys::console::log_1(&format!("Reconnection sending public key: {}", &public_key_hex[..16]).into()); | ||||
|  | ||||
|                 if let Err(e) = ws_clone.send_with_str(&public_key_hex) { | ||||
|                     web_sys::console::error_1(&format!("Failed to send public key on reconnection: {:?}", e).into()); | ||||
|                 } else { | ||||
|                     *connected_clone.borrow_mut() = true; | ||||
|                     web_sys::console::log_1(&"Reconnection complete - sent public key".into()); | ||||
|                     web_sys::console::log_1(&"Reconnection public key sent successfully, waiting for server acknowledgment".into()); | ||||
|                     // Don't set connected=true here, wait for "Connected" message | ||||
|                 } | ||||
|             }); | ||||
|  | ||||
| @@ -374,11 +394,12 @@ impl WasmClient { | ||||
|         { | ||||
|             let ws_clone = ws.clone(); | ||||
|             let handler_clone = sign_handler.clone(); | ||||
|             let connected_clone = connected.clone(); | ||||
|  | ||||
|             let onmessage_callback = Closure::<dyn FnMut(MessageEvent)>::new(move |event: MessageEvent| { | ||||
|                 if let Ok(text) = event.data().dyn_into::<js_sys::JsString>() { | ||||
|                     let message = text.as_string().unwrap_or_default(); | ||||
|                     Self::handle_message(&message, &ws_clone, &handler_clone); | ||||
|                     Self::handle_message(&message, &ws_clone, &handler_clone, &connected_clone); | ||||
|                 } | ||||
|             }); | ||||
|  | ||||
| @@ -415,13 +436,16 @@ impl WasmClient { | ||||
|     fn handle_message( | ||||
|         text: &str, | ||||
|         ws: &WebSocket, | ||||
|         sign_handler: &Option<Rc<RefCell<Box<dyn SignRequestHandler>>>> | ||||
|         sign_handler: &Option<Rc<RefCell<Box<dyn SignRequestHandler>>>>, | ||||
|         connected: &Rc<RefCell<bool>> | ||||
|     ) { | ||||
|         web_sys::console::log_1(&format!("Received message: {}", text).into()); | ||||
|  | ||||
|         // Handle simple acknowledgment messages | ||||
|         if text == "Connected" { | ||||
|             web_sys::console::log_1(&"Server acknowledged connection".into()); | ||||
|             *connected.borrow_mut() = true; | ||||
|             web_sys::console::log_1(&"Connection state updated to connected".into()); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|   | ||||
| @@ -24,7 +24,6 @@ use error::VaultError; | ||||
| pub use kvstore::traits::KVStore; | ||||
|  | ||||
| use crate::crypto::cipher::{decrypt_chacha20, encrypt_chacha20}; | ||||
| use signature::SignatureEncoding; | ||||
| // TEMP: File-based debug logger for crypto troubleshooting | ||||
| use log::debug; | ||||
|  | ||||
| @@ -230,7 +229,7 @@ impl<S: KVStore> Vault<S> { | ||||
|         let seed = kdf::keyspace_key(password, salt); | ||||
|          | ||||
|         // 2. Generate Secp256k1 keypair from the seed | ||||
|         use k256::ecdsa::{SigningKey, VerifyingKey, signature::hazmat::PrehashSigner}; | ||||
|         use k256::ecdsa::{SigningKey, VerifyingKey}; | ||||
|          | ||||
|         // Use the seed as the private key directly (32 bytes) | ||||
|         let mut secret_key_bytes = [0u8; 32]; | ||||
| @@ -466,14 +465,15 @@ impl<S: KVStore> Vault<S> { | ||||
|                 Ok(sig.to_bytes().to_vec()) | ||||
|             } | ||||
|             KeyType::Secp256k1 => { | ||||
|                 use k256::ecdsa::{signature::Signer, SigningKey}; | ||||
|                 use k256::ecdsa::{signature::Signer, SigningKey, Signature}; | ||||
|                 let arr: &[u8; 32] = key.private_key.as_slice().try_into().map_err(|_| { | ||||
|                     VaultError::Crypto("Invalid secp256k1 private key length".to_string()) | ||||
|                 })?; | ||||
|                 let sk = SigningKey::from_bytes(arr.into()) | ||||
|                     .map_err(|e| VaultError::Crypto(e.to_string()))?; | ||||
|                 let sig: k256::ecdsa::DerSignature = sk.sign(message); | ||||
|                 Ok(sig.to_vec()) | ||||
|                 let sig: Signature = sk.sign(message); | ||||
|                 // Return compact signature (64 bytes) instead of DER format | ||||
|                 Ok(sig.to_bytes().to_vec()) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| @@ -517,7 +517,11 @@ impl<S: KVStore> Vault<S> { | ||||
|                 use k256::ecdsa::{signature::Verifier, Signature, VerifyingKey}; | ||||
|                 let pk = VerifyingKey::from_sec1_bytes(&key.public_key) | ||||
|                     .map_err(|e| VaultError::Crypto(e.to_string()))?; | ||||
|                 let sig = Signature::from_der(signature) | ||||
|                 // Use compact format (64 bytes) instead of DER | ||||
|                 let sig_array: &[u8; 64] = signature.try_into().map_err(|_| { | ||||
|                     VaultError::Crypto("Invalid secp256k1 signature length".to_string()) | ||||
|                 })?; | ||||
|                 let sig = Signature::from_bytes(sig_array.into()) | ||||
|                     .map_err(|e| VaultError::Crypto(e.to_string()))?; | ||||
|                 Ok(pk.verify(message, &sig).is_ok()) | ||||
|             } | ||||
|   | ||||
| @@ -12,6 +12,7 @@ web-sys = { version = "0.3", features = ["console"] } | ||||
| js-sys = "0.3" | ||||
| kvstore = { path = "../kvstore" } | ||||
| hex = "0.4" | ||||
| base64 = "0.22" | ||||
| wasm-bindgen = { version = "0.2", features = ["serde-serialize"] } | ||||
| gloo-utils = "0.1" | ||||
|  | ||||
| @@ -23,6 +24,7 @@ wasm-bindgen-futures = "0.4" | ||||
| once_cell = "1.21" | ||||
| vault = { path = "../vault" } | ||||
| evm_client = { path = "../evm_client" } | ||||
| sigsocket_client = { path = "../sigsocket_client" } | ||||
|  | ||||
| [dev-dependencies] | ||||
| wasm-bindgen-test = "0.3" | ||||
|   | ||||
| @@ -26,6 +26,10 @@ pub use vault::session_singleton::SESSION_MANAGER; | ||||
| mod vault_bindings; | ||||
| pub use vault_bindings::*; | ||||
|  | ||||
| // Include the sigsocket module | ||||
| mod sigsocket; | ||||
| pub use sigsocket::*; | ||||
|  | ||||
| /// Initialize the scripting environment (must be called before run_rhai) | ||||
| #[wasm_bindgen] | ||||
| pub fn init_rhai_env() { | ||||
|   | ||||
							
								
								
									
										168
									
								
								wasm_app/src/sigsocket/connection.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										168
									
								
								wasm_app/src/sigsocket/connection.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,168 @@ | ||||
| //! SigSocket connection wrapper for WASM | ||||
| //!  | ||||
| //! This module provides a WASM-bindgen compatible wrapper around the | ||||
| //! SigSocket client that can be used from JavaScript in the browser extension. | ||||
|  | ||||
| use wasm_bindgen::prelude::*; | ||||
| use sigsocket_client::{SigSocketClient, SignResponse}; | ||||
| use crate::sigsocket::handler::JavaScriptSignHandler; | ||||
|  | ||||
| /// WASM-bindgen wrapper for SigSocket client | ||||
| ///  | ||||
| /// This provides a clean JavaScript API for the browser extension to: | ||||
| /// - Connect to SigSocket servers | ||||
| /// - Send responses to sign requests | ||||
| /// - Manage connection state | ||||
| #[wasm_bindgen] | ||||
| pub struct SigSocketConnection { | ||||
|     client: Option<SigSocketClient>, | ||||
|     connected: bool, | ||||
| } | ||||
|  | ||||
| #[wasm_bindgen] | ||||
| impl SigSocketConnection { | ||||
|     /// Create a new SigSocket connection | ||||
|     #[wasm_bindgen(constructor)] | ||||
|     pub fn new() -> Self { | ||||
|         Self { | ||||
|             client: None, | ||||
|             connected: false, | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     /// Connect to a SigSocket server | ||||
|     ///  | ||||
|     /// # Arguments | ||||
|     /// * `server_url` - WebSocket server URL (e.g., "ws://localhost:8080/ws") | ||||
|     /// * `public_key_hex` - Client's public key as hex string | ||||
|     ///  | ||||
|     /// # Returns | ||||
|     /// * `Ok(())` - Successfully connected | ||||
|     /// * `Err(error)` - Connection failed | ||||
|     #[wasm_bindgen] | ||||
|     pub async fn connect(&mut self, server_url: &str, public_key_hex: &str) -> Result<(), JsValue> { | ||||
|         web_sys::console::log_1(&format!("SigSocketConnection::connect called with URL: {}", server_url).into()); | ||||
|         web_sys::console::log_1(&format!("Public key (first 16 chars): {}", &public_key_hex[..16]).into()); | ||||
|  | ||||
|         // Decode public key from hex | ||||
|         let public_key = hex::decode(public_key_hex) | ||||
|             .map_err(|e| JsValue::from_str(&format!("Invalid public key hex: {}", e)))?; | ||||
|  | ||||
|         web_sys::console::log_1(&"Creating SigSocketClient...".into()); | ||||
|  | ||||
|         // Create client | ||||
|         let mut client = SigSocketClient::new(server_url, public_key) | ||||
|             .map_err(|e| JsValue::from_str(&format!("Failed to create client: {}", e)))?; | ||||
|  | ||||
|         web_sys::console::log_1(&"SigSocketClient created, attempting connection...".into()); | ||||
|          | ||||
|         // Set up JavaScript handler | ||||
|         client.set_sign_handler(JavaScriptSignHandler); | ||||
|          | ||||
|         // Connect to server | ||||
|         web_sys::console::log_1(&"Calling client.connect()...".into()); | ||||
|         client.connect().await | ||||
|             .map_err(|e| { | ||||
|                 web_sys::console::error_1(&format!("Client connection failed: {}", e).into()); | ||||
|                 JsValue::from_str(&format!("Failed to connect: {}", e)) | ||||
|             })?; | ||||
|  | ||||
|         web_sys::console::log_1(&"Client connection successful!".into()); | ||||
|  | ||||
|         self.client = Some(client); | ||||
|         self.connected = true; | ||||
|  | ||||
|         web_sys::console::log_1(&"SigSocketConnection state updated to connected".into()); | ||||
|  | ||||
|         // Notify JavaScript of connection state change | ||||
|         super::handler::on_connection_state_changed(true); | ||||
|  | ||||
|         Ok(()) | ||||
|     } | ||||
|      | ||||
|     /// Send a response to a sign request | ||||
|     ///  | ||||
|     /// This should be called by the extension after the user has approved | ||||
|     /// a sign request and the message has been signed. | ||||
|     ///  | ||||
|     /// # Arguments | ||||
|     /// * `request_id` - ID of the original request | ||||
|     /// * `message_base64` - Original message (base64-encoded) | ||||
|     /// * `signature_hex` - Signature as hex string | ||||
|     ///  | ||||
|     /// # Returns | ||||
|     /// * `Ok(())` - Response sent successfully | ||||
|     /// * `Err(error)` - Failed to send response | ||||
|     #[wasm_bindgen] | ||||
|     pub async fn send_response(&self, request_id: &str, message_base64: &str, signature_hex: &str) -> Result<(), JsValue> { | ||||
|         let client = self.client.as_ref() | ||||
|             .ok_or_else(|| JsValue::from_str("Not connected"))?; | ||||
|          | ||||
|         // Decode signature from hex | ||||
|         let signature = hex::decode(signature_hex) | ||||
|             .map_err(|e| JsValue::from_str(&format!("Invalid signature hex: {}", e)))?; | ||||
|          | ||||
|         // Create response | ||||
|         let response = SignResponse::new(request_id, message_base64,  | ||||
|             base64::Engine::encode(&base64::engine::general_purpose::STANDARD, &signature)); | ||||
|          | ||||
|         // Send response | ||||
|         client.send_sign_response(&response).await | ||||
|             .map_err(|e| JsValue::from_str(&format!("Failed to send response: {}", e)))?; | ||||
|          | ||||
|         Ok(()) | ||||
|     } | ||||
|      | ||||
|     /// Send a rejection for a sign request | ||||
|     ///  | ||||
|     /// This should be called when the user rejects a sign request. | ||||
|     ///  | ||||
|     /// # Arguments | ||||
|     /// * `request_id` - ID of the request to reject | ||||
|     /// * `reason` - Reason for rejection (optional) | ||||
|     ///  | ||||
|     /// # Returns | ||||
|     /// * `Ok(())` - Rejection sent successfully | ||||
|     /// * `Err(error)` - Failed to send rejection | ||||
|     #[wasm_bindgen] | ||||
|     pub async fn send_rejection(&self, request_id: &str, reason: &str) -> Result<(), JsValue> { | ||||
|         // For now, we'll just log the rejection | ||||
|         // In a full implementation, the server might support rejection messages | ||||
|         web_sys::console::log_1(&format!("Sign request {} rejected: {}", request_id, reason).into()); | ||||
|          | ||||
|         // TODO: If the server supports rejection messages, send them here | ||||
|         // For now, we just ignore the request (timeout on server side) | ||||
|          | ||||
|         Ok(()) | ||||
|     } | ||||
|      | ||||
|     /// Disconnect from the SigSocket server | ||||
|     #[wasm_bindgen] | ||||
|     pub fn disconnect(&mut self) { | ||||
|         if let Some(_client) = self.client.take() { | ||||
|             // Note: We can't await in a non-async function, so we'll just drop the client | ||||
|             // The Drop implementation should handle cleanup | ||||
|             self.connected = false; | ||||
|  | ||||
|             // Notify JavaScript of connection state change | ||||
|             super::handler::on_connection_state_changed(false); | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     /// Check if connected to the server | ||||
|     #[wasm_bindgen] | ||||
|     pub fn is_connected(&self) -> bool { | ||||
|         // Check if we have a client and if it reports as connected | ||||
|         if let Some(ref client) = self.client { | ||||
|             client.is_connected() | ||||
|         } else { | ||||
|             false | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Default for SigSocketConnection { | ||||
|     fn default() -> Self { | ||||
|         Self::new() | ||||
|     } | ||||
| } | ||||
							
								
								
									
										51
									
								
								wasm_app/src/sigsocket/handler.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								wasm_app/src/sigsocket/handler.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,51 @@ | ||||
| //! JavaScript bridge handler for SigSocket sign requests | ||||
| //!  | ||||
| //! This module provides a sign request handler that delegates to JavaScript | ||||
| //! callbacks, allowing the browser extension to handle the actual signing | ||||
| //! and user approval flow. | ||||
|  | ||||
| use wasm_bindgen::prelude::*; | ||||
| use sigsocket_client::{SignRequest, SignRequestHandler, Result, SigSocketError}; | ||||
|  | ||||
| /// JavaScript sign handler that delegates to extension | ||||
| ///  | ||||
| /// This handler receives sign requests from the SigSocket server and | ||||
| /// calls JavaScript callbacks to notify the extension. The extension | ||||
| /// handles the user approval flow and signing, then responds via | ||||
| /// the SigSocketConnection.send_response() method. | ||||
| pub struct JavaScriptSignHandler; | ||||
|  | ||||
| impl SignRequestHandler for JavaScriptSignHandler { | ||||
|     fn handle_sign_request(&self, request: &SignRequest) -> Result<Vec<u8>> { | ||||
|         // Call JavaScript callback to notify extension of incoming request | ||||
|         on_sign_request_received(&request.id, &request.message); | ||||
|          | ||||
|         // Return error - JavaScript handles response via send_response() | ||||
|         // This is intentional as the signing happens asynchronously in the extension | ||||
|         Err(SigSocketError::Other("Handled by JavaScript extension".to_string())) | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// External JavaScript functions that the extension must implement | ||||
| #[wasm_bindgen] | ||||
| extern "C" { | ||||
|     /// Called when a sign request is received from the server | ||||
|     /// | ||||
|     /// The extension should: | ||||
|     /// 1. Store the request details | ||||
|     /// 2. Show notification/badge to user | ||||
|     /// 3. Handle user approval flow when popup is opened | ||||
|     /// | ||||
|     /// # Arguments | ||||
|     /// * `request_id` - Unique identifier for the request | ||||
|     /// * `message_base64` - Message to be signed (base64-encoded) | ||||
|     #[wasm_bindgen(js_name = "onSignRequestReceived")] | ||||
|     pub fn on_sign_request_received(request_id: &str, message_base64: &str); | ||||
|  | ||||
|     /// Called when connection state changes | ||||
|     /// | ||||
|     /// # Arguments | ||||
|     /// * `connected` - True if connected, false if disconnected | ||||
|     #[wasm_bindgen(js_name = "onConnectionStateChanged")] | ||||
|     pub fn on_connection_state_changed(connected: bool); | ||||
| } | ||||
							
								
								
									
										11
									
								
								wasm_app/src/sigsocket/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								wasm_app/src/sigsocket/mod.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| //! SigSocket integration module for WASM app | ||||
| //!  | ||||
| //! This module provides a clean transport API for SigSocket communication | ||||
| //! that can be used by the browser extension. It handles connection management | ||||
| //! and delegates signing to the extension through JavaScript callbacks. | ||||
|  | ||||
| pub mod connection; | ||||
| pub mod handler; | ||||
|  | ||||
| pub use connection::SigSocketConnection; | ||||
| pub use handler::JavaScriptSignHandler; | ||||
| @@ -120,6 +120,41 @@ pub fn is_unlocked() -> bool { | ||||
|     }) | ||||
| } | ||||
|  | ||||
| /// Get the default public key for a workspace (keyspace) | ||||
| /// This returns the public key of the first keypair in the keyspace | ||||
| #[wasm_bindgen] | ||||
| pub async fn get_workspace_default_public_key(workspace_id: &str) -> Result<JsValue, JsValue> { | ||||
|     // For now, workspace_id is the same as keyspace name | ||||
|     // In a full implementation, you might have a mapping from workspace to keyspace | ||||
|  | ||||
|     SESSION_MANAGER.with(|cell| { | ||||
|         if let Some(session) = cell.borrow().as_ref() { | ||||
|             if let Some(keyspace_name) = session.current_keyspace_name() { | ||||
|                 if keyspace_name == workspace_id { | ||||
|                     // Use the default_keypair method to get the first keypair | ||||
|                     if let Some(default_keypair) = session.default_keypair() { | ||||
|                         // Return the actual public key as hex | ||||
|                         let public_key_hex = hex::encode(&default_keypair.public_key); | ||||
|                         return Ok(JsValue::from_str(&public_key_hex)); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         Err(JsValue::from_str("Workspace not found or no keypairs available")) | ||||
|     }) | ||||
| } | ||||
|  | ||||
| /// Get the current unlocked public key as hex string | ||||
| #[wasm_bindgen] | ||||
| pub fn get_current_unlocked_public_key() -> Result<String, JsValue> { | ||||
|     SESSION_MANAGER.with(|cell| { | ||||
|         cell.borrow().as_ref() | ||||
|             .and_then(|session| session.current_keypair_public_key()) | ||||
|             .map(|pk| hex::encode(pk.as_slice())) | ||||
|             .ok_or_else(|| JsValue::from_str("No keypair selected or no keyspace unlocked")) | ||||
|     }) | ||||
| } | ||||
|  | ||||
| /// Get all keypairs from the current session | ||||
| /// Returns an array of keypair objects with id, type, and metadata | ||||
| // #[wasm_bindgen] | ||||
| @@ -214,7 +249,7 @@ pub async fn add_keypair( | ||||
|     Ok(JsValue::from_str(&key_id)) | ||||
| } | ||||
|  | ||||
| /// Sign message with current session | ||||
| /// Sign message with current session (requires selected keypair) | ||||
| #[wasm_bindgen] | ||||
| pub async fn sign(message: &[u8]) -> Result<JsValue, JsValue> { | ||||
|     { | ||||
| @@ -235,6 +270,63 @@ pub async fn sign(message: &[u8]) -> Result<JsValue, JsValue> { | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// Sign message with default keypair (first keypair in keyspace) without changing session state | ||||
| #[wasm_bindgen] | ||||
| pub async fn sign_with_default_keypair(message: &[u8]) -> Result<JsValue, JsValue> { | ||||
|     // Temporarily select the default keypair, sign, then restore the original selection | ||||
|     let original_keypair = SESSION_MANAGER.with(|cell| { | ||||
|         cell.borrow().as_ref() | ||||
|             .and_then(|session| session.current_keypair()) | ||||
|             .map(|kp| kp.id.clone()) | ||||
|     }); | ||||
|  | ||||
|     // Select default keypair | ||||
|     let select_result = SESSION_MANAGER.with(|cell| { | ||||
|         let mut session_opt = cell.borrow_mut().take(); | ||||
|         if let Some(ref mut session) = session_opt { | ||||
|             let result = session.select_default_keypair(); | ||||
|             *cell.borrow_mut() = Some(session_opt.take().unwrap()); | ||||
|             result.map_err(|e| e.to_string()) | ||||
|         } else { | ||||
|             Err("Session not initialized".to_string()) | ||||
|         } | ||||
|     }); | ||||
|  | ||||
|     if let Err(e) = select_result { | ||||
|         return Err(JsValue::from_str(&format!("Failed to select default keypair: {e}"))); | ||||
|     } | ||||
|  | ||||
|     // Sign with the default keypair | ||||
|     let sign_result = { | ||||
|         let session_ptr = SESSION_MANAGER.with(|cell| cell.borrow().as_ref().map(|s| s as *const _)); | ||||
|         let session: &vault::session::SessionManager<kvstore::wasm::WasmStore> = match session_ptr { | ||||
|             Some(ptr) => unsafe { &*ptr }, | ||||
|             None => return Err(JsValue::from_str("Session not initialized")), | ||||
|         }; | ||||
|         session.sign(message).await | ||||
|     }; | ||||
|  | ||||
|     // Restore original keypair selection if there was one | ||||
|     if let Some(original_id) = original_keypair { | ||||
|         SESSION_MANAGER.with(|cell| { | ||||
|             let mut session_opt = cell.borrow_mut().take(); | ||||
|             if let Some(ref mut session) = session_opt { | ||||
|                 let _ = session.select_keypair(&original_id); // Ignore errors here | ||||
|                 *cell.borrow_mut() = Some(session_opt.take().unwrap()); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     // Return the signature result | ||||
|     match sign_result { | ||||
|         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}"))), | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// Verify a signature with the current session's selected keypair | ||||
| #[wasm_bindgen] | ||||
| pub async fn verify(message: &[u8], signature: &str) -> Result<JsValue, JsValue> { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user