Merge p5/security: SECURITY.md + threat model, state.json 0600 (P5)
This commit is contained in:
commit
b6d38aca74
|
|
@ -87,7 +87,7 @@ make deps && make && make install # deps: system packages + Rust 1.95+ (one ti
|
||||||
|
|
||||||
## Security in one paragraph
|
## 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
|
## Documentation
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -157,6 +157,10 @@ make install # 安裝:Mac → ~/Applications/AnyBot.app;Linux → ~/.loca
|
||||||
| [docs/release.md](docs/release.md) | 打包和自動建置 |
|
| [docs/release.md](docs/release.md) | 打包和自動建置 |
|
||||||
| [FEATURES.md](FEATURES.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,請遵守各服務的使用條款與你自己的金鑰合約。
|
AnyBot 以 **Apache License 2.0** 授權(見 [LICENSE](LICENSE))散布:著作權屬本 repo 的貢獻者(The AnyBot contributors)。程式碼由本專案獨立撰寫;對第三方服務(xAI、OpenAI 等)的呼叫都走各家的公開 API,請遵守各服務的使用條款與你自己的金鑰合約。
|
||||||
|
|
|
||||||
|
|
@ -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 上。
|
||||||
|
|
@ -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.
|
||||||
|
|
@ -504,6 +504,12 @@ impl State {
|
||||||
if let Ok(mut f) = std::fs::File::create(&tmp) {
|
if let Ok(mut f) = std::fs::File::create(&tmp) {
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
if f.write_all(s.as_bytes()).and_then(|_| f.sync_all()).is_ok() {
|
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();
|
return std::fs::rename(&tmp, &p).is_ok();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue