feat: implement browser extension UI with WebAssembly integration
This commit is contained in:
@@ -12,7 +12,7 @@ tokio = { version = "1.37", features = ["rt", "macros"] }
|
||||
kvstore = { path = "../kvstore" }
|
||||
scrypt = "0.11"
|
||||
sha2 = "0.10"
|
||||
aes-gcm = "0.10"
|
||||
# aes-gcm = "0.10"
|
||||
pbkdf2 = "0.12"
|
||||
signature = "2.2"
|
||||
async-trait = "0.1"
|
||||
@@ -22,17 +22,20 @@ ed25519-dalek = "2.1"
|
||||
rand_core = "0.6"
|
||||
log = "0.4"
|
||||
thiserror = "1"
|
||||
env_logger = "0.11"
|
||||
console_log = "1"
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
env_logger = "0.11"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
hex = "0.4"
|
||||
zeroize = "1.8.1"
|
||||
rhai = "1.21.0"
|
||||
|
||||
|
||||
[dev-dependencies]
|
||||
wasm-bindgen-test = "0.3"
|
||||
console_error_panic_hook = "0.1"
|
||||
# console_error_panic_hook = "0.1"
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
|
||||
tempfile = "3.10"
|
||||
@@ -42,7 +45,16 @@ chrono = "0.4"
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
getrandom = { version = "0.3", features = ["wasm_js"] }
|
||||
getrandom_02 = { package = "getrandom", version = "0.2", features = ["js"] }
|
||||
getrandom_02 = { package = "getrandom", version = "0.2.16", features = ["js"] }
|
||||
wasm-bindgen = "0.2"
|
||||
js-sys = "0.3"
|
||||
console_error_panic_hook = "0.1"
|
||||
# console_error_panic_hook = "0.1"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
hex = "0.4"
|
||||
rhai = "1.21.0"
|
||||
zeroize = "1.8.1"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
native = []
|
@@ -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>,
|
||||
|
@@ -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
|
||||
|
@@ -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;
|
||||
|
||||
|
@@ -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();
|
||||
}
|
||||
}
|
||||
|
@@ -15,17 +15,21 @@ async fn session_manager_end_to_end() {
|
||||
let keyspace = "personal";
|
||||
let password = b"testpass";
|
||||
|
||||
// Create keyspace
|
||||
vault.create_keyspace(keyspace, password, None).await.expect("create_keyspace");
|
||||
// Add keypair
|
||||
let key_id = vault.add_keypair(keyspace, password, Some(KeyType::Secp256k1), Some(KeyMetadata { name: Some("main".to_string()), created_at: None, tags: None })).await.expect("add_keypair");
|
||||
|
||||
// Create session manager
|
||||
let mut session = SessionManager::new(vault);
|
||||
session.unlock_keyspace(keyspace, password).await.expect("unlock_keyspace");
|
||||
session.select_keyspace(keyspace).expect("select_keyspace");
|
||||
// Create and unlock keyspace in one step
|
||||
session.create_keyspace(keyspace, password, None).await.expect("create_keyspace via session");
|
||||
// Add keypair using session API
|
||||
let key_id = session.add_keypair(Some(KeyType::Secp256k1), Some(KeyMetadata { name: Some("main".to_string()), created_at: None, tags: None })).await.expect("add_keypair via session");
|
||||
session.select_keypair(&key_id).expect("select_keypair");
|
||||
|
||||
// Test add_keypair with metadata via SessionManager
|
||||
let meta = KeyMetadata { name: Some("user1-key".to_string()), created_at: None, tags: Some(vec!["tag1".to_string()]) };
|
||||
let key_id2 = session.add_keypair(Some(KeyType::Ed25519), Some(meta.clone())).await.expect("add_keypair via session");
|
||||
// List keypairs and check metadata
|
||||
let keypairs = session.list_keypairs().expect("list_keypairs");
|
||||
assert!(keypairs.iter().any(|k| k.id == key_id2 && k.metadata.as_ref().unwrap().name.as_deref() == Some("user1-key")), "metadata name should be present");
|
||||
|
||||
// Sign and verify
|
||||
let msg = b"hello world";
|
||||
let sig = session.sign(msg).await.expect("sign");
|
||||
@@ -55,7 +59,8 @@ async fn session_manager_errors() {
|
||||
let vault = Vault::new(store);
|
||||
let mut session = SessionManager::new(vault);
|
||||
// No keyspace unlocked
|
||||
assert!(session.select_keyspace("none").is_err());
|
||||
// select_keyspace removed; test unlocking a non-existent keyspace or selecting a keypair from an empty keyspace instead.
|
||||
assert!(session.select_keypair("none").is_err());
|
||||
assert!(session.select_keypair("none").is_err());
|
||||
assert!(session.sign(b"fail").await.is_err());
|
||||
}
|
||||
|
@@ -16,7 +16,7 @@ async fn test_keypair_management_and_crypto() {
|
||||
// All imports are WASM-specific and local to the test function
|
||||
use kvstore::wasm::WasmStore;
|
||||
use vault::Vault;
|
||||
let store = WasmStore::open("testdb_wasm_keypair_management").await.unwrap();
|
||||
let store = WasmStore::open("vault").await.unwrap();
|
||||
let mut vault = Vault::new(store);
|
||||
vault.create_keyspace("testspace", b"pw", None).await.unwrap();
|
||||
let key_id = vault.add_keypair("testspace", b"pw", None, None).await.unwrap();
|
||||
|
@@ -11,19 +11,65 @@ use vault::Vault;
|
||||
wasm_bindgen_test_configure!(run_in_browser);
|
||||
|
||||
#[wasm_bindgen_test(async)]
|
||||
async fn test_session_manager_end_to_end() {
|
||||
// Example: test session manager logic in WASM
|
||||
// This is a placeholder for your real test logic.
|
||||
// All imports are WASM-specific and local to the test function
|
||||
async fn test_session_manager_lock_unlock_keypairs_persistence() {
|
||||
use kvstore::wasm::WasmStore;
|
||||
use vault::{Vault, KeyType, KeyMetadata};
|
||||
use vault::session::SessionManager;
|
||||
let store = WasmStore::open("testdb_wasm_session_manager").await.unwrap();
|
||||
let vault = Vault::new(store);
|
||||
let mut manager = SessionManager::new(vault);
|
||||
let keyspace = "testspace";
|
||||
// This test can only check session initialization/select_keyspace logic as SessionManager does not create keypairs directly.
|
||||
// manager.select_keyspace(keyspace) would fail unless the keyspace exists.
|
||||
// For a true end-to-end test, use Vault to create the keyspace and keypair, then test SessionManager.
|
||||
// For now, just test that SessionManager can be constructed.
|
||||
assert!(manager.current_keyspace().is_none());
|
||||
let store = WasmStore::open("test-session-manager-lock-unlock").await.unwrap();
|
||||
let mut vault = Vault::new(store);
|
||||
let keyspace = "testspace2";
|
||||
let password = b"testpass2";
|
||||
|
||||
// 1. Create session manager
|
||||
let mut session = SessionManager::new(vault);
|
||||
// Create and unlock keyspace in one step
|
||||
session.create_keyspace(keyspace, password, None).await.expect("create_keyspace via session");
|
||||
// 2. Add two keypairs with names using session API
|
||||
let meta1 = KeyMetadata { name: Some("keypair-one".to_string()), created_at: None, tags: None };
|
||||
let meta2 = KeyMetadata { name: Some("keypair-two".to_string()), created_at: None, tags: None };
|
||||
let id1 = session.add_keypair(Some(KeyType::Secp256k1), Some(meta1.clone())).await.expect("add_keypair1 via session");
|
||||
let id2 = session.add_keypair(Some(KeyType::Ed25519), Some(meta2.clone())).await.expect("add_keypair2 via session");
|
||||
|
||||
// 3. List, store keys and names
|
||||
let keypairs_before = session.list_keypairs().expect("list_keypairs before").iter().map(|k| (k.id.clone(), k.public_key.clone(), k.private_key.clone(), k.metadata.clone())).collect::<Vec<_>>();
|
||||
let keypairs_before = session.list_keypairs().expect("list_keypairs before").iter().map(|k| (k.id.clone(), k.public_key.clone(), k.private_key.clone(), k.metadata.clone())).collect::<Vec<_>>();
|
||||
assert_eq!(keypairs_before.len(), 2);
|
||||
assert!(keypairs_before.iter().any(|k| k.0 == id1 && k.3.as_ref().unwrap().name.as_deref() == Some("keypair-one")));
|
||||
assert!(keypairs_before.iter().any(|k| k.0 == id2 && k.3.as_ref().unwrap().name.as_deref() == Some("keypair-two")));
|
||||
|
||||
// 4. Lock (logout)
|
||||
session.logout();
|
||||
assert!(session.current_keyspace().is_none());
|
||||
|
||||
// 5. Unlock again
|
||||
session.unlock_keyspace(keyspace, password).await.expect("unlock_keyspace again");
|
||||
// select_keyspace removed; unlocking a keyspace is sufficient after refactor.
|
||||
|
||||
// 6. List and check keys/names match
|
||||
let keypairs_after = session.list_keypairs().expect("list_keypairs after").iter().map(|k| (k.id.clone(), k.public_key.clone(), k.private_key.clone(), k.metadata.clone())).collect::<Vec<_>>();
|
||||
assert_eq!(keypairs_before, keypairs_after, "Keypairs before and after lock/unlock should match");
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test(async)]
|
||||
async fn test_session_manager_end_to_end() {
|
||||
use kvstore::wasm::WasmStore;
|
||||
use vault::{Vault, KeyType, KeyMetadata};
|
||||
use vault::session::SessionManager;
|
||||
let store = WasmStore::open("test-session-manager").await.unwrap();
|
||||
let keyspace = "testspace";
|
||||
let password = b"testpass";
|
||||
|
||||
// Create session manager
|
||||
let mut session = SessionManager::new(Vault::new(store));
|
||||
// Create and unlock keyspace in one step
|
||||
session.create_keyspace(keyspace, password, None).await.expect("create_keyspace via session");
|
||||
// Add keypair using session API
|
||||
let key_id = session.add_keypair(Some(KeyType::Secp256k1), Some(KeyMetadata { name: Some("main".to_string()), created_at: None, tags: None })).await.expect("add_keypair via session");
|
||||
|
||||
// Test add_keypair with metadata via SessionManager
|
||||
let meta = KeyMetadata { name: Some("user1-key".to_string()), created_at: None, tags: Some(vec!["tag1".to_string()]) };
|
||||
let key_id2 = session.add_keypair(Some(KeyType::Ed25519), Some(meta.clone())).await.expect("add_keypair via session");
|
||||
// List keypairs and check metadata
|
||||
let keypairs = session.list_keypairs().expect("list_keypairs");
|
||||
assert!(keypairs.iter().any(|k| k.id == key_id2 && k.metadata.as_ref().unwrap().name.as_deref() == Some("user1-key")), "metadata name should be present");
|
||||
}
|
||||
|
Reference in New Issue
Block a user