wraith/src-tauri/src/commands/credentials.rs
Vantz Stockwell 6c7b277494
Some checks failed
Build & Sign Wraith / Build Windows + Sign (push) Failing after 6s
fix: SSH auth — decrypt credentials from vault before connecting
Added decrypt_password and decrypt_ssh_key Tauri commands.
Connect flow now resolves credentialId → decrypted credentials
from the vault. Falls back to window.prompt on auth failure.
Fixed case-sensitive error string matching.

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

99 lines
3.3 KiB
Rust

use tauri::State;
use crate::credentials::Credential;
use crate::AppState;
/// Guard helper: lock the credentials mutex and return a ref to the inner
/// `CredentialService`, or a "Vault is locked" error if the vault has not
/// been unlocked for this session.
///
/// This is a macro rather than a function because returning a `MutexGuard`
/// from a helper function would require lifetime annotations that complicate
/// the tauri command signatures unnecessarily.
macro_rules! require_unlocked {
($state:expr) => {{
let guard = $state
.credentials
.lock()
.map_err(|_| "Credentials mutex was poisoned".to_string())?;
if guard.is_none() {
return Err("Vault is locked — call unlock before accessing credentials".into());
}
// SAFETY: we just checked `is_none` above, so `unwrap` cannot panic.
guard
}};
}
/// Return all credentials ordered by name.
///
/// Secret values (passwords, private keys) are never included — only metadata.
#[tauri::command]
pub fn list_credentials(state: State<'_, AppState>) -> Result<Vec<Credential>, String> {
let guard = require_unlocked!(state);
guard.as_ref().unwrap().list()
}
/// Store a new username/password credential.
///
/// The `password` value is encrypted with AES-256-GCM before being persisted.
/// Returns the created credential record (without the plaintext password).
/// `domain` is `None` for non-domain credentials; `Some("")` is treated as NULL.
#[tauri::command]
pub fn create_password(
name: String,
username: String,
password: String,
domain: Option<String>,
state: State<'_, AppState>,
) -> Result<Credential, String> {
let guard = require_unlocked!(state);
guard
.as_ref()
.unwrap()
.create_password(name, username, password, domain)
}
/// Store a new SSH private key credential.
///
/// Both `private_key_pem` and `passphrase` are encrypted before storage.
/// Pass `None` for `passphrase` when the key has no passphrase.
/// Returns the created credential record without any secret material.
#[tauri::command]
pub fn create_ssh_key(
name: String,
username: String,
private_key_pem: String,
passphrase: Option<String>,
state: State<'_, AppState>,
) -> Result<Credential, String> {
let guard = require_unlocked!(state);
guard
.as_ref()
.unwrap()
.create_ssh_key(name, username, private_key_pem, passphrase)
}
/// Delete a credential by id.
///
/// For SSH key credentials, the associated `ssh_keys` row is also deleted.
/// Returns `Err` if the vault is locked or the id does not exist.
#[tauri::command]
pub fn delete_credential(id: i64, state: State<'_, AppState>) -> Result<(), String> {
let guard = require_unlocked!(state);
guard.as_ref().unwrap().delete(id)
}
/// Decrypt and return the password for a credential.
#[tauri::command]
pub fn decrypt_password(credential_id: i64, state: State<'_, AppState>) -> Result<String, String> {
let guard = require_unlocked!(state);
guard.as_ref().unwrap().decrypt_password(credential_id)
}
/// Decrypt and return the SSH private key and passphrase.
#[tauri::command]
pub fn decrypt_ssh_key(ssh_key_id: i64, state: State<'_, AppState>) -> Result<(String, String), String> {
let guard = require_unlocked!(state);
guard.as_ref().unwrap().decrypt_ssh_key(ssh_key_id)
}