From e194b76f6fb768df630757ed690a0c6624ea46b8 Mon Sep 17 00:00:00 2001 From: iamdoubz <> Date: Fri, 3 Jul 2026 08:06:05 -0500 Subject: [PATCH] feat(vault): vault lifecycle commands; seal summary; decrypt on export (T8.8) --- src-tauri/src/commands.rs | 73 +++++++++++++++++++++++++++++++++++---- 1 file changed, 66 insertions(+), 7 deletions(-) diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index e67a976..9ebf5a5 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -1403,12 +1403,22 @@ async fn export_meeting_to( std::fs::create_dir_all(dest_path) .map_err(|e| WaError::new("export", e.to_string()))?; let source_dir = meeting_dir(meeting_id); - for name in ["audio.wav", "transcript.json"] { - let src = source_dir.join(name); - if src.exists() { - std::fs::copy(&src, dest_path.join(name)) - .map_err(|e| WaError::new("export", e.to_string()))?; - } + // Audio is stored plaintext — copy as-is. + let audio_src = source_dir.join("audio.wav"); + if audio_src.exists() { + std::fs::copy(&audio_src, dest_path.join("audio.wav")) + .map_err(|e| WaError::new("export", e.to_string()))?; + } + // transcript.json may be vault-sealed; export the decrypted content + // so the bundle is usable (T8.8). + let transcript_src = source_dir.join("transcript.json"); + if transcript_src.exists() { + let bytes = std::fs::read(&transcript_src) + .map_err(|e| WaError::new("export", e.to_string()))?; + let plain = + crate::vault::open(&bytes).map_err(|e| WaError::new("vault", e.to_string()))?; + std::fs::write(dest_path.join("transcript.json"), plain) + .map_err(|e| WaError::new("export", e.to_string()))?; } std::fs::write(dest_path.join("notes.md"), &meeting.notes_markdown) .map_err(|e| WaError::new("export", e.to_string()))?; @@ -1645,7 +1655,10 @@ pub async fn generate_summary( }; let json = serde_json::to_string_pretty(&summary_file) .map_err(|e| WaError::new("llm", e.to_string()))?; - let _ = std::fs::write(meeting_dir(&meeting_id).join("summary.json"), json); + // Seal at rest when the vault is unlocked (T8.8, FR-SEC-3); passthrough otherwise. + if let Ok(sealed) = crate::vault::seal(json.as_bytes()) { + let _ = std::fs::write(meeting_dir(&meeting_id).join("summary.json"), sealed); + } let _ = app.emit( "llm://done", @@ -2139,6 +2152,52 @@ pub async fn set_sync_enabled(enabled: bool) -> WaResult<()> { save_settings(&settings) } +// ---- At-rest encryption vault (Phase 8, T8.8, FR-SEC-3) ---- + +#[tauri::command] +pub async fn vault_status() -> WaResult { + Ok(serde_json::json!({ + "enabled": crate::vault::is_enabled(), + "unlocked": crate::vault::is_unlocked(), + })) +} + +#[derive(Deserialize)] +pub struct VaultPasswordArgs { + pub password: String, +} + +/// Turn on the vault: derive a key from the password and start sealing artifacts +/// at rest. Leaves the vault unlocked for the session. +#[tauri::command] +pub async fn enable_vault(args: VaultPasswordArgs) -> WaResult<()> { + crate::vault::enable(&args.password).map_err(|e| WaError::new("vault", e.to_string())) +} + +#[tauri::command] +pub async fn unlock_vault(args: VaultPasswordArgs) -> WaResult<()> { + crate::vault::unlock(&args.password).map_err(|e| WaError::new("vault", e.to_string())) +} + +#[tauri::command] +pub async fn lock_vault() -> WaResult<()> { + crate::vault::lock(); + Ok(()) +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ChangeVaultPasswordArgs { + pub old_password: String, + pub new_password: String, +} + +#[tauri::command] +pub async fn change_vault_password(args: ChangeVaultPasswordArgs) -> WaResult<()> { + crate::vault::change_password(&args.old_password, &args.new_password) + .map_err(|e| WaError::new("vault", e.to_string())) +} + // ---- Sync durable queue (T9.5) ---- /// Give up auto-retrying after this many failures; the job stays `failed` with