修复: FR-S1 api_key IPC mask + 编辑不回填(防前端拿明文,P0 安全)

- ai_list_providers: 返回前 mask api_key(首尾各4+••••),IPC 不传明文给前端 realm
- ai_save_provider: api_key 空(编辑不改)→保留原 DB 值
- Settings: 编辑不回填 apiKey(留空=不改) + saveProvider 校验编辑时 apiKey 可空
- 注:DB 仍明文存储(c,keyring 加密存储)留迁移单列;本轮解 IPC 传明文 + 前端 realm 持有明文
来源 fullstack-review §2 P0 安全;cargo/vue-tsc 0 err
This commit is contained in:
2026-06-14 22:18:34 +08:00
parent f58743efbc
commit 49ac0601e1
2 changed files with 35 additions and 3 deletions

View File

@@ -265,10 +265,28 @@ pub async fn ai_chat_stop(state: State<'_, AppState>, app: AppHandle) -> Result<
// 提供商管理
// ============================================================
/// api_key 脱敏:IPC 不传明文给前端(FR-S1),保留首尾各 4 字符便于辨识
fn mask_api_key(key: &str) -> String {
let chars: Vec<char> = key.chars().collect();
if chars.len() <= 8 {
return "".repeat(chars.len());
}
let prefix: String = chars[..4].iter().collect();
let suffix: String = chars[chars.len() - 4..].iter().collect();
format!("{}••••{}", prefix, suffix)
}
/// 列出所有已配置的 AI 提供商is_default 真相源为 DB重启不丢
#[tauri::command]
pub async fn ai_list_providers(state: State<'_, AppState>) -> Result<Vec<AiProviderRecord>, String> {
state.ai_providers.list_all().await.map_err(|e| e.to_string())
let mut providers = state.ai_providers.list_all().await.map_err(|e| e.to_string())?;
// IPC 不传明文 api_key(FR-S1):前端编辑用空 apiKey 表示不改,mask 后前端 realm 不持有明文
for p in &mut providers {
if !p.api_key.is_empty() {
p.api_key = mask_api_key(&p.api_key);
}
}
Ok(providers)
}
/// 保存/更新 AI 提供商配置
@@ -300,6 +318,18 @@ pub async fn ai_save_provider(
.map_err(|e| e.to_string())?
.iter().any(|p| p.is_default),
};
// api_key 空(编辑不改 key,前端 list 拿到 mask 故留空)→保留原 DB 值(FR-S1)
let api_key = if api_key.is_empty() {
match &id {
Some(pid) => state.ai_providers.get_by_id(pid).await
.map_err(|e| e.to_string())?
.map(|p| p.api_key)
.unwrap_or_default(),
None => String::new(),
}
} else {
api_key
};
let record = AiProviderRecord {
id: id.unwrap_or_else(new_id),
name,