128 lines
4.4 KiB
Rust
128 lines
4.4 KiB
Rust
use sha2::{Digest, Sha256};
|
|
use url::Url;
|
|
|
|
const CREDENTIAL_SERVICE: &str = "cn.huapont.ctms.desktop.session";
|
|
|
|
fn credential_account(server_origin: &str) -> Result<String, String> {
|
|
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<keyring::Entry, String> {
|
|
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<Option<String>, 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"));
|
|
}
|
|
}
|