feat: Add SigSocket integration with WASM client and JavaScript bridge for sign requests
This commit is contained in:
@@ -12,10 +12,11 @@ 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"
|
||||
|
||||
#
|
||||
#
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
rhai = { version = "1.16", features = ["serde"] }
|
||||
@@ -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