From dd1f7950e51b57693d7b0d9e70a07e1399e12dce Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Sat, 2 May 2026 16:45:17 +0800 Subject: [PATCH] feat: use file-based secret store in debug mode to avoid macOS Keychain prompts During development, every Rust recompile changes the binary signature, causing macOS to invalidate Keychain "Always Allow" and prompt for the password on every hot reload. Switch to a JSON file store in debug builds so dev workflow is uninterrupted; release builds still use the system keychain. --- src-tauri/src/commands/connection.rs | 10 +-- src-tauri/src/commands/connection_secrets.rs | 64 ++++++++++++++++++-- 2 files changed, 63 insertions(+), 11 deletions(-) diff --git a/src-tauri/src/commands/connection.rs b/src-tauri/src/commands/connection.rs index 8bc87690ee..7706ed528e 100644 --- a/src-tauri/src/commands/connection.rs +++ b/src-tauri/src/commands/connection.rs @@ -4,7 +4,7 @@ use tauri::{AppHandle, Manager, State}; use tokio::sync::Mutex; use crate::commands::connection_secrets::{ - load_connections_from_file, save_connections_to_file, KeyringConnectionSecretStore, + create_secret_store, load_connections_from_file, save_connections_to_file, }; use crate::commands::query_cancel::RunningQueries; use crate::db; @@ -233,15 +233,15 @@ pub async fn save_connections( configs: Vec, ) -> Result<(), String> { let path = connections_file(&app)?; - let store = KeyringConnectionSecretStore; - save_connections_to_file(&path, &configs, &store) + let store = create_secret_store(&app); + save_connections_to_file(&path, &configs, &*store) } #[tauri::command] pub async fn load_connections(app: AppHandle) -> Result, String> { let path = connections_file(&app)?; - let store = KeyringConnectionSecretStore; - load_connections_from_file(&path, &store) + let store = create_secret_store(&app); + load_connections_from_file(&path, &*store) } #[tauri::command] diff --git a/src-tauri/src/commands/connection_secrets.rs b/src-tauri/src/commands/connection_secrets.rs index c720d261f2..4dec9232d9 100644 --- a/src-tauri/src/commands/connection_secrets.rs +++ b/src-tauri/src/commands/connection_secrets.rs @@ -1,6 +1,8 @@ use crate::models::connection::ConnectionConfig; +use std::collections::HashMap; use std::collections::HashSet; -use std::path::Path; +use std::path::{Path, PathBuf}; +use tauri::{AppHandle, Manager}; pub(super) const MAIN_PASSWORD_KEY: &str = "password"; pub(super) const SSH_PASSWORD_KEY: &str = "ssh_password"; @@ -42,10 +44,60 @@ impl ConnectionSecretStore for KeyringConnectionSecretStore { } } +pub(super) struct FileSecretStore { + path: PathBuf, +} + +impl FileSecretStore { + fn new(path: PathBuf) -> Self { + Self { path } + } + + fn read_store(&self) -> HashMap { + std::fs::read_to_string(&self.path) + .ok() + .and_then(|json| serde_json::from_str(&json).ok()) + .unwrap_or_default() + } + + fn write_store(&self, map: &HashMap) -> Result<(), String> { + let json = serde_json::to_string_pretty(map).map_err(|e| e.to_string())?; + std::fs::write(&self.path, json).map_err(|e| e.to_string()) + } +} + +impl ConnectionSecretStore for FileSecretStore { + fn set_secret(&self, connection_id: &str, key: &str, secret: &str) -> Result<(), String> { + let mut map = self.read_store(); + map.insert(secret_account(connection_id, key), secret.to_string()); + self.write_store(&map) + } + + fn get_secret(&self, connection_id: &str, key: &str) -> Result, String> { + Ok(self.read_store().get(&secret_account(connection_id, key)).cloned()) + } + + fn delete_secret(&self, connection_id: &str, key: &str) -> Result<(), String> { + let mut map = self.read_store(); + map.remove(&secret_account(connection_id, key)); + self.write_store(&map) + } +} + +pub(super) fn create_secret_store(app: &AppHandle) -> Box { + if cfg!(debug_assertions) { + let dir = app.path().app_data_dir().expect("failed to resolve app data dir"); + std::fs::create_dir_all(&dir).ok(); + Box::new(FileSecretStore::new(dir.join("secrets.json"))) + } else { + Box::new(KeyringConnectionSecretStore) + } +} + pub(super) fn save_connections_to_file( path: &Path, configs: &[ConnectionConfig], - store: &impl ConnectionSecretStore, + store: &dyn ConnectionSecretStore, ) -> Result<(), String> { delete_removed_connection_secrets(path, configs, store)?; for config in configs { @@ -64,7 +116,7 @@ pub(super) fn save_connections_to_file( pub(super) fn load_connections_from_file( path: &Path, - store: &impl ConnectionSecretStore, + store: &dyn ConnectionSecretStore, ) -> Result, String> { if !path.exists() { return Ok(vec![]); @@ -118,7 +170,7 @@ pub(super) fn load_connections_from_file( fn delete_removed_connection_secrets( path: &Path, configs: &[ConnectionConfig], - store: &impl ConnectionSecretStore, + store: &dyn ConnectionSecretStore, ) -> Result<(), String> { if !path.exists() { return Ok(()); @@ -141,7 +193,7 @@ fn delete_removed_connection_secrets( } fn persist_secret( - store: &impl ConnectionSecretStore, + store: &dyn ConnectionSecretStore, connection_id: &str, key: &str, secret: &str, @@ -154,7 +206,7 @@ fn persist_secret( } fn persist_optional_secret( - store: &impl ConnectionSecretStore, + store: &dyn ConnectionSecretStore, connection_id: &str, key: &str, secret: Option<&str>,