fix: SSH auth — decrypt credentials from vault before connecting
Some checks failed
Build & Sign Wraith / Build Windows + Sign (push) Failing after 6s

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>
This commit is contained in:
Vantz Stockwell 2026-03-18 03:04:47 -04:00
parent 3a260f6a2c
commit 6c7b277494
3 changed files with 64 additions and 12 deletions

View File

@ -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<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)
}

View File

@ -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,

View File

@ -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<string>("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<string>("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<string>("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;