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);
|
let guard = require_unlocked!(state);
|
||||||
guard.as_ref().unwrap().delete(id)
|
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_password,
|
||||||
commands::credentials::create_ssh_key,
|
commands::credentials::create_ssh_key,
|
||||||
commands::credentials::delete_credential,
|
commands::credentials::delete_credential,
|
||||||
|
commands::credentials::decrypt_password,
|
||||||
|
commands::credentials::decrypt_ssh_key,
|
||||||
commands::ssh_commands::connect_ssh,
|
commands::ssh_commands::connect_ssh,
|
||||||
commands::ssh_commands::connect_ssh_with_key,
|
commands::ssh_commands::connect_ssh_with_key,
|
||||||
commands::ssh_commands::ssh_write,
|
commands::ssh_commands::ssh_write,
|
||||||
|
|||||||
@ -98,25 +98,60 @@ export const useSessionStore = defineStore("session", () => {
|
|||||||
try {
|
try {
|
||||||
if (conn.protocol === "ssh") {
|
if (conn.protocol === "ssh") {
|
||||||
let sessionId: string;
|
let sessionId: string;
|
||||||
let resolvedUsername: string | undefined;
|
let resolvedUsername = "";
|
||||||
let resolvedPassword = "";
|
let resolvedPassword = "";
|
||||||
|
|
||||||
// Extract stored username from connection options JSON if present
|
// If connection has a linked credential, decrypt it from the vault
|
||||||
if (conn.options) {
|
if (conn.credentialId) {
|
||||||
try {
|
try {
|
||||||
const opts = JSON.parse(conn.options);
|
const cred = await invoke<{ id: number; name: string; username: string | null; credentialType: string; sshKeyId: number | null }>("list_credentials")
|
||||||
if (opts?.username) resolvedUsername = opts.username;
|
.then((creds: any[]) => creds.find((c: any) => c.id === conn.credentialId));
|
||||||
if (opts?.password) resolvedPassword = opts.password;
|
|
||||||
} catch {
|
if (cred) {
|
||||||
// ignore malformed options
|
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 {
|
try {
|
||||||
|
if (!resolvedUsername) {
|
||||||
|
// No credential linked — prompt immediately
|
||||||
|
throw new Error("NO_CREDENTIALS");
|
||||||
|
}
|
||||||
sessionId = await invoke<string>("connect_ssh", {
|
sessionId = await invoke<string>("connect_ssh", {
|
||||||
hostname: conn.hostname,
|
hostname: conn.hostname,
|
||||||
port: conn.port,
|
port: conn.port,
|
||||||
username: resolvedUsername ?? "",
|
username: resolvedUsername,
|
||||||
password: resolvedPassword,
|
password: resolvedPassword,
|
||||||
cols: 120,
|
cols: 120,
|
||||||
rows: 40,
|
rows: 40,
|
||||||
@ -129,10 +164,11 @@ export const useSessionStore = defineStore("session", () => {
|
|||||||
: String(sshErr);
|
: String(sshErr);
|
||||||
|
|
||||||
// If no credentials or auth failed, prompt for username/password
|
// If no credentials or auth failed, prompt for username/password
|
||||||
if (errMsg.includes("NO_CREDENTIALS") || errMsg.includes("unable to authenticate") || errMsg.includes("authentication")) {
|
const errLower = errMsg.toLowerCase();
|
||||||
const username = prompt(`Username for ${conn.hostname}:`, "root");
|
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");
|
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");
|
if (password === null) throw new Error("Connection cancelled");
|
||||||
|
|
||||||
resolvedUsername = username;
|
resolvedUsername = username;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user