feat: 添加加密凭据恢复与 Windows 会话撤销

This commit is contained in:
2026-09-08 14:46:30 +08:00
parent a7c4261ccd
commit db2038f042
12 changed files with 554 additions and 56 deletions
+97
View File
@@ -22,6 +22,8 @@ struct Host {
recent: Mutex<Option<RecentVaultStore>>,
core: Arc<Mutex<Option<CoreSupervisor>>>,
credentials: Arc<Mutex<Option<CredentialBroker>>>,
#[cfg(windows)]
session_monitor: Mutex<Option<notesagent_host::session_lock::SessionMonitor>>,
streams: Arc<Mutex<HashMap<String, tauri::async_runtime::JoinHandle<()>>>>,
}
@@ -378,6 +380,15 @@ fn credentials_status(host: State<'_, Host>) -> Result<serde_json::Value, String
#[tauri::command]
async fn credentials_unlock(host: State<'_, Host>, password: String) -> Result<(), String> {
#[cfg(windows)]
if host
.session_monitor
.lock()
.map_err(|_| "HOST_BUSY")?
.is_none()
{
return Err("SESSION_MONITOR_UNAVAILABLE".into());
}
let broker = host.credentials.clone();
let password = Zeroizing::new(password.into_bytes());
tauri::async_runtime::spawn_blocking(move || {
@@ -455,6 +466,62 @@ async fn credentials_change_password(
.map_err(|_| "HOST_BUSY")?
}
#[tauri::command]
async fn credentials_backup(host: State<'_, Host>) -> Result<bool, String> {
let Some(path) = rfd::FileDialog::new()
.set_title("导出加密凭据备份(请选择新文件)")
.set_file_name("OpenNexus.onxcred")
.add_filter("OpenNexus credential backup", &["onxcred"])
.save_file()
else {
return Ok(false);
};
let broker = host.credentials.clone();
tauri::async_runtime::spawn_blocking(move || {
broker
.lock()
.map_err(|_| "HOST_BUSY")?
.as_ref()
.ok_or("HOST_NOT_READY")?
.backup(&path)?;
Ok(true)
})
.await
.map_err(|_| "HOST_BUSY")?
}
#[tauri::command]
async fn credentials_restore(
host: State<'_, Host>,
password: String,
) -> Result<Option<usize>, String> {
let password = Zeroizing::new(password.into_bytes());
let Some(path) = rfd::FileDialog::new()
.set_title("选择加密凭据备份")
.add_filter("OpenNexus credential backup", &["onxcred"])
.pick_file()
else {
return Ok(None);
};
if rfd::MessageDialog::new().set_title("恢复凭据备份")
.set_description("恢复将替换本机凭据库。当前加密文件会另存为恢复前备份;恢复后仍需解锁。笔记不会被替换。是否继续?")
.set_buttons(rfd::MessageButtons::YesNo).show() != rfd::MessageDialogResult::Yes {
return Ok(None);
}
let broker = host.credentials.clone();
tauri::async_runtime::spawn_blocking(move || {
broker
.lock()
.map_err(|_| "HOST_BUSY")?
.as_mut()
.ok_or("HOST_NOT_READY")?
.restore(&path, password)
.map(Some)
})
.await
.map_err(|_| "HOST_BUSY")?
}
#[tauri::command]
fn editor_capabilities(app: tauri::AppHandle, metadata_enabled: bool) -> Result<(), String> {
app.state::<tauri::menu::MenuItem<tauri::Wry>>()
@@ -602,6 +669,34 @@ fn main() {
.map_err(|_| std::io::Error::other("HOST_BUSY"))? = Some(CredentialBroker::new(
app.path().app_data_dir()?.join("credentials/stronghold.v1"),
));
#[cfg(windows)]
{
let signal = credential_state
.lock()
.map_err(|_| std::io::Error::other("HOST_BUSY"))?
.as_ref()
.ok_or_else(|| std::io::Error::other("HOST_NOT_READY"))?
.lock_signal();
*app.state::<Host>()
.session_monitor
.lock()
.map_err(|_| std::io::Error::other("HOST_BUSY"))? =
notesagent_host::session_lock::SessionMonitor::start(signal).ok();
}
let weak_credentials = Arc::downgrade(&credential_state);
std::thread::spawn(move || {
while let Some(state) = weak_credentials.upgrade() {
if let Ok(mut broker) = state.try_lock() {
if let Some(broker) = broker.as_mut() {
if broker.is_locked() {
broker.lock();
}
}
}
drop(state);
std::thread::sleep(Duration::from_millis(200));
}
});
let data_dir = app.path().app_data_dir()?.join("core-data");
// Debug builds use this worktree's interpreter; release builds only use bundled Core.
let core = if cfg!(debug_assertions) {
@@ -676,6 +771,8 @@ fn main() {
credentials_lock,
credentials_change_password,
credentials_import,
credentials_backup,
credentials_restore,
core_request,
core_request_prepare,
core_request_cancel,