wraith/src-tauri/src/commands/vault.rs
Vantz Stockwell da2dd5bbfc fix: SEC-3/CONC-1/2/3 vault zeroize + async mutex + cancellation tokens
- Vault key uses Zeroizing<[u8; 32]>, passwords zeroized after use
- vault/credentials Mutex upgraded to tokio::sync::Mutex
- CWD tracker + monitor use CancellationToken for clean shutdown
- Monitor exec_command has 10s timeout, 3-strike dead connection heuristic

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 16:40:10 -04:00

105 lines
3.6 KiB
Rust

use tauri::State;
use zeroize::Zeroize;
use crate::vault::{self, VaultService};
use crate::credentials::CredentialService;
use crate::AppState;
/// Returns `true` if no vault has ever been created on this machine.
///
/// The frontend shows the first-run setup screen when this returns `true`.
#[tauri::command]
pub fn is_first_run(state: State<'_, AppState>) -> bool {
state.is_first_run()
}
/// Create a new vault protected by `password`.
///
/// Derives a 256-bit key from the password, encrypts a known check value,
/// persists the salt and check value to the settings table, and activates
/// the vault for the current session.
///
/// Returns `Err` if the vault has already been set up or if any storage
/// operation fails.
#[tauri::command]
pub async fn create_vault(mut password: String, state: State<'_, AppState>) -> Result<(), String> {
let result = async {
if !state.is_first_run() {
return Err("Vault already exists — use unlock instead of create".into());
}
let salt = vault::generate_salt();
let key = vault::derive_key(&password, &salt);
let vs = VaultService::new(key.clone());
// Persist the salt so we can re-derive the key on future unlocks.
state.settings.set("vault_salt", &hex::encode(salt))?;
// Persist a known-plaintext check so unlock can verify the password.
let check = vs.encrypt("wraith-vault-check")?;
state.settings.set("vault_check", &check)?;
// Activate the vault and credentials service for this session.
let cred_svc = CredentialService::new(state.db.clone(), VaultService::new(key));
*state.credentials.lock().await = Some(cred_svc);
*state.vault.lock().await = Some(vs);
Ok(())
}.await;
password.zeroize();
result
}
/// Unlock an existing vault using the master password.
///
/// Re-derives the key from the stored salt, decrypts the check value to
/// verify the password is correct, then activates the vault for the session.
///
/// Returns `Err("Incorrect master password")` if the password is wrong.
#[tauri::command]
pub async fn unlock(mut password: String, state: State<'_, AppState>) -> Result<(), String> {
let result = async {
let salt_hex = state
.settings
.get("vault_salt")
.ok_or_else(|| "Vault has not been set up — call create_vault first".to_string())?;
let salt = hex::decode(&salt_hex)
.map_err(|e| format!("Stored vault salt is corrupt: {e}"))?;
let key = vault::derive_key(&password, &salt);
let vs = VaultService::new(key.clone());
// Verify the password by decrypting the check value.
let check_blob = state
.settings
.get("vault_check")
.ok_or_else(|| "Vault check value is missing — vault may be corrupt".to_string())?;
let check_plain = vs
.decrypt(&check_blob)
.map_err(|_| "Incorrect master password".to_string())?;
if check_plain != "wraith-vault-check" {
return Err("Incorrect master password".into());
}
// Activate the vault and credentials service for this session.
let cred_svc = CredentialService::new(state.db.clone(), VaultService::new(key));
*state.credentials.lock().await = Some(cred_svc);
*state.vault.lock().await = Some(vs);
Ok(())
}.await;
password.zeroize();
result
}
/// Returns `true` if the vault is currently unlocked for this session.
#[tauri::command]
pub async fn is_unlocked(state: State<'_, AppState>) -> Result<bool, String> {
Ok(state.is_unlocked().await)
}