- Created SelfFreezoneClient in Self components
- Wraps SDK FreezoneScriptClient for Self-specific operations
- Implements send_verification_email method
- Uses Rhai script template for email verification
- Includes template variable substitution
- Added serde-wasm-bindgen dependency
Usage:
let client = SelfFreezoneClient::builder()
.supervisor_url("http://localhost:8080")
.secret("my-secret")
.build()?;
client.send_verification_email(
"user@example.com",
"123456",
"https://verify.com/abc"
).await?;
18 KiB
18 KiB
Vault System Documentation
Overview
The Self vault system provides secure storage and management of multiple encrypted cryptographic keys. It enables users to maintain multiple digital identities, each with its own key pair, while ensuring all private keys remain encrypted and under user control.
Architecture
Core Components
graph TB
VM[Vault Manager] --> V[Vault]
VM --> VE[Vault Entry]
V --> LS[Local Storage]
VE --> EPK[Encrypted Private Key]
VE --> MD[Metadata]
subgraph "Encryption Layer"
EPK --> AES[AES-256-GCM]
AES --> PBKDF2[PBKDF2 Key Derivation]
end
subgraph "Storage Layer"
LS --> JSON[JSON Format]
JSON --> B64[Base64 Encoding]
end
Data Structures
Vault Entry
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VaultEntry {
pub id: String, // Unique identifier (UUID)
pub name: String, // User-friendly name
pub email: String, // Associated email address
pub public_key: String, // Hex-encoded public key
pub encrypted_private_key: EncryptedPrivateKey, // Encrypted private key
pub created_at: String, // ISO 8601 timestamp
}
Vault Configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VaultConfig {
pub app_name: String, // Application identifier
pub storage_key: String, // LocalStorage key prefix
pub auto_lock_timeout: u32, // Auto-lock timeout in minutes
}
Vault Operations
Creating a New Vault Entry
impl Vault {
pub fn store_keypair(
name: &str,
email: &str,
keypair: &KeyPair,
password: &str,
) -> Result<String, VaultError> {
// Generate unique ID
let id = Uuid::new_v4().to_string();
// Encrypt private key
let encrypted_private_key = encrypt_private_key(&keypair.private_key, password)?;
// Create vault entry
let entry = VaultEntry {
id: id.clone(),
name: name.to_string(),
email: email.to_string(),
public_key: keypair.public_key.clone(),
encrypted_private_key,
created_at: Utc::now().to_rfc3339(),
};
// Store in vault
self.add_entry(entry)?;
Ok(id)
}
}
Retrieving Keys from Vault
impl Vault {
pub fn retrieve_keypair(password: &str) -> Result<(String, String), VaultError> {
// Get primary identity from storage
let storage = web_sys::window()
.and_then(|w| w.local_storage().ok().flatten())
.ok_or(VaultError::StorageNotAvailable)?;
let vault_data = storage
.get_item("self_vault")
.map_err(|_| VaultError::StorageError)?
.ok_or(VaultError::NoKeysStored)?;
let vault: VaultData = serde_json::from_str(&vault_data)
.map_err(|_| VaultError::InvalidVaultFormat)?;
// Find primary identity
let entry = vault.entries
.values()
.next()
.ok_or(VaultError::NoKeysStored)?;
// Decrypt private key
let private_key = decrypt_private_key(&entry.encrypted_private_key, password)?;
Ok((private_key, entry.public_key.clone()))
}
}
Listing Vault Entries
impl VaultManager {
pub fn list_identities(&self) -> Vec<IdentitySummary> {
self.vault_data
.entries
.values()
.map(|entry| IdentitySummary {
id: entry.id.clone(),
name: entry.name.clone(),
email: entry.email.clone(),
public_key: entry.public_key.clone(),
created_at: entry.created_at.clone(),
})
.collect()
}
}
Vault Manager Component
Component State
pub struct VaultManager {
vault_data: VaultData,
selected_identity: Option<String>,
password_input: String,
show_password_input: bool,
loading: bool,
error_message: Option<String>,
show_create_form: bool,
new_identity_name: String,
new_identity_email: String,
}
Key Management Operations
Adding New Identity
fn handle_create_identity(&mut self, ctx: &Context<Self>) {
if self.new_identity_name.trim().is_empty() ||
self.new_identity_email.trim().is_empty() {
self.error_message = Some("Name and email are required".to_string());
return;
}
let name = self.new_identity_name.clone();
let email = self.new_identity_email.clone();
let password = self.password_input.clone();
let link = ctx.link().clone();
wasm_bindgen_futures::spawn_local(async move {
match generate_keypair() {
Ok(keypair) => {
match Vault::store_keypair(&name, &email, &keypair, &password) {
Ok(id) => {
link.send_message(VaultMsg::IdentityCreated(id));
}
Err(e) => {
link.send_message(VaultMsg::Error(format!("Failed to store identity: {}", e)));
}
}
}
Err(e) => {
link.send_message(VaultMsg::Error(format!("Failed to generate keys: {}", e)));
}
}
});
}
Selecting Identity
fn handle_select_identity(&mut self, identity_id: String, ctx: &Context<Self>) {
if self.password_input.trim().is_empty() {
self.error_message = Some("Password required to access identity".to_string());
return;
}
self.loading = true;
self.selected_identity = Some(identity_id.clone());
let password = self.password_input.clone();
let link = ctx.link().clone();
wasm_bindgen_futures::spawn_local(async move {
match Vault::decrypt_identity(&identity_id, &password) {
Ok(keypair) => {
link.send_message(VaultMsg::IdentitySelected(keypair));
}
Err(e) => {
link.send_message(VaultMsg::Error(format!("Failed to decrypt identity: {}", e)));
}
}
});
}
Storage Format
Vault Data Structure
{
"version": "1.0",
"created_at": "2024-01-01T00:00:00Z",
"entries": {
"uuid-1": {
"id": "uuid-1",
"name": "Primary Identity",
"email": "user@example.com",
"public_key": "04a1b2c3d4e5f6...",
"encrypted_private_key": {
"encrypted_data": "base64-ciphertext",
"nonce": "base64-nonce",
"salt": "base64-salt"
},
"created_at": "2024-01-01T00:00:00Z"
},
"uuid-2": {
"id": "uuid-2",
"name": "Work Identity",
"email": "work@company.com",
"public_key": "04b2c3d4e5f6a1...",
"encrypted_private_key": {
"encrypted_data": "base64-ciphertext-2",
"nonce": "base64-nonce-2",
"salt": "base64-salt-2"
},
"created_at": "2024-01-02T00:00:00Z"
}
}
}
Storage Keys
- Primary Vault:
self_vault- Main vault storage - Active Identity:
self_active_identity- Currently selected identity ID - Session Data:
self_session- Temporary session information
Security Model
Encryption Strategy
- Individual Key Encryption: Each private key encrypted separately
- Unique Salts: Each key uses its own random salt
- Password-Based Access: Same password can decrypt all keys in vault
- No Master Key: No single key encrypts the entire vault
Password Management
impl VaultManager {
fn validate_password(&self, password: &str) -> Result<(), VaultError> {
if password.len() < 8 {
return Err(VaultError::WeakPassword("Password must be at least 8 characters".to_string()));
}
// Additional password strength checks
let has_upper = password.chars().any(|c| c.is_uppercase());
let has_lower = password.chars().any(|c| c.is_lowercase());
let has_digit = password.chars().any(|c| c.is_numeric());
if !has_upper || !has_lower || !has_digit {
return Err(VaultError::WeakPassword(
"Password must contain uppercase, lowercase, and numeric characters".to_string()
));
}
Ok(())
}
}
Access Control
pub enum VaultAccess {
ReadOnly, // Can view public information only
Decrypt, // Can decrypt and use private keys
Manage, // Can add/remove identities
}
impl VaultManager {
pub fn check_access(&self, required_access: VaultAccess) -> bool {
match required_access {
VaultAccess::ReadOnly => true,
VaultAccess::Decrypt => self.is_unlocked(),
VaultAccess::Manage => self.is_unlocked() && self.has_management_privileges(),
}
}
}
Error Handling
Vault Error Types
#[derive(Debug, Clone)]
pub enum VaultError {
StorageNotAvailable,
StorageError,
NoKeysStored,
InvalidVaultFormat,
EncryptionFailed(String),
DecryptionFailed(String),
WeakPassword(String),
IdentityNotFound(String),
DuplicateIdentity(String),
InvalidKeyFormat,
}
impl fmt::Display for VaultError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
VaultError::StorageNotAvailable => write!(f, "Browser storage not available"),
VaultError::StorageError => write!(f, "Failed to access storage"),
VaultError::NoKeysStored => write!(f, "No keys found in vault"),
VaultError::InvalidVaultFormat => write!(f, "Invalid vault data format"),
VaultError::EncryptionFailed(msg) => write!(f, "Encryption failed: {}", msg),
VaultError::DecryptionFailed(msg) => write!(f, "Decryption failed: {}", msg),
VaultError::WeakPassword(msg) => write!(f, "Weak password: {}", msg),
VaultError::IdentityNotFound(id) => write!(f, "Identity not found: {}", id),
VaultError::DuplicateIdentity(email) => write!(f, "Identity already exists: {}", email),
VaultError::InvalidKeyFormat => write!(f, "Invalid key format"),
}
}
}
User Interface
Vault Manager UI Components
Identity List
fn render_identity_list(&self, ctx: &Context<Self>) -> Html {
let identities = self.list_identities();
html! {
<div class="identity-list">
<h5>{"Stored Identities"}</h5>
{for identities.iter().map(|identity| {
let identity_id = identity.id.clone();
html! {
<div class="identity-card" key={identity.id.clone()}>
<div class="identity-info">
<h6>{&identity.name}</h6>
<p class="text-muted">{&identity.email}</p>
<small class="text-muted">
{"Created: "}{&identity.created_at}
</small>
</div>
<div class="identity-actions">
<button class="btn btn-primary btn-sm"
onclick={ctx.link().callback(move |_| {
VaultMsg::SelectIdentity(identity_id.clone())
})}>
{"Select"}
</button>
</div>
</div>
}
})}
</div>
}
}
Create Identity Form
fn render_create_form(&self, ctx: &Context<Self>) -> Html {
html! {
<div class="create-identity-form">
<h5>{"Create New Identity"}</h5>
<div class="mb-3">
<label class="form-label">{"Name"}</label>
<input type="text" class="form-control"
value={self.new_identity_name.clone()}
oninput={ctx.link().callback(|e: InputEvent| {
let input: HtmlInputElement = e.target_unchecked_into();
VaultMsg::UpdateNewIdentityName(input.value())
})} />
</div>
<div class="mb-3">
<label class="form-label">{"Email"}</label>
<input type="email" class="form-control"
value={self.new_identity_email.clone()}
oninput={ctx.link().callback(|e: InputEvent| {
let input: HtmlInputElement = e.target_unchecked_into();
VaultMsg::UpdateNewIdentityEmail(input.value())
})} />
</div>
<div class="mb-3">
<label class="form-label">{"Password"}</label>
<input type="password" class="form-control"
value={self.password_input.clone()}
oninput={ctx.link().callback(|e: InputEvent| {
let input: HtmlInputElement = e.target_unchecked_into();
VaultMsg::UpdatePassword(input.value())
})} />
</div>
<button class="btn btn-success"
onclick={ctx.link().callback(|_| VaultMsg::CreateIdentity)}
disabled={self.loading}>
{if self.loading { "Creating..." } else { "Create Identity" }}
</button>
</div>
}
}
Integration with Other Components
Registration Integration
// Auto-store generated keys during registration
impl Registration {
fn complete_registration(&mut self, ctx: &Context<Self>) {
if let (Some(keypair), Some(password)) = (&self.generated_keypair, &self.password) {
// Store in vault automatically
match Vault::store_keypair(
&self.name,
&self.email,
keypair,
password
) {
Ok(id) => {
web_sys::console::log_1(&format!("Identity stored in vault: {}", id).into());
}
Err(e) => {
web_sys::console::log_1(&format!("Failed to store in vault: {}", e).into());
}
}
}
}
}
Login Integration
// Select identity from vault for login
impl Login {
fn load_from_vault(&mut self, identity_id: &str, password: &str) -> Result<(), String> {
match Vault::decrypt_identity(identity_id, password) {
Ok(keypair) => {
self.current_keypair = Some(keypair);
Ok(())
}
Err(e) => Err(format!("Failed to load identity: {}", e))
}
}
}
Backup and Recovery
Export Functionality
impl VaultManager {
pub fn export_vault(&self, password: &str) -> Result<String, VaultError> {
// Verify password can decrypt at least one identity
self.verify_vault_password(password)?;
// Export vault data (still encrypted)
let export_data = ExportData {
version: "1.0".to_string(),
exported_at: Utc::now().to_rfc3339(),
vault: self.vault_data.clone(),
};
serde_json::to_string_pretty(&export_data)
.map_err(|_| VaultError::StorageError)
}
pub fn import_vault(&mut self, import_data: &str, password: &str) -> Result<(), VaultError> {
let export_data: ExportData = serde_json::from_str(import_data)
.map_err(|_| VaultError::InvalidVaultFormat)?;
// Verify password can decrypt imported identities
for entry in export_data.vault.entries.values() {
decrypt_private_key(&entry.encrypted_private_key, password)?;
}
// Merge with existing vault
for (id, entry) in export_data.vault.entries {
self.vault_data.entries.insert(id, entry);
}
self.save_vault()
}
}
Recovery Options
- Password Recovery: Not possible - passwords are not stored
- Vault Export: Users must export vault data regularly
- Individual Key Backup: Each private key can be backed up separately
- Seed Phrase: Future enhancement for deterministic key generation
Performance Considerations
Optimization Strategies
- Lazy Loading: Load vault data only when needed
- Caching: Cache decrypted keys in memory during session
- Batch Operations: Group multiple vault operations
- Background Sync: Sync vault changes in background
Memory Management
impl Drop for VaultManager {
fn drop(&mut self) {
// Clear sensitive data from memory
self.password_input.zeroize();
if let Some(ref mut keypair) = self.cached_keypair {
keypair.private_key.zeroize();
}
}
}
Future Enhancements
Planned Features
- Hierarchical Deterministic Keys: BIP32-style key derivation
- Hardware Token Integration: WebAuthn support
- Vault Synchronization: Cross-device vault sync
- Biometric Authentication: WebAuthn biometric support
- Key Rotation: Automatic key rotation policies
- Audit Trail: Comprehensive logging of vault operations
Advanced Security Features
- Multi-Factor Authentication: Additional authentication factors
- Time-Based Access: Temporary key access permissions
- Geolocation Restrictions: Location-based access controls
- Device Binding: Tie vault access to specific devices