use sha2::{Digest, Sha256}; use url::Url; const CREDENTIAL_SERVICE: &str = "cn.huapont.ctms.desktop.session"; fn credential_account(server_origin: &str) -> Result { let parsed = Url::parse(server_origin).map_err(|_| "服务器地址格式不正确".to_string())?; let is_https = parsed.scheme() == "https"; let is_local_http = parsed.scheme() == "http" && matches!( parsed.host_str(), Some("localhost") | Some("127.0.0.1") | Some("::1") ); if !is_https && !is_local_http { return Err("非本地服务必须使用 HTTPS".to_string()); } if parsed.username() != "" || parsed.password().is_some() { return Err("服务器地址不能包含凭据".to_string()); } let origin = parsed.origin().ascii_serialization(); let digest = Sha256::digest(origin.as_bytes()); Ok(format!("{digest:x}")) } #[cfg(any(target_os = "macos", windows))] fn get_entry(server_origin: &str) -> Result { let account = credential_account(server_origin)?; keyring::Entry::new(CREDENTIAL_SERVICE, &account) .map_err(|error| format!("无法访问系统凭据库:{error}")) } #[tauri::command] pub async fn credential_get(server_origin: String) -> Result, String> { tauri::async_runtime::spawn_blocking(move || { #[cfg(any(target_os = "macos", windows))] { let entry = get_entry(&server_origin)?; return match entry.get_password() { Ok(token) => Ok(Some(token)), Err(keyring::Error::NoEntry) => Ok(None), Err(error) => Err(format!("读取系统凭据失败:{error}")), }; } #[cfg(not(any(target_os = "macos", windows)))] { let _ = credential_account(&server_origin)?; Err("当前平台不支持系统凭据存储".to_string()) } }) .await .map_err(|error| format!("读取系统凭据任务失败:{error}"))? } #[tauri::command] pub async fn credential_set(server_origin: String, token: String) -> Result<(), String> { if token.trim().is_empty() { return Err("拒绝保存空凭据".to_string()); } tauri::async_runtime::spawn_blocking(move || { #[cfg(any(target_os = "macos", windows))] { return get_entry(&server_origin)? .set_password(&token) .map_err(|error| format!("保存系统凭据失败:{error}")); } #[cfg(not(any(target_os = "macos", windows)))] { let _ = credential_account(&server_origin)?; Err("当前平台不支持系统凭据存储".to_string()) } }) .await .map_err(|error| format!("保存系统凭据任务失败:{error}"))? } #[tauri::command] pub async fn credential_delete(server_origin: String) -> Result<(), String> { tauri::async_runtime::spawn_blocking(move || { #[cfg(any(target_os = "macos", windows))] { let entry = get_entry(&server_origin)?; return match entry.delete_credential() { Ok(()) | Err(keyring::Error::NoEntry) => Ok(()), Err(error) => Err(format!("删除系统凭据失败:{error}")), }; } #[cfg(not(any(target_os = "macos", windows)))] { let _ = credential_account(&server_origin)?; Err("当前平台不支持系统凭据存储".to_string()) } }) .await .map_err(|error| format!("删除系统凭据任务失败:{error}"))? } #[cfg(test)] mod tests { use super::credential_account; #[test] fn account_is_stable_for_same_origin() { assert_eq!( credential_account("https://ctms.example.com/path").unwrap(), credential_account("https://ctms.example.com/other").unwrap() ); } #[test] fn rejects_insecure_remote_origin() { assert!(credential_account("http://ctms.example.com").is_err()); assert!(credential_account("http://localhost:8000").is_ok()); } #[test] fn rejects_origins_with_embedded_credentials() { assert!(credential_account("https://user:secret@ctms.example.com").is_err()); } #[test] fn account_does_not_expose_server_origin() { let account = credential_account("https://ctms.example.com/path").unwrap(); assert!(!account.contains("ctms.example.com")); assert!(!account.contains("https")); } }