feat: Add SigSocket integration with WASM client and JavaScript bridge for sign requests
This commit is contained in:
parent
9f143ded9d
commit
4e1e707f85
@ -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);
|
||||||
if let Err(e) = ws_clone.send_with_str(&intro_message) {
|
web_sys::console::log_1(&format!("MAIN CONNECTION: Sending public key: {}", &intro_message[..16]).into());
|
||||||
web_sys::console::error_1(&format!("Failed to send introduction: {:?}", e).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()));
|
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| {
|
|
||||||
|
// 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 connected = self.connected.clone();
|
||||||
let timeout_ms = 5000; // 5 second timeout
|
|
||||||
|
|
||||||
// Check connection status periodically
|
// Wait up to 30 seconds, checking every 500ms
|
||||||
let check_connection = Rc::new(RefCell::new(None));
|
for attempt in 1..=60 {
|
||||||
let check_connection_clone = check_connection.clone();
|
// Check if we're connected
|
||||||
|
|
||||||
let interval_callback = Closure::wrap(Box::new(move || {
|
|
||||||
if *connected.borrow() {
|
if *connected.borrow() {
|
||||||
// Connected successfully
|
web_sys::console::log_1(&format!("wait_for_connection: Connected after {} attempts ({}ms)", attempt, attempt * 500).into());
|
||||||
resolve.call0(&wasm_bindgen::JsValue::UNDEFINED).unwrap();
|
return Ok(());
|
||||||
|
|
||||||
// Clear the interval
|
|
||||||
if let Some(interval_id) = check_connection_clone.borrow_mut().take() {
|
|
||||||
web_sys::window().unwrap().clear_interval_with_handle(interval_id);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}) as Box<dyn FnMut()>);
|
|
||||||
|
|
||||||
// Set up interval to check connection every 100ms
|
// Wait 500ms before next check
|
||||||
let interval_id = web_sys::window()
|
let promise = Promise::new(&mut |resolve, _reject| {
|
||||||
.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
|
|
||||||
let timeout_callback = Closure::wrap(Box::new(move || {
|
let timeout_callback = Closure::wrap(Box::new(move || {
|
||||||
reject.call1(&wasm_bindgen::JsValue::UNDEFINED,
|
resolve.call0(&wasm_bindgen::JsValue::UNDEFINED).unwrap();
|
||||||
&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);
|
|
||||||
}
|
|
||||||
}) as Box<dyn FnMut()>);
|
}) as Box<dyn FnMut()>);
|
||||||
|
|
||||||
web_sys::window()
|
web_sys::window()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.set_timeout_with_callback_and_timeout_and_arguments_0(
|
.set_timeout_with_callback_and_timeout_and_arguments_0(
|
||||||
timeout_callback.as_ref().unchecked_ref(),
|
timeout_callback.as_ref().unchecked_ref(),
|
||||||
timeout_ms,
|
500,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
timeout_callback.forget();
|
timeout_callback.forget();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Wait for the promise to resolve
|
let _ = JsFuture::from(promise).await;
|
||||||
JsFuture::from(promise).await
|
|
||||||
.map_err(|_| SigSocketError::Connection("Connection timeout".to_string()))?;
|
|
||||||
|
|
||||||
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)
|
/// 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,6 +12,7 @@ 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"
|
||||||
|
|
||||||
@ -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> {
|
||||||
|
Loading…
Reference in New Issue
Block a user