feat: Add SessionManager for ergonomic key management

This commit is contained in:
2025-05-16 00:15:07 +03:00
parent 791752c3a5
commit 73233ec69b
10 changed files with 870 additions and 4 deletions

View File

@@ -3,11 +3,13 @@
//! vault: Cryptographic keyspace and operations
mod data;
pub use crate::data::{KeyType, KeyMetadata};
pub mod data;
pub use crate::session::SessionManager;
pub use crate::data::{KeyType, KeyMetadata, KeyEntry};
mod error;
mod crypto;
mod session;
mod utils;
use kvstore::KVStore;

View File

@@ -1,4 +1,228 @@
//! Session manager for the vault crate (optional)
//! Provides ergonomic, stateful access to unlocked keyspaces and keypairs for interactive applications.
//! All state is local to the SessionManager instance. No global state.
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>,
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>,
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,
}
}
}
// 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 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());
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()))?;
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)
}
/// 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> {
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(())
}
/// 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()))?;
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)
}
/// 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
}
}
// 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();
}
}
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();
}
}