Files
self/docs/vault-system.md
Timur Gordon f970f3fb58 Add SelfFreezoneClient wrapper for Self components
- 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?;
2025-11-03 16:16:18 +01:00

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

  1. Individual Key Encryption: Each private key encrypted separately
  2. Unique Salts: Each key uses its own random salt
  3. Password-Based Access: Same password can decrypt all keys in vault
  4. 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

  1. Password Recovery: Not possible - passwords are not stored
  2. Vault Export: Users must export vault data regularly
  3. Individual Key Backup: Each private key can be backed up separately
  4. Seed Phrase: Future enhancement for deterministic key generation

Performance Considerations

Optimization Strategies

  1. Lazy Loading: Load vault data only when needed
  2. Caching: Cache decrypted keys in memory during session
  3. Batch Operations: Group multiple vault operations
  4. 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

  1. Hierarchical Deterministic Keys: BIP32-style key derivation
  2. Hardware Token Integration: WebAuthn support
  3. Vault Synchronization: Cross-device vault sync
  4. Biometric Authentication: WebAuthn biometric support
  5. Key Rotation: Automatic key rotation policies
  6. Audit Trail: Comprehensive logging of vault operations

Advanced Security Features

  1. Multi-Factor Authentication: Additional authentication factors
  2. Time-Based Access: Temporary key access permissions
  3. Geolocation Restrictions: Location-based access controls
  4. Device Binding: Tie vault access to specific devices