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)), |             reconnect_attempts: Rc::new(RefCell::new(0)), | ||||||
|             max_reconnect_attempts: 5, |             max_reconnect_attempts: 5, | ||||||
|             reconnect_delay_ms: 1000, // Start with 1 second |             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 |     /// Single connection attempt | ||||||
|     async fn try_connect(&mut self) -> Result<()> { |     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 |         // Create WebSocket | ||||||
|         let ws = WebSocket::new(&self.url) |         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 |         // Set binary type | ||||||
|         ws.set_binary_type(BinaryType::Arraybuffer); |         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 connected = self.connected.clone(); | ||||||
|         let public_key = self.public_key.clone(); |         let public_key = self.public_key.clone(); | ||||||
|  |  | ||||||
|         // Set up onopen handler |         // Set up onopen handler | ||||||
|         { |         { | ||||||
|             let ws_clone = ws.clone(); |             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| { |             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) |                 // Send introduction message (hex-encoded public key) | ||||||
|                 let intro_message = hex::encode(&public_key); |                 let intro_message = hex::encode(&public_key_clone); | ||||||
|  |                 web_sys::console::log_1(&format!("MAIN CONNECTION: Sending public key: {}", &intro_message[..16]).into()); | ||||||
|  |  | ||||||
|                 if let Err(e) = ws_clone.send_with_str(&intro_message) { |                 if let Err(e) = ws_clone.send_with_str(&intro_message) { | ||||||
|                     web_sys::console::error_1(&format!("Failed to send introduction: {:?}", e).into()); |                     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()); | ||||||
|                 } |                 } | ||||||
|                  |  | ||||||
|                 web_sys::console::log_1(&"Connected to sigsocket server".into()); |  | ||||||
|             }); |             }); | ||||||
|              |  | ||||||
|             ws.set_onopen(Some(onopen_callback.as_ref().unchecked_ref())); |             ws.set_onopen(Some(onopen_callback.as_ref().unchecked_ref())); | ||||||
|             onopen_callback.forget(); // Prevent cleanup |             onopen_callback.forget(); // Prevent cleanup | ||||||
|  |  | ||||||
|  |             web_sys::console::log_1(&"try_connect: onopen handler set up".into()); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         // Set up onmessage handler |         // Set up onmessage handler | ||||||
|         { |         { | ||||||
|             let ws_clone = ws.clone(); |             let ws_clone = ws.clone(); | ||||||
|             let handler_clone = self.sign_handler.clone(); |             let handler_clone = self.sign_handler.clone(); | ||||||
|  |             let connected_clone = connected.clone(); | ||||||
|  |  | ||||||
|             let onmessage_callback = Closure::<dyn FnMut(MessageEvent)>::new(move |event: MessageEvent| { |             let onmessage_callback = Closure::<dyn FnMut(MessageEvent)>::new(move |event: MessageEvent| { | ||||||
|                 if let Ok(text) = event.data().dyn_into::<js_sys::JsString>() { |                 if let Ok(text) = event.data().dyn_into::<js_sys::JsString>() { | ||||||
|                     let message = text.as_string().unwrap_or_default(); |                     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 |                     // 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())); |             ws.set_onmessage(Some(onmessage_callback.as_ref().unchecked_ref())); | ||||||
|             onmessage_callback.forget(); // Prevent cleanup |             onmessage_callback.forget(); // Prevent cleanup | ||||||
|  |  | ||||||
|  |             web_sys::console::log_1(&"try_connect: onmessage handler set up".into()); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         // Set up onerror handler |         // Set up onerror handler | ||||||
|         { |         { | ||||||
|             let onerror_callback = Closure::<dyn FnMut(Event)>::new(move |event| { |             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())); |             ws.set_onerror(Some(onerror_callback.as_ref().unchecked_ref())); | ||||||
|             onerror_callback.forget(); // Prevent cleanup |             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 |         // Set up onclose handler with auto-reconnection support | ||||||
| @@ -218,10 +247,18 @@ impl WasmClient { | |||||||
|             onclose_callback.forget(); // Prevent cleanup |             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); |         self.websocket = Some(ws); | ||||||
|  |  | ||||||
|         // Wait for connection to be established |         web_sys::console::log_1(&"try_connect: WebSocket stored, waiting for connection to be established".into()); | ||||||
|         self.wait_for_connection().await |  | ||||||
|  |         // 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 |     /// Wait for WebSocket connection to be established | ||||||
| @@ -229,66 +266,47 @@ impl WasmClient { | |||||||
|         use wasm_bindgen_futures::JsFuture; |         use wasm_bindgen_futures::JsFuture; | ||||||
|         use js_sys::Promise; |         use js_sys::Promise; | ||||||
|  |  | ||||||
|         // Create a promise that resolves when connected or rejects on timeout |         web_sys::console::log_1(&"wait_for_connection: Starting to wait for connection".into()); | ||||||
|         let promise = Promise::new(&mut |resolve, reject| { |  | ||||||
|             let connected = self.connected.clone(); |  | ||||||
|             let timeout_ms = 5000; // 5 second timeout |  | ||||||
|  |  | ||||||
|             // Check connection status periodically |         // Simple approach: just wait a bit and check if we're connected | ||||||
|             let check_connection = Rc::new(RefCell::new(None)); |         // The onopen handler should have fired by now if the connection is working | ||||||
|             let check_connection_clone = check_connection.clone(); |  | ||||||
|  |  | ||||||
|             let interval_callback = Closure::wrap(Box::new(move || { |         let connected = self.connected.clone(); | ||||||
|                 if *connected.borrow() { |  | ||||||
|                     // Connected successfully |         // Wait up to 30 seconds, checking every 500ms | ||||||
|  |         for attempt in 1..=60 { | ||||||
|  |             // Check if we're connected | ||||||
|  |             if *connected.borrow() { | ||||||
|  |                 web_sys::console::log_1(&format!("wait_for_connection: Connected after {} attempts ({}ms)", attempt, attempt * 500).into()); | ||||||
|  |                 return Ok(()); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             // Wait 500ms before next check | ||||||
|  |             let promise = Promise::new(&mut |resolve, _reject| { | ||||||
|  |                 let timeout_callback = Closure::wrap(Box::new(move || { | ||||||
|                     resolve.call0(&wasm_bindgen::JsValue::UNDEFINED).unwrap(); |                     resolve.call0(&wasm_bindgen::JsValue::UNDEFINED).unwrap(); | ||||||
|  |                 }) as Box<dyn FnMut()>); | ||||||
|  |  | ||||||
|                     // Clear the interval |                 web_sys::window() | ||||||
|                     if let Some(interval_id) = check_connection_clone.borrow_mut().take() { |                     .unwrap() | ||||||
|                         web_sys::window().unwrap().clear_interval_with_handle(interval_id); |                     .set_timeout_with_callback_and_timeout_and_arguments_0( | ||||||
|                     } |                         timeout_callback.as_ref().unchecked_ref(), | ||||||
|                 } |                         500, | ||||||
|             }) as Box<dyn FnMut()>); |                     ) | ||||||
|  |                     .unwrap(); | ||||||
|  |  | ||||||
|             // Set up interval to check connection every 100ms |                 timeout_callback.forget(); | ||||||
|             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); |             let _ = JsFuture::from(promise).await; | ||||||
|             interval_callback.forget(); |  | ||||||
|  |  | ||||||
|             // Set up timeout |             if attempt % 10 == 0 { | ||||||
|             let timeout_callback = Closure::wrap(Box::new(move || { |                 web_sys::console::log_1(&format!("wait_for_connection: Still waiting... attempt {}/60", attempt).into()); | ||||||
|                 reject.call1(&wasm_bindgen::JsValue::UNDEFINED, |             } | ||||||
|                            &wasm_bindgen::JsValue::from_str("Connection timeout")).unwrap(); |         } | ||||||
|  |  | ||||||
|                 // Clear the interval on timeout |         web_sys::console::error_1(&"wait_for_connection: Timeout after 30 seconds".into()); | ||||||
|                 if let Some(interval_id) = check_connection.borrow_mut().take() { |         Err(SigSocketError::Connection("Connection timeout".to_string())) | ||||||
|                     web_sys::window().unwrap().clear_interval_with_handle(interval_id); |  | ||||||
|                 } |  | ||||||
|             }) 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, |  | ||||||
|                 ) |  | ||||||
|                 .unwrap(); |  | ||||||
|  |  | ||||||
|             timeout_callback.forget(); |  | ||||||
|         }); |  | ||||||
|  |  | ||||||
|         // Wait for the promise to resolve |  | ||||||
|         JsFuture::from(promise).await |  | ||||||
|             .map_err(|_| SigSocketError::Connection("Connection timeout".to_string()))?; |  | ||||||
|  |  | ||||||
|         Ok(()) |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /// Schedule a reconnection attempt (called from onclose handler) |     /// Schedule a reconnection attempt (called from onclose handler) | ||||||
| @@ -354,15 +372,17 @@ impl WasmClient { | |||||||
|             let ws_clone = ws.clone(); |             let ws_clone = ws.clone(); | ||||||
|  |  | ||||||
|             let onopen_callback = Closure::<dyn FnMut(Event)>::new(move |_event| { |             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 |                 // Send public key introduction | ||||||
|                 let public_key_hex = hex::encode(&public_key_clone); |                 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) { |                 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()); |                     web_sys::console::error_1(&format!("Failed to send public key on reconnection: {:?}", e).into()); | ||||||
|                 } else { |                 } else { | ||||||
|                     *connected_clone.borrow_mut() = true; |                     web_sys::console::log_1(&"Reconnection public key sent successfully, waiting for server acknowledgment".into()); | ||||||
|                     web_sys::console::log_1(&"Reconnection complete - sent public key".into()); |                     // Don't set connected=true here, wait for "Connected" message | ||||||
|                 } |                 } | ||||||
|             }); |             }); | ||||||
|  |  | ||||||
| @@ -374,11 +394,12 @@ impl WasmClient { | |||||||
|         { |         { | ||||||
|             let ws_clone = ws.clone(); |             let ws_clone = ws.clone(); | ||||||
|             let handler_clone = sign_handler.clone(); |             let handler_clone = sign_handler.clone(); | ||||||
|  |             let connected_clone = connected.clone(); | ||||||
|  |  | ||||||
|             let onmessage_callback = Closure::<dyn FnMut(MessageEvent)>::new(move |event: MessageEvent| { |             let onmessage_callback = Closure::<dyn FnMut(MessageEvent)>::new(move |event: MessageEvent| { | ||||||
|                 if let Ok(text) = event.data().dyn_into::<js_sys::JsString>() { |                 if let Ok(text) = event.data().dyn_into::<js_sys::JsString>() { | ||||||
|                     let message = text.as_string().unwrap_or_default(); |                     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( |     fn handle_message( | ||||||
|         text: &str, |         text: &str, | ||||||
|         ws: &WebSocket, |         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()); |         web_sys::console::log_1(&format!("Received message: {}", text).into()); | ||||||
|  |  | ||||||
|         // Handle simple acknowledgment messages |         // Handle simple acknowledgment messages | ||||||
|         if text == "Connected" { |         if text == "Connected" { | ||||||
|             web_sys::console::log_1(&"Server acknowledged connection".into()); |             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; |             return; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -24,7 +24,6 @@ use error::VaultError; | |||||||
| pub use kvstore::traits::KVStore; | pub use kvstore::traits::KVStore; | ||||||
|  |  | ||||||
| use crate::crypto::cipher::{decrypt_chacha20, encrypt_chacha20}; | use crate::crypto::cipher::{decrypt_chacha20, encrypt_chacha20}; | ||||||
| use signature::SignatureEncoding; |  | ||||||
| // TEMP: File-based debug logger for crypto troubleshooting | // TEMP: File-based debug logger for crypto troubleshooting | ||||||
| use log::debug; | use log::debug; | ||||||
|  |  | ||||||
| @@ -230,7 +229,7 @@ impl<S: KVStore> Vault<S> { | |||||||
|         let seed = kdf::keyspace_key(password, salt); |         let seed = kdf::keyspace_key(password, salt); | ||||||
|          |          | ||||||
|         // 2. Generate Secp256k1 keypair from the seed |         // 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) |         // Use the seed as the private key directly (32 bytes) | ||||||
|         let mut secret_key_bytes = [0u8; 32]; |         let mut secret_key_bytes = [0u8; 32]; | ||||||
| @@ -466,14 +465,15 @@ impl<S: KVStore> Vault<S> { | |||||||
|                 Ok(sig.to_bytes().to_vec()) |                 Ok(sig.to_bytes().to_vec()) | ||||||
|             } |             } | ||||||
|             KeyType::Secp256k1 => { |             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(|_| { |                 let arr: &[u8; 32] = key.private_key.as_slice().try_into().map_err(|_| { | ||||||
|                     VaultError::Crypto("Invalid secp256k1 private key length".to_string()) |                     VaultError::Crypto("Invalid secp256k1 private key length".to_string()) | ||||||
|                 })?; |                 })?; | ||||||
|                 let sk = SigningKey::from_bytes(arr.into()) |                 let sk = SigningKey::from_bytes(arr.into()) | ||||||
|                     .map_err(|e| VaultError::Crypto(e.to_string()))?; |                     .map_err(|e| VaultError::Crypto(e.to_string()))?; | ||||||
|                 let sig: k256::ecdsa::DerSignature = sk.sign(message); |                 let sig: Signature = sk.sign(message); | ||||||
|                 Ok(sig.to_vec()) |                 // 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}; |                 use k256::ecdsa::{signature::Verifier, Signature, VerifyingKey}; | ||||||
|                 let pk = VerifyingKey::from_sec1_bytes(&key.public_key) |                 let pk = VerifyingKey::from_sec1_bytes(&key.public_key) | ||||||
|                     .map_err(|e| VaultError::Crypto(e.to_string()))?; |                     .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()))?; |                     .map_err(|e| VaultError::Crypto(e.to_string()))?; | ||||||
|                 Ok(pk.verify(message, &sig).is_ok()) |                 Ok(pk.verify(message, &sig).is_ok()) | ||||||
|             } |             } | ||||||
|   | |||||||
| @@ -12,10 +12,11 @@ web-sys = { version = "0.3", features = ["console"] } | |||||||
| js-sys = "0.3" | js-sys = "0.3" | ||||||
| kvstore = { path = "../kvstore" } | kvstore = { path = "../kvstore" } | ||||||
| hex = "0.4" | hex = "0.4" | ||||||
|  | base64 = "0.22" | ||||||
| wasm-bindgen = { version = "0.2", features = ["serde-serialize"] } | wasm-bindgen = { version = "0.2", features = ["serde-serialize"] } | ||||||
| gloo-utils = "0.1" | gloo-utils = "0.1" | ||||||
|  |  | ||||||
| #  | # | ||||||
| serde = { version = "1.0", features = ["derive"] } | serde = { version = "1.0", features = ["derive"] } | ||||||
| serde_json = "1.0" | serde_json = "1.0" | ||||||
| rhai = { version = "1.16", features = ["serde"] } | rhai = { version = "1.16", features = ["serde"] } | ||||||
| @@ -23,6 +24,7 @@ wasm-bindgen-futures = "0.4" | |||||||
| once_cell = "1.21" | once_cell = "1.21" | ||||||
| vault = { path = "../vault" } | vault = { path = "../vault" } | ||||||
| evm_client = { path = "../evm_client" } | evm_client = { path = "../evm_client" } | ||||||
|  | sigsocket_client = { path = "../sigsocket_client" } | ||||||
|  |  | ||||||
| [dev-dependencies] | [dev-dependencies] | ||||||
| wasm-bindgen-test = "0.3" | wasm-bindgen-test = "0.3" | ||||||
|   | |||||||
| @@ -26,6 +26,10 @@ pub use vault::session_singleton::SESSION_MANAGER; | |||||||
| mod vault_bindings; | mod vault_bindings; | ||||||
| pub use 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) | /// Initialize the scripting environment (must be called before run_rhai) | ||||||
| #[wasm_bindgen] | #[wasm_bindgen] | ||||||
| pub fn init_rhai_env() { | 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 | /// Get all keypairs from the current session | ||||||
| /// Returns an array of keypair objects with id, type, and metadata | /// Returns an array of keypair objects with id, type, and metadata | ||||||
| // #[wasm_bindgen] | // #[wasm_bindgen] | ||||||
| @@ -214,7 +249,7 @@ pub async fn add_keypair( | |||||||
|     Ok(JsValue::from_str(&key_id)) |     Ok(JsValue::from_str(&key_id)) | ||||||
| } | } | ||||||
|  |  | ||||||
| /// Sign message with current session | /// Sign message with current session (requires selected keypair) | ||||||
| #[wasm_bindgen] | #[wasm_bindgen] | ||||||
| pub async fn sign(message: &[u8]) -> Result<JsValue, JsValue> { | 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 | /// Verify a signature with the current session's selected keypair | ||||||
| #[wasm_bindgen] | #[wasm_bindgen] | ||||||
| pub async fn verify(message: &[u8], signature: &str) -> Result<JsValue, JsValue> { | pub async fn verify(message: &[u8], signature: &str) -> Result<JsValue, JsValue> { | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user