From c30fc33305b1e91cbdde534ee0b97d7b241281e1 Mon Sep 17 00:00:00 2001 From: 64673a <96230516+64673a@users.noreply.github.com> Date: Wed, 9 Sep 2026 16:29:31 +0800 Subject: [PATCH] feat(ssh): prompt to reset a changed host fingerprint --- .../components/ssh/SshHostKeyPromptDialog.vue | 72 ++++++++- .../__tests__/SshHostKeyPromptDialog.spec.ts | 71 ++++++++ apps/desktop/src/i18n/locales/en.ts | 8 + apps/desktop/src/i18n/locales/zh-CN.ts | 8 + apps/desktop/src/i18n/locales/zh-TW.ts | 8 + crates/dbx-core/src/db/sqlite_worker.rs | 1 + crates/dbx-core/src/db/ssh_host_key.rs | 91 +++++++++-- crates/dbx-core/src/db/ssh_prompt.rs | 30 +++- crates/dbx-core/src/db/ssh_tunnel.rs | 153 ++++++++++++++++-- crates/dbx-web/src/ssh_prompt.rs | 1 + docs/content/docs/ssh-tunnel.cn.mdx | 2 +- docs/content/docs/ssh-tunnel.mdx | 2 +- docs/content/docs/ssh-tunnel.tr.mdx | 2 +- src-tauri/src/commands/ssh_prompt.rs | 1 + 14 files changed, 418 insertions(+), 32 deletions(-) diff --git a/apps/desktop/src/components/ssh/SshHostKeyPromptDialog.vue b/apps/desktop/src/components/ssh/SshHostKeyPromptDialog.vue index 3ee2d07457..5e93330ab3 100644 --- a/apps/desktop/src/components/ssh/SshHostKeyPromptDialog.vue +++ b/apps/desktop/src/components/ssh/SshHostKeyPromptDialog.vue @@ -2,6 +2,7 @@ import { ref, computed, onMounted, onBeforeUnmount } from "vue"; import { useI18n } from "vue-i18n"; import { invoke } from "@tauri-apps/api/core"; +import { TriangleAlert } from "@lucide/vue"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { useToast } from "@/composables/useToast"; @@ -11,11 +12,12 @@ import { apiUrl } from "@/lib/common/webPath"; interface SshPromptRequest { id: string; - kind: "HostKeyVerify" | "SecretInput" | "WorkerUploadConsent"; + kind: "HostKeyVerify" | "HostKeyChanged" | "SecretInput" | "WorkerUploadConsent"; host: string; port: number; key_type?: string | null; fingerprint?: string | null; + previous_fingerprint?: string | null; prompt?: string | null; echo?: boolean; } @@ -38,6 +40,19 @@ const queue = ref([]); const current = computed(() => queue.value[0] ?? null); const isSecretPrompt = computed(() => current.value?.kind === "SecretInput"); const isWorkerUploadPrompt = computed(() => current.value?.kind === "WorkerUploadConsent"); +const isHostKeyChanged = computed(() => current.value?.kind === "HostKeyChanged"); +const titleKey = computed(() => { + if (isSecretPrompt.value) return "connection.sshInteractiveTitle"; + if (isWorkerUploadPrompt.value) return "connection.sshWorkerUploadConsentTitle"; + if (isHostKeyChanged.value) return "connection.sshHostKeyChangedTitle"; + return "connection.sshHostKeyVerifyTitle"; +}); +const messageKey = computed(() => { + if (isSecretPrompt.value) return "connection.sshInteractiveMessage"; + if (isWorkerUploadPrompt.value) return "connection.sshWorkerUploadConsentMessage"; + if (isHostKeyChanged.value) return "connection.sshHostKeyChangedMessage"; + return "connection.sshHostKeyVerifyMessage"; +}); const visible = ref(false); const remember = ref(true); @@ -224,10 +239,24 @@ function resetPromptState() { secretCode.value = ""; } +function fingerprintBody(value: string | null | undefined): string { + return (value ?? "").replace(/^SHA256:/i, ""); +} + function accept() { void resolve("accept"); } +function acceptSessionOnly() { + remember.value = false; + void resolve("accept"); +} + +function acceptAndUpdate() { + remember.value = true; + void resolve("accept"); +} + function reject() { void resolve("reject"); } @@ -250,16 +279,35 @@ function submitSecret() { 100). --> - - {{ t(isSecretPrompt ? "connection.sshInteractiveTitle" : isWorkerUploadPrompt ? "connection.sshWorkerUploadConsentTitle" : "connection.sshHostKeyVerifyTitle") }} - + {{ t(titleKey) }} - {{ t(isSecretPrompt ? "connection.sshInteractiveMessage" : isWorkerUploadPrompt ? "connection.sshWorkerUploadConsentMessage" : "connection.sshHostKeyVerifyMessage", { host: current?.host ?? "", port: current?.port ?? "" }) }} + {{ t(messageKey, { host: current?.host ?? "", port: current?.port ?? "" }) }}
-
+
+
+
+
+
+
{{ t("connection.sshHostKeyChangedCurrent", { keyType: current.key_type || "—" }) }}
+
{{ fingerprintBody(current.fingerprint) || "—" }}
+
+
+
{{ t("connection.sshHostKeyChangedSaved") }}
+
{{ fingerprintBody(current.previous_fingerprint) || "—" }}
+
+

{{ t("connection.sshHostKeyChangedWarning") }}

+
+
+ +
{{ current.kind === "WorkerUploadConsent" ? t("connection.sshWorkerUploadConsentDigest") : t("connection.sshHostKeyVerifyKeyType") }}
{{ current.kind === "WorkerUploadConsent" ? current.fingerprint || "—" : current.key_type || "—" }}
@@ -292,9 +340,17 @@ function submitSecret() { -