diff --git a/src-tauri/src/commands/credentials.rs b/src-tauri/src/commands/credentials.rs index 71988bc..8681c5f 100644 --- a/src-tauri/src/commands/credentials.rs +++ b/src-tauri/src/commands/credentials.rs @@ -82,3 +82,17 @@ pub fn delete_credential(id: i64, state: State<'_, AppState>) -> Result<(), Stri 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 { + 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) +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index b9982b0..15dcc6d 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -172,6 +172,8 @@ pub fn run() { commands::credentials::create_password, commands::credentials::create_ssh_key, commands::credentials::delete_credential, + commands::credentials::decrypt_password, + commands::credentials::decrypt_ssh_key, commands::ssh_commands::connect_ssh, commands::ssh_commands::connect_ssh_with_key, commands::ssh_commands::ssh_write, diff --git a/src/stores/session.store.ts b/src/stores/session.store.ts index 250cbf5..56ab124 100644 --- a/src/stores/session.store.ts +++ b/src/stores/session.store.ts @@ -98,25 +98,60 @@ export const useSessionStore = defineStore("session", () => { try { if (conn.protocol === "ssh") { let sessionId: string; - let resolvedUsername: string | undefined; + let resolvedUsername = ""; let resolvedPassword = ""; - // Extract stored username from connection options JSON if present - if (conn.options) { + // If connection has a linked credential, decrypt it from the vault + if (conn.credentialId) { try { - const opts = JSON.parse(conn.options); - if (opts?.username) resolvedUsername = opts.username; - if (opts?.password) resolvedPassword = opts.password; - } catch { - // ignore malformed options + const cred = await invoke<{ id: number; name: string; username: string | null; credentialType: string; sshKeyId: number | null }>("list_credentials") + .then((creds: any[]) => creds.find((c: any) => c.id === conn.credentialId)); + + if (cred) { + resolvedUsername = cred.username ?? ""; + + if (cred.credentialType === "ssh_key" && cred.sshKeyId) { + // SSH key auth — decrypt key from vault + const [privateKey, passphrase] = await invoke<[string, string]>("decrypt_ssh_key", { sshKeyId: cred.sshKeyId }); + sessionId = await invoke("connect_ssh_with_key", { + hostname: conn.hostname, + port: conn.port, + username: resolvedUsername, + privateKeyPem: privateKey, + passphrase: passphrase || null, + cols: 120, + rows: 40, + }); + + sessions.value.push({ + id: sessionId, + connectionId, + name: disambiguatedName(conn.name, connectionId), + protocol: "ssh", + active: true, + username: resolvedUsername, + }); + activeSessionId.value = sessionId; + return; // early return — key auth handled + } else { + // Password auth — decrypt password from vault + resolvedPassword = await invoke("decrypt_password", { credentialId: cred.id }); + } + } + } catch (credErr) { + console.warn("Failed to resolve credential, will prompt:", credErr); } } try { + if (!resolvedUsername) { + // No credential linked — prompt immediately + throw new Error("NO_CREDENTIALS"); + } sessionId = await invoke("connect_ssh", { hostname: conn.hostname, port: conn.port, - username: resolvedUsername ?? "", + username: resolvedUsername, password: resolvedPassword, cols: 120, rows: 40, @@ -129,10 +164,11 @@ export const useSessionStore = defineStore("session", () => { : String(sshErr); // If no credentials or auth failed, prompt for username/password - if (errMsg.includes("NO_CREDENTIALS") || errMsg.includes("unable to authenticate") || errMsg.includes("authentication")) { - const username = prompt(`Username for ${conn.hostname}:`, "root"); + const errLower = errMsg.toLowerCase(); + if (errLower.includes("no_credentials") || errLower.includes("unable to authenticate") || errLower.includes("authentication") || errLower.includes("rejected")) { + const username = window.prompt(`Username for ${conn.hostname}:`, resolvedUsername || "root"); if (!username) throw new Error("Connection cancelled"); - const password = prompt(`Password for ${username}@${conn.hostname}:`); + const password = window.prompt(`Password for ${username}@${conn.hostname}:`); if (password === null) throw new Error("Connection cancelled"); resolvedUsername = username;