fix: SSH auth — decrypt credentials from vault before connecting
Some checks failed
Build & Sign Wraith / Build Windows + Sign (push) Failing after 6s
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:
parent
3a260f6a2c
commit
6c7b277494
@ -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)
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user