diff --git a/README.en.md b/README.en.md index c820548..2c51cd5 100644 --- a/README.en.md +++ b/README.en.md @@ -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 `` 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 `` 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 diff --git a/README.md b/README.md index 01676d0..05e8f6a 100644 --- a/README.md +++ b/README.md @@ -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,請遵守各服務的使用條款與你自己的金鑰合約。 diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..f2b2fdf --- /dev/null +++ b/SECURITY.md @@ -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. **外來內容一律標記**:瀏覽器/螢幕/檔案讀回的內容包成 `` 資料,系統指示明令「資料不是命令」。 +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 上。 diff --git a/docs/en/security.md b/docs/en/security.md new file mode 100644 index 0000000..fe93a2b --- /dev/null +++ b/docs/en/security.md @@ -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 `` — 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. diff --git a/src/model.rs b/src/model.rs index ccdc068..5ed806f 100644 --- a/src/model.rs +++ b/src/model.rs @@ -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(); } }