feat: implement browser extension UI with WebAssembly integration

This commit is contained in:
Sameh Abouel-saad
2025-05-22 11:53:32 +03:00
parent 13945a8725
commit ed76ba3d8d
74 changed files with 7054 additions and 577 deletions

View File

@@ -1,5 +1,7 @@
//! Data models for the vault crate
// Only keep serde derives on structs, remove unused imports
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct VaultMetadata {
pub name: String,
@@ -7,7 +9,7 @@ pub struct VaultMetadata {
// ... other vault-level metadata
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
pub struct KeyspaceMetadata {
pub name: String,
pub salt: [u8; 16], // Unique salt for this keyspace
@@ -17,12 +19,28 @@ pub struct KeyspaceMetadata {
// ... other keyspace metadata
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
pub struct KeyspaceData {
pub keypairs: Vec<KeyEntry>,
// ... other keyspace-level metadata
}
impl zeroize::Zeroize for KeyspaceData {
fn zeroize(&mut self) {
for key in &mut self.keypairs {
key.zeroize();
}
self.keypairs.zeroize();
}
}
impl zeroize::Zeroize for KeyEntry {
fn zeroize(&mut self) {
self.private_key.zeroize();
// Optionally, zeroize other fields if needed
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct KeyEntry {
pub id: String,
@@ -39,7 +57,7 @@ pub enum KeyType {
// ...
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct KeyMetadata {
pub name: Option<String>,
pub created_at: Option<u64>,

View File

@@ -39,7 +39,7 @@ impl<S: kvstore::traits::KVStore + Send + Sync + Clone + 'static> RhaiSessionMan
pub fn sign(&self, message: rhai::Blob) -> Result<rhai::Blob, String> {
let sm = self.inner.lock().unwrap();
// Try to get the current keyspace name from session state if possible
let keypair = sm.current_keypair().ok_or("No keypair selected")?;
let _keypair = sm.current_keypair().ok_or("No keypair selected")?;
// Sign using the session manager; password and keyspace are not needed (already unlocked)
crate::rhai_sync_helpers::sign_sync::<S>(
&sm,
@@ -50,14 +50,46 @@ impl<S: kvstore::traits::KVStore + Send + Sync + Clone + 'static> RhaiSessionMan
#[cfg(target_arch = "wasm32")]
impl<S: kvstore::traits::KVStore + Clone + 'static> RhaiSessionManager<S> {
// WASM-specific implementation (stub for now)
pub fn select_keypair(&self, key_id: String) -> Result<(), String> {
// Use the global singleton for session management
crate::session_singleton::SESSION_MANAGER.with(|cell| {
let mut opt = cell.borrow_mut();
if let Some(session) = opt.as_mut() {
session.select_keypair(&key_id).map_err(|e| format!("select_keypair error: {e}"))
} else {
Err("Session not initialized".to_string())
}
})
}
pub fn current_keypair(&self) -> Option<String> {
crate::session_singleton::SESSION_MANAGER.with(|cell| {
let opt = cell.borrow();
opt.as_ref()
.and_then(|session| session.current_keypair().map(|k| k.id.clone()))
})
}
pub fn logout(&self) {
crate::session_singleton::SESSION_MANAGER.with(|cell| {
let mut opt = cell.borrow_mut();
if let Some(session) = opt.as_mut() {
session.logout();
}
});
}
pub fn sign(&self, _message: rhai::Blob) -> Result<rhai::Blob, String> {
// Signing is async in WASM; must be called from JS/wasm-bindgen, not Rhai
Err("sign is async in WASM; use the WASM sign() API from JS instead".to_string())
}
}
// WASM-specific API: no Arc/Mutex, just a reference
#[cfg(target_arch = "wasm32")]
pub fn register_rhai_api<S: kvstore::traits::KVStore + Clone + 'static>(
engine: &mut Engine,
session_manager: &SessionManager<S>,
// session_manager: &SessionManager<S>,
) {
// WASM registration logic (adapt as needed)
// Example: engine.register_type::<RhaiSessionManager<S>>();
@@ -66,7 +98,7 @@ pub fn register_rhai_api<S: kvstore::traits::KVStore + Clone + 'static>(
engine.register_fn("select_keypair", |key_id: String| {
crate::wasm_helpers::select_keypair_global(&key_id)
}); // Calls the shared WASM session singleton
engine.register_fn("sign", |message: rhai::Blob| -> Result<rhai::Blob, String> {
engine.register_fn("sign", |_message: rhai::Blob| -> Result<rhai::Blob, String> {
Err("sign is async in WASM; use the WASM sign() API from JS instead".to_string())
});
// No global session object in WASM; use JS/WASM API for session ops

View File

@@ -1,8 +1,10 @@
//! Synchronous wrappers for async Vault and EVM client APIs for use in Rhai bindings.
//! These use block_on for native, and spawn_local for WASM if needed.
use crate::session::SessionManager;
// Synchronous wrappers for async Vault and EVM client APIs for use in Rhai bindings.
// These use block_on for native, and spawn_local for WASM if needed.
#[cfg(not(target_arch = "wasm32"))]
use tokio::runtime::Handle;

View File

@@ -2,79 +2,60 @@
//! Provides ergonomic, stateful access to unlocked keyspaces and keypairs for interactive applications.
//! All state is local to the SessionManager instance. No global state.
use crate::{KVStore, KeyEntry, KeyspaceData, Vault, VaultError};
use std::collections::HashMap;
use zeroize::Zeroize;
use crate::{Vault, KeyspaceData, KeyEntry, VaultError, KVStore};
/// SessionManager: Ergonomic, stateful wrapper over the Vault stateless API.
#[cfg(not(target_arch = "wasm32"))]
pub struct SessionManager<S: KVStore + Send + Sync> {
vault: Vault<S>,
unlocked_keyspaces: HashMap<String, (Vec<u8>, KeyspaceData)>, // name -> (password, data)
current_keyspace: Option<String>,
unlocked_keyspace: Option<(String, Vec<u8>, KeyspaceData)>, // (name, password, data)
current_keypair: Option<String>,
}
#[cfg(target_arch = "wasm32")]
pub struct SessionManager<S: KVStore> {
vault: Vault<S>,
unlocked_keyspaces: HashMap<String, (Vec<u8>, KeyspaceData)>, // name -> (password, data)
current_keyspace: Option<String>,
unlocked_keyspace: Option<(String, Vec<u8>, KeyspaceData)>, // (name, password, data)
current_keypair: Option<String>,
}
#[cfg(not(target_arch = "wasm32"))]
impl<S: KVStore + Send + Sync> SessionManager<S> {
/// Create a new session manager from a Vault instance.
pub fn new(vault: Vault<S>) -> Self {
Self {
vault,
unlocked_keyspaces: HashMap::new(),
current_keyspace: None,
current_keypair: None,
}
}
}
#[cfg(target_arch = "wasm32")]
impl<S: KVStore> SessionManager<S> {
/// Create a new session manager from a Vault instance.
pub fn new(vault: Vault<S>) -> Self {
Self {
vault,
unlocked_keyspaces: HashMap::new(),
current_keyspace: None,
current_keypair: None,
}
pub fn get_vault_mut(&mut self) -> &mut Vault<S> {
&mut self.vault
}
}
// Native impl for all methods
#[cfg(not(target_arch = "wasm32"))]
impl<S: KVStore + Send + Sync> SessionManager<S> {
/// Unlock a keyspace and store its decrypted data in memory.
pub fn new(vault: Vault<S>) -> Self {
Self {
vault,
unlocked_keyspace: None,
current_keypair: None,
}
}
pub async fn create_keyspace(&mut self, name: &str, password: &[u8], tags: Option<Vec<String>>) -> Result<(), VaultError> {
self.vault.create_keyspace(name, password, tags).await?;
self.unlock_keyspace(name, password).await
}
pub async fn unlock_keyspace(&mut self, name: &str, password: &[u8]) -> Result<(), VaultError> {
let data = self.vault.unlock_keyspace(name, password).await?;
self.unlocked_keyspaces.insert(name.to_string(), (password.to_vec(), data));
self.current_keyspace = Some(name.to_string());
self.unlocked_keyspace = Some((name.to_string(), password.to_vec(), data));
self.current_keypair = None;
Ok(())
}
/// Select a previously unlocked keyspace as the current context.
pub fn select_keyspace(&mut self, name: &str) -> Result<(), VaultError> {
if self.unlocked_keyspaces.contains_key(name) {
self.current_keyspace = Some(name.to_string());
self.current_keypair = None;
Ok(())
} else {
Err(VaultError::Crypto("Keyspace not unlocked".to_string()))
}
}
/// Select a keypair within the current keyspace.
pub fn select_keypair(&mut self, key_id: &str) -> Result<(), VaultError> {
let keyspace = self.current_keyspace.as_ref().ok_or_else(|| VaultError::Crypto("No keyspace selected".to_string()))?;
let (_, data) = self.unlocked_keyspaces.get(keyspace).ok_or_else(|| VaultError::Crypto("Keyspace not unlocked".to_string()))?;
let data = self
.unlocked_keyspace
.as_ref()
.map(|(_, _, d)| d)
.ok_or_else(|| VaultError::Crypto("No keyspace unlocked".to_string()))?;
if data.keypairs.iter().any(|k| k.id == key_id) {
self.current_keypair = Some(key_id.to_string());
Ok(())
@@ -83,146 +64,164 @@ impl<S: KVStore + Send + Sync> SessionManager<S> {
}
}
/// Get the currently selected keyspace data (if any).
pub fn current_keyspace(&self) -> Option<&KeyspaceData> {
self.current_keyspace.as_ref()
.and_then(|name| self.unlocked_keyspaces.get(name))
.map(|(_, data)| data)
}
/// Get the currently selected keypair (if any).
pub fn current_keypair(&self) -> Option<&KeyEntry> {
let keyspace = self.current_keyspace()?;
let key_id = self.current_keypair.as_ref()?;
keyspace.keypairs.iter().find(|k| &k.id == key_id)
}
/// Sign a message with the currently selected keypair.
pub async fn sign(&self, message: &[u8]) -> Result<Vec<u8>, VaultError> {
let _keyspace = self.current_keyspace().ok_or(VaultError::Crypto("No keyspace selected".to_string()))?;
let keypair = self.current_keypair().ok_or(VaultError::Crypto("No keypair selected".to_string()))?;
let (password, _) = self.unlocked_keyspaces.get(self.current_keyspace.as_ref().unwrap()).unwrap();
self.vault.sign(
self.current_keyspace.as_ref().unwrap(),
password,
&keypair.id,
message,
).await
}
/// Get a reference to the underlying Vault (for stateless operations in tests).
pub fn get_vault(&self) -> &Vault<S> {
&self.vault
}
}
// WASM impl for all methods
#[cfg(target_arch = "wasm32")]
impl<S: KVStore> SessionManager<S> {
/// Unlock a keyspace and store its decrypted data in memory.
pub async fn unlock_keyspace(&mut self, name: &str, password: &[u8]) -> Result<(), VaultError> {
pub async fn add_keypair(
&mut self,
key_type: Option<crate::KeyType>,
metadata: Option<crate::KeyMetadata>,
) -> Result<String, VaultError> {
let (name, password, _) = self
.unlocked_keyspace
.as_ref()
.ok_or_else(|| VaultError::Crypto("No keyspace unlocked".to_string()))?;
let id = self
.vault
.add_keypair(name, password, key_type, metadata.clone())
.await?;
let data = self.vault.unlock_keyspace(name, password).await?;
self.unlocked_keyspaces.insert(name.to_string(), (password.to_vec(), data));
self.current_keyspace = Some(name.to_string());
Ok(())
self.unlocked_keyspace = Some((name.clone(), password.clone(), data));
Ok(id)
}
/// Select a previously unlocked keyspace as the current context.
pub fn select_keyspace(&mut self, name: &str) -> Result<(), VaultError> {
if self.unlocked_keyspaces.contains_key(name) {
self.current_keyspace = Some(name.to_string());
self.current_keypair = None;
Ok(())
} else {
Err(VaultError::Crypto("Keyspace not unlocked".to_string()))
}
pub fn list_keypairs(&self) -> Option<&[KeyEntry]> {
self.current_keyspace().map(|ks| ks.keypairs.as_slice())
}
/// Select a keypair within the current keyspace.
pub fn select_keypair(&mut self, key_id: &str) -> Result<(), VaultError> {
let keyspace = self.current_keyspace.as_ref().ok_or_else(|| VaultError::Crypto("No keyspace selected".to_string()))?;
let (_, data) = self.unlocked_keyspaces.get(keyspace).ok_or_else(|| VaultError::Crypto("Keyspace not unlocked".to_string()))?;
if data.keypairs.iter().any(|k| k.id == key_id) {
self.current_keypair = Some(key_id.to_string());
Ok(())
} else {
Err(VaultError::Crypto("Keypair not found".to_string()))
}
}
/// Get the currently selected keyspace data (if any).
pub fn current_keyspace(&self) -> Option<&KeyspaceData> {
self.current_keyspace.as_ref()
.and_then(|name| self.unlocked_keyspaces.get(name))
.map(|(_, data)| data)
self.unlocked_keyspace.as_ref().map(|(_, _, data)| data)
}
/// Get the currently selected keypair (if any).
pub fn current_keypair(&self) -> Option<&KeyEntry> {
let keyspace = self.current_keyspace()?;
let key_id = self.current_keypair.as_ref()?;
keyspace.keypairs.iter().find(|k| &k.id == key_id)
}
/// Sign a message with the currently selected keypair.
pub async fn sign(&self, message: &[u8]) -> Result<Vec<u8>, VaultError> {
let _keyspace = self.current_keyspace().ok_or(VaultError::Crypto("No keyspace selected".to_string()))?;
let keypair = self.current_keypair().ok_or(VaultError::Crypto("No keypair selected".to_string()))?;
let (password, _) = self.unlocked_keyspaces.get(self.current_keyspace.as_ref().unwrap()).unwrap();
self.vault.sign(
self.current_keyspace.as_ref().unwrap(),
password,
&keypair.id,
message,
).await
let (name, password, _) = self
.unlocked_keyspace
.as_ref()
.ok_or(VaultError::Crypto("No keyspace unlocked".to_string()))?;
let keypair = self
.current_keypair()
.ok_or(VaultError::Crypto("No keypair selected".to_string()))?;
self.vault.sign(name, password, &keypair.id, message).await
}
/// Get a reference to the underlying Vault (for stateless operations in tests).
pub fn get_vault(&self) -> &Vault<S> {
&self.vault
}
}
// Shared impl for methods needed by Drop
#[cfg(not(target_arch = "wasm32"))]
impl<S: KVStore + Send + Sync> SessionManager<S> {
/// Wipe all unlocked keyspaces and secrets from memory.
pub fn logout(&mut self) {
for (pw, data) in self.unlocked_keyspaces.values_mut() {
pw.zeroize();
// KeyspaceData and KeyEntry use Vec<u8> for secrets, drop will clear
for k in &mut data.keypairs {
k.private_key.zeroize();
}
if let Some((_, mut password, mut data)) = self.unlocked_keyspace.take() {
password.zeroize();
data.zeroize();
}
self.unlocked_keyspaces.clear();
self.current_keyspace = None;
self.current_keypair = None;
}
}
#[cfg(target_arch = "wasm32")]
impl<S: KVStore> SessionManager<S> {
/// Wipe all unlocked keyspaces and secrets from memory.
pub fn logout(&mut self) {
for (pw, data) in self.unlocked_keyspaces.values_mut() {
pw.zeroize();
// KeyspaceData and KeyEntry use Vec<u8> for secrets, drop will clear
for k in &mut data.keypairs {
k.private_key.zeroize();
}
}
self.unlocked_keyspaces.clear();
self.current_keyspace = None;
self.current_keypair = None;
}
}
// END wasm32 impl
#[cfg(not(target_arch = "wasm32"))]
impl<S: KVStore + Send + Sync> Drop for SessionManager<S> {
fn drop(&mut self) {
self.logout();
}
}
#[cfg(target_arch = "wasm32")]
impl<S: KVStore> SessionManager<S> {
pub fn new(vault: Vault<S>) -> Self {
Self {
vault,
unlocked_keyspace: None,
current_keypair: None,
}
}
pub async fn create_keyspace(&mut self, name: &str, password: &[u8], tags: Option<Vec<String>>) -> Result<(), VaultError> {
self.vault.create_keyspace(name, password, tags).await?;
self.unlock_keyspace(name, password).await
}
pub async fn unlock_keyspace(&mut self, name: &str, password: &[u8]) -> Result<(), VaultError> {
let data = self.vault.unlock_keyspace(name, password).await?;
self.unlocked_keyspace = Some((name.to_string(), password.to_vec(), data));
self.current_keypair = None;
Ok(())
}
pub fn select_keypair(&mut self, key_id: &str) -> Result<(), VaultError> {
let data = self
.unlocked_keyspace
.as_ref()
.map(|(_, _, d)| d)
.ok_or_else(|| VaultError::Crypto("No keyspace unlocked".to_string()))?;
if data.keypairs.iter().any(|k| k.id == key_id) {
self.current_keypair = Some(key_id.to_string());
Ok(())
} else {
Err(VaultError::Crypto("Keypair not found".to_string()))
}
}
pub async fn add_keypair(
&mut self,
key_type: Option<crate::KeyType>,
metadata: Option<crate::KeyMetadata>,
) -> Result<String, VaultError> {
let (name, password, _) = self
.unlocked_keyspace
.as_ref()
.ok_or_else(|| VaultError::Crypto("No keyspace unlocked".to_string()))?;
let id = self
.vault
.add_keypair(name, password, key_type, metadata.clone())
.await?;
let data = self.vault.unlock_keyspace(name, password).await?;
self.unlocked_keyspace = Some((name.clone(), password.clone(), data));
Ok(id)
}
pub fn list_keypairs(&self) -> Option<&[KeyEntry]> {
self.current_keyspace().map(|ks| ks.keypairs.as_slice())
}
pub fn current_keyspace(&self) -> Option<&KeyspaceData> {
self.unlocked_keyspace.as_ref().map(|(_, _, data)| data)
}
pub fn current_keypair(&self) -> Option<&KeyEntry> {
let keyspace = self.current_keyspace()?;
let key_id = self.current_keypair.as_ref()?;
keyspace.keypairs.iter().find(|k| &k.id == key_id)
}
pub async fn sign(&self, message: &[u8]) -> Result<Vec<u8>, VaultError> {
let (name, password, _) = self
.unlocked_keyspace
.as_ref()
.ok_or(VaultError::Crypto("No keyspace unlocked".to_string()))?;
let keypair = self
.current_keypair()
.ok_or(VaultError::Crypto("No keypair selected".to_string()))?;
self.vault.sign(name, password, &keypair.id, message).await
}
pub fn get_vault(&self) -> &Vault<S> {
&self.vault
}
pub fn logout(&mut self) {
if let Some((_, mut password, mut data)) = self.unlocked_keyspace.take() {
password.zeroize();
data.zeroize();
}
self.current_keypair = None;
}
}
#[cfg(target_arch = "wasm32")]
impl<S: KVStore> Drop for SessionManager<S> {
fn drop(&mut self) {
self.logout();
}
}