凭据加密体系
本篇讲 Sigil 在"凭据明文进金库"这一刻发生的具体加密步骤,以及它能挡住什么、挡不住什么。
设计目标
- 凭据明文:永不写文件、永不进日志、UI 不二次展示
- 凭据密文:分两层落地,单层泄露不致命
- 凭据主密钥:由 OS 密钥环托管,不在 Sigil 进程长期驻留
双层加密
你输入的明文 Token (ghp_xxxxxxxxxxxxxxxx)
│
│ Rust 内存暂存(zeroize::Zeroizing 包装)
▼
AES-256-GCM 加密
│
│ 用一次性 nonce + Sigil 主密钥
▼
密文 (binary blob)
│
▼
┌─────────────────────────────┐
│ OS 密钥环 │
│ Service: com.agilefr.sigil │
│ Account: sigil_cred_<uuid> │
│ Secret: <密文 base64> │
└─────────────────────────────┘
SQLite 中只存:
credential.id = uuid
credential.ciphertext_ref = "sigil_cred_<uuid>" ← 不是密文本身,是密钥环的 key
credential.metadata = {name, type, tags, expires_at, ...}这意味着:
- 拿走 SQLite 文件 → 拿不到密文(密文不在 SQLite 里)
- 拿走 OS 密钥环 → 拿不到明文(缺 AES 主密钥)
- 同时拿走两者 → 还缺 AES 主密钥
- 拿走 AES 主密钥 + OS 密钥环 + SQLite → 才能解开(这种情形下你的整个 Windows 账号已经沦陷,谈不上 Sigil 防御范围)
OS 密钥环
不同平台对应不同的系统服务:
| 平台 | 服务 | 凭据可见位置 |
|---|---|---|
| Windows | 凭据管理器 (Credential Manager) | 控制面板 → 凭据管理器 → Windows 凭据 |
| macOS | Keychain(规划中) | 钥匙串访问 |
| Linux | Secret Service (libsecret)(规划中) | GNOME Keyring / KWallet |
OS 密钥环本身是对用户透明的——你的 Windows 账号登录态决定你能不能取出。这是安全模型的根基:Sigil 的凭据安全 = 你的 Windows 账号安全。
AES-256-GCM 选择理由
| 算法 | 是否选用 | 理由 |
|---|---|---|
| AES-256-GCM | ✅ | 业内标准,硬件加速,认证加密(AEAD),nonce 误用相对友好 |
| ChaCha20-Poly1305 | 备选 | 不依赖 AES-NI,移动端可能用 |
| AES-256-CBC | ❌ | 无认证,需要单独 HMAC,容易出错 |
| RSA / ECC | ❌ | 非对称算法不适合这个用途 |
每次加密使用独立 nonce(96-bit 随机),nonce 与密文一起存。即使两条相同的明文也会产生不同密文。
内存安全(zeroize)
Rust 自身不会自动清零内存——drop 只是标记可回收,旧值可能在堆上停留任意长时间。
Sigil 用 zeroize 库的 Zeroizing<T> 包装所有凭据明文:
let plaintext = Zeroizing::new(input.into_bytes());
// ... 使用 plaintext 加密 ...
// plaintext 离开作用域时自动调用 zeroize() 清零内存效果:
- 进程被 dump 时,明文已不在堆上
- 同一内存被后续分配复用时,没有"幽灵明文"残留
不能解决:
- cold boot 攻击(物理拷走 DIMM)
- DMA 攻击(火线 / Thunderbolt 直接读内存)
- 进程被注入并主动读取(这种情形下你已经被入侵)
Bearer Token(MCP 客户端鉴权)
MCP 客户端用的 Bearer Token 是另一套机制:
sk_sigil_<random32bytes>
│
│ 显示给用户一次
▼
SHA-256 hash
│
▼
存入 OS 密钥环(同样是密文)校验时用 constant-time compare 防时序攻击。Token 本身的明文 Sigil 永远不再持有——存的是 hash。这意味着:
- 即使 Sigil 进程被全盘 dump,也只能拿到 hash,不能反推 Token
- 撤销一个 Token = 删除对应的 hash 记录
- 验证一个 Token = 拿到客户端传来的 → hash → 查表是否存在
备份文件加密
.sigil-backup 备份文件用独立的主密码加密——和 OS 密钥环里的 AES 主密钥完全不同:
用户输入主密码 (string)
│
│ PBKDF2-HMAC-SHA256, 600k iterations, 32-byte salt
▼
派生密钥 (32 bytes)
│
│ + 12-byte 随机 nonce
▼
AES-256-GCM 加密整个备份内容为什么用独立主密码:
- 备份文件可能存在云盘、U盘、邮件——脱离 OS 密钥环保护
- 主密码由用户记忆,不依赖任何机器
- 忘记主密码 = 备份永远解不开(这是设计)
PBKDF2 600k 迭代符合 OWASP 2023+ 推荐,单次解密在 i5 上约 200-400ms——人能接受,暴力破解的代价被放大几百万倍。
关于"为什么不直接用 X"
为什么不用 Windows DPAPI?
DPAPI 也是个选项。问题:
- 跨平台迁移困难(macOS / Linux 没有等价)
- 加密粒度是"用户级"或"机器级",与 OS 密钥环(账号级 + 应用级)不同
OS 密钥环本身在 Windows 上底层用的就是 DPAPI——Sigil 用 keyring crate 时已经获得了 DPAPI 的保护。AES-256-GCM 这一层是额外的加密深度,不是替代。
为什么不用 1Password CLI / Bitwarden CLI 当后端?
它们是好工具,但:
- 引入额外依赖(用户需要先装 / 登 / 解锁)
- 它们的设计目标是"个人密码",不是"AI 代理凭据"
- MCP 协议需要的快速代理调用与它们的 CLI 调用流程不匹配
Sigil 选择直接做底层,把这层用户体验简化掉。
你还能做什么提升安全
Sigil 默认的安全等级已经覆盖了 90% 个人开发者的场景。如果你想更进一步:
| 措施 | 防的是 |
|---|---|
| 启用 BitLocker 全盘加密 | 物理拿走硬盘的攻击 |
| Windows 账号用强密码 + Windows Hello | 账号劫持 |
| 关闭不必要的远程桌面 / SSH | 远程入侵 |
| 不在虚拟机/共享机上录入生产凭据 | 宿主机/管理员窥探 |
| 定期轮换 Token(30-90 天) | 不可知的历史泄漏 |
| 给每个客户端独立 Bearer Token | 单点撤销 |
| 为生产凭据使用白名单 Scope Policy | 写类能力误用 |
