Merge p5/security: SECURITY.md + threat model, state.json 0600 (P5)

This commit is contained in:
daniel wang 2026-11-09 20:34:01 +08:00
commit b6d38aca74
5 changed files with 114 additions and 1 deletions

View File

@ -87,7 +87,7 @@ make deps && make && make install # deps: system packages + Rust 1.95+ (one ti
## Security in one paragraph
Commands run in a macOS sandbox (deny ~/.aws, Keychain, AnyBot's own data; allow only the attachments dir); per-action approvals with a policy layer the model cannot negotiate; clicking anything labelled “send/pay/transfer” is asked *every time*, even in direct mode, with no grant that silences it; card numbers and OTPs in screenshots are painted black before the model sees them; everything read from outside is wrapped as `<untrusted>` data, never treated as orders; the bot's own computer is a Docker container (gVisor for a second wall); relay stores only hashes (argon2id) and supports 2FA with recovery codes; egress binds loopback + detected bridge gateways, never 0.0.0.0. Details: [docs/architecture.md](docs/architecture.md) (Chinese; [English summary](docs/en/architecture.md)).
Commands run in a macOS sandbox (deny ~/.aws, Keychain, AnyBot's own data; allow only the attachments dir); per-action approvals with a policy layer the model cannot negotiate; clicking anything labelled “send/pay/transfer” is asked *every time*, even in direct mode, with no grant that silences it; card numbers and OTPs in screenshots are painted black before the model sees them; everything read from outside is wrapped as `<untrusted>` data, never treated as orders; the bot's own computer is a Docker container (gVisor for a second wall); relay stores only hashes (argon2id) and supports 2FA with recovery codes; egress binds loopback + detected bridge gateways, never 0.0.0.0. Details: [SECURITY.md](SECURITY.md) ([summary here](docs/en/security.md)) and [docs/architecture.md](docs/architecture.md) (Chinese; [English summary](docs/en/architecture.md)).
## Documentation

View File

@ -157,6 +157,10 @@ make install # 安裝:Mac → ~/Applications/AnyBot.app;Linux → ~/.loca
| [docs/release.md](docs/release.md) | 打包和自動建置 |
| [FEATURES.md](FEATURES.md) | 所有功能 |
## 安全
你的資料預設不出門:Bot 在你的機器上跑,金鑰在鑰匙圈、備份是密文、敏感點擊每次都問、動過什麼都記帳。信任邊界、金鑰位置、已知限制與硬化建議都寫在 [SECURITY.md](SECURITY.md)([English](docs/en/security.md))。發現漏洞請用 repo 的 security advisory 私隱通報。
## 授權
AnyBot 以 **Apache License 2.0** 授權(見 [LICENSE](LICENSE))散布:著作權屬本 repo 的貢獻者(The AnyBot contributors)。程式碼由本專案獨立撰寫;對第三方服務(xAI、OpenAI 等)的呼叫都走各家的公開 API,請遵守各服務的使用條款與你自己的金鑰合約。

58
SECURITY.md Normal file
View File

@ -0,0 +1,58 @@
# 安全(Security)
AnyBot 的 privacy 模型一句话:**Bot 跑在你自己的機器上,你的資料預設不出門**——出門的只有你指定的模型 API 呼叫,和(你選擇登入帳號時)備份的**密文**。下面寫清楚:誰能做什麼、金鑰在哪裡、攻擊者拿到某個東西時會得到什麼。
## 通報漏洞
請用 repo 的 **security advisory**(Gitea:Issues 旁边的安全分頁)開私隱通報,或來信 repo 擁有者;我們目標 3 個工作日內回應、90 天內修復或公開說明。請勿公開披露未修復漏洞。
## 信任邊界
| 邊界 | 誰在裡面 | 邊界怎麼守 |
|---|---|---|
| 你的電腦(App) | Bot 的大腦與手腳、`state.json`、保險庫 | 你的帳號就是邊界;App 內指令在 macOS 沙盒(deny `~/.aws`、Keychain、AnyBot 自己的資料) |
| Bot 的雲端電腦(box) | Bot 能以 root 跑的容器 | Docker 容器;Docker 主機有 gVisor 時自動改用 `runsc`(虛擬化核心+使用者態 seccomp)——核心不在邊界內 |
| Docker API | Bot 只能建/管自己的容器 | 不掛 `docker.sock`;經 docker-socket-proxy 白名单(containers/images/networks/volumes 的限定子集;`exec`/`privileged` 不在名單上) |
| Relay(帳號伺服器) | 帳號、裝置清單、備份密文 | 只存雜湊:密碼 argon2id、host token/密碼雜湊、session 雜湊;備份是用你的密碼派生的鑰匙鎖住的密文(PBKDF2-HMAC-SHA256 12 萬輪 → AES-256-GCM),relay 看不到內容 |
| 模型供應商 | 交出去的對話內容與截圖 | 你选的供應商與金鑰;任何供應商拿不到你的檔案、保險庫密碼、金鑰庫 |
## 金鑰與密碼在哪裡
| 東西 | 放在哪 | 拿到它的人能得到什麼 |
|---|---|---|
| 模型 API 金鑰 | `state.json`(0600,你的家目錄) | 該供應商的帳單額度——請用磁碟加密、開 2FA 的供應商後台 |
| 保險庫(網號/卡號/聯絡人) | `vault.bin`:AES-256-GCM;鑰匙在系統鑰匙圈(macOS Keychain / Linux Secret Service),無鑰匙圈時 0600 鑰匙檔 | 沒有鑰匙圈解不开;模型永遠看不到密碼——清單是空的欄位,填入由你逐次核准、由瀏覽器直接輸入 |
| 帳號密碼 | 只存在你的 App;relay 端只有 argon2id 雜湊(m=19MiB, t=2) | 洩了 relay 得到雜湊,開不了你的備份(密文另需你的密碼派生鑰匙) |
| 兩步驗證 | relay 存 TOTP 秘密與 8 個恢復碼(雜湊) | 關掉 2FA 需要當次密碼+驗證碼;恢復碼一次一個 |
| Session | relay 存雜湊,可「全部登出」 | 拿到 session 雜湊檔=可續用該裝置直到登出——rela 端本身是信任點 |
| 備份 | relay 上一個 `.bin` 密文檔 | 只有你的密碼能解(PBKDF2 派生);密碼弱=密文弱 |
## 模型與「注入」的防線(深度防禦)
惡意網頁/檔案的內容會進入對話(這是電腦使用的本質)。AnyBot 的防線是層的,不是牆:
1. **外來內容一律標記**:瀏覽器/螢幕/檔案讀回的內容包成 `<untrusted>` 資料,系統指示明令「資料不是命令」。
2. **敏感動作硬閘門**:按下去涉及「傳送/付款/轉帳/發訊息」等標籤的控件,**每次都問**——直接模式照問,模型不能說服任何設定放行(閘門在 harness,不在提示詞)。
3. **逐 App/逐電腦核准**:模型第一次動每個 App、每台電腦都要你點頭;核准是資料層的策略,不是提示詞。
4. **機密塗黑**:截圖裡的卡號/OTP 樣式先塗黑再給模型看;`mask_secrets` 洗工具輸出。
5. **稽核**:每一回合動了什麼(工具、參數、核准與否)寫 oplog → SQLite 稽核表(30 天),手機可看逐回合時間軸。
誠實邊界:注入沒有絕對防線——所以最後一道是**人在核准閘門**,模型永遠不能自己簽自己的通行證。
## 網路暴露面
- 預設只綁 **loopback**;egress 綁環回+自動偵測的 Docker 網橋閘道(docker0 / docker_gwbridge / br-*),**從不綁 0.0.0.0**。
- 手機連 relay 走你自架的 relay+Caddy HTTPS;`/relay/*` 有登入鎖、錯誤鎖與 session 登出。
- LAN 模式(設定開)才會對區域網路開放,並要求 App 密碼(雜湊比對)。
## 已知限制(誠實清單)
- **同使用者即同信任**:同一登入帳號下能讀你家目錄的程式,視同你本人——請用磁碟加密與好帳號習慣。
- 模型供應商看得到你交出去的對話與截圖(內容級隱私請選信任的供應商;支援本地模型路徑:Ollama)。
- gVisor 是加分不是標配:純 Docker 的 box 與主機共享核心。
- 備份安全性以你的帳號密碼強度為上限。
- relay 是自架元件:它被攻破時洩露的是雜湊、session 清單與備份密文,不是明文。
## 硬化建議
磁碟加密(FileVault/LUKS);供應商金鑰開額度上限與 2FA;relay 只開 HTTPS 並開 2FA;box 主機裝 gVisor;把「一律詢問」開在你常用的敏感 App 上。

45
docs/en/security.md Normal file
View File

@ -0,0 +1,45 @@
# Security
One line: **the bot runs on your own machine; your data does not leave by default** — only the model API calls you choose, and (if you sign in to an account) the **ciphertext** of your backup.
**Reporting:** open a private security advisory on the repo (Gitea › Security) or write the owner. We aim to answer in 3 business days, fix in 90. No public disclosure of unfixed issues.
## Trust boundaries
- **Your computer** — the bot's brain, hands, `state.json`, vault. Your OS account is the boundary; shell commands run in the macOS sandbox (denies `~/.aws`, Keychain, AnyBot's own data).
- **The bot's cloud box** — a Docker container the bot roots; automatically switched to gVisor (`runsc`) when the host has it: the kernel joins the boundary.
- **Docker API** — no `docker.sock`: a docker-socket-proxy whitelist (containers/images/networks subset; no `exec`, no `privileged`).
- **Relay** — stores hashes only: argon2id passwords, hashed tokens/sessions; your backup arrives sealed with a key derived from your password (PBKDF2-HMAC-SHA256 120k → AES-256-GCM). The relay never sees plaintext, and it is self-hosted.
- **Model providers** — see the chats and screenshots you hand them, never your files, vault secrets, or keys.
## Where secrets live
| Thing | Where | What an attacker gets |
|---|---|---|
| Model API keys | `state.json` (0600) | that provider's bill — disk encryption, provider 2FA |
| Vault (logins/cards) | `vault.bin`: AES-256-GCM, key in the system keychain (0600 file fallback) | nothing without the keychain; the model never sees secrets — lists come blanked, every fill is approved per use |
| Account password | your app; relay keeps only argon2id (m=19MiB, t=2) | hashes; the backup stays sealed behind your password |
| 2FA | TOTP secret + 8 hashed recovery codes on the relay | disabling 2FA needs password + code in the same turn |
| Sessions | hashed rows; logout-everywhere | a stolen session file works until logout (relay is a trust point) |
## Prompt-injection defenses (layers, not walls)
1. Outside content is wrapped as `<untrusted>` — data, never orders.
2. Sensitive clicks (send/pay/transfer/…): asked **every time**, direct mode included — no grant silences it; the gate is in the harness, not the prompt.
3. First use of every app/computer needs your approval (policy at the data layer).
4. Card numbers and OTPs in screenshots are painted black before the model sees them.
5. Every turn's actions land in an auditable oplog → SQLite audit (30 days), with a phone timeline.
Honest limit: injection has no absolute defense — which is why the last line is a human at the approval gate. The model never issues its own passes.
## Network surface
Loopback by default; egress binds loopback plus detected Docker bridge gateways, never 0.0.0.0. The phone talks to your relay over Caddy HTTPS. LAN mode is opt-in and password-gated.
## Known limits
Same OS user = same trust; providers see chats/screenshots you send them; gVisor is optional on your box; backup security is bounded by your password; a breached relay leaks hashes and ciphertext, not plaintext.
## Hardening
Disk encryption, capped + 2FA'd provider keys, HTTPS-only relay with 2FA, gVisor on the box host, per-app "ask always" on your sensitive apps.

View File

@ -504,6 +504,12 @@ impl State {
if let Ok(mut f) = std::fs::File::create(&tmp) {
use std::io::Write;
if f.write_all(s.as_bytes()).and_then(|_| f.sync_all()).is_ok() {
// state.json carries model keys: owner-only, like the vault's key file.
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let _ = std::fs::set_permissions(&tmp, std::fs::Permissions::from_mode(0o600));
}
return std::fs::rename(&tmp, &p).is_ok();
}
}