- 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>
105 lines
3.6 KiB
Rust
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)
|
|
}
|