From caa580fce90e08d38a56ad66104ae313d81c444b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E6=80=A7=E9=A9=8A?= Date: Thu, 5 Nov 2026 15:48:20 +0800 Subject: [PATCH] Export drops threads; a broken keyring stops with the secret recoverable --- src/share.rs | 2 +- src/vault.rs | 15 +++++++++++---- src/vault_store.rs | 8 ++++++-- 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/share.rs b/src/share.rs index f7280c9..448f8a7 100644 --- a/src/share.rs +++ b/src/share.rs @@ -40,7 +40,7 @@ fn scrub(v: &mut Value) { pub fn export(b: &Bot) -> String { let mut v = serde_json::to_value(b).unwrap_or(Value::Null); if let Value::Object(m) = &mut v { - for k in ["messages", "allow_rules", "unread", "last_turn", "workdir", "pinned", "id"] { + for k in ["messages", "threads", "allow_rules", "unread", "last_turn", "workdir", "pinned", "id"] { m.remove(k); } if let Some(Value::Array(routines)) = m.get_mut("routines") { diff --git a/src/vault.rs b/src/vault.rs index 2246b95..5ee0831 100644 --- a/src/vault.rs +++ b/src/vault.rs @@ -178,7 +178,10 @@ fn onepassword(host: &str) -> Option { return None; } let items: Value = serde_json::from_slice(&list.stdout).ok()?; - let id = items.as_array()?.iter().find(|i| i["urls"].as_array().into_iter().flatten().any(|u| host_of(u["href"].as_str().unwrap_or("")).ends_with(host)))?["id"].as_str()?.to_string(); + // Match the host exactly or as a subdomain (with the dot): "notgoogle.com" must not + // answer for "google.com" (a plain ends_with fills the wrong login on a lookalike domain). + let host_match = |h: &str, want: &str| h == want || h.ends_with(&format!(".{want}")); + let id = items.as_array()?.iter().find(|i| i["urls"].as_array().into_iter().flatten().any(|u| host_match(&host_of(u["href"].as_str().unwrap_or("")), host)))?["id"].as_str()?.to_string(); let got = Command::new("op").env("PATH", &path).args(["item", "get", &id, "--fields", "label=username,label=password", "--reveal", "--format", "json"]).output().ok()?; let fields: Value = serde_json::from_slice(&got.stdout).ok()?; let arr = fields.as_array()?; @@ -412,9 +415,6 @@ pub fn fill_login(plan: &LoginPlan, submit: bool) -> Result { let (username, password, source) = match (&plan.item, &plan.other) { (Some(i), _) => { let (u, p) = crate::vault_store::login_secret(&i.id).ok_or("保險庫裡沒有這筆登入資訊")?; - if !plan.matched { - crate::vault_store::add_site(&i.id, &plan.host)?; - } (u, p, "保險庫") } (None, Some(l)) => (l.username.clone(), l.password.clone(), l.source), @@ -425,6 +425,13 @@ pub fn fill_login(plan: &LoginPlan, submit: bool) -> Result { if got["password"].as_bool() != Some(true) && got["user"].as_bool() != Some(true) { return Err("這一頁找不到帳號或密碼欄位。先打開登入頁(有些網站要先按「登入」或先填帳號、按下一步才出現密碼欄)。".into()); } + // Remember the domain only after the fill worked: adding it on a failed (or lookalike) + // page made the next visit show matched:true and skip the anti-phishing warning. + if !plan.matched { + if let Some(i) = &plan.item { + crate::vault_store::add_site(&i.id, &plan.host)?; + } + } Ok(format!( "已在 {} 填入{}{}(來自{source},你看不到密碼)。{}", plan.host, diff --git a/src/vault_store.rs b/src/vault_store.rs index 273d418..3f89d6e 100644 --- a/src/vault_store.rs +++ b/src/vault_store.rs @@ -381,8 +381,12 @@ fn store_key(key: &[u8]) -> Result<(), String> { fn key(create: bool) -> Result { let k = match read_key() { Some(k) if k.len() == 32 => k, - _ if !create => return Err("找不到保險庫的金鑰(系統鑰匙圈裡的「AnyBot vault key」)".into()), - _ => { + // An entry exists but is not a key: generating a fresh one (and -U overwriting the + // entry) would destroy the only copy of the old key — every saved password would be + // undecryptable forever. Stop and tell the person instead. + Some(_) => return Err("保險庫金鑰損毀:鑰匙圈裡的「AnyBot vault key」內容不正確。已停止動作,避免覆蓋後舊密碼永遠解不開;若你確定不需要舊密碼,可手動刪除該鑰匙圈項目再試。".into()), + None if !create => return Err("找不到保險庫的金鑰(系統鑰匙圈裡的「AnyBot vault key」)".into()), + None => { let mut k = vec![0u8; 32]; SystemRandom::new().fill(&mut k).map_err(|_| "產生金鑰失敗")?; store_key(&k)?;