Appearance
调研:保险库格式、密钥结构、不用服务器的同步与共享(2026-11-16)
读过源码的项目:rbw、kbs2、oak-keyring、Rowel、Secretive、KeePassXC,以及 Bitwarden 桌面版的 Windows Hello 代码。读过的文档:gopass 文档,1Password 和 Bitwarden 2026 年版白皮书。
已有项目的做法
| 项目 | 许可证 | 要点 |
|---|---|---|
| rbw | MIT | 解密后的密钥只放在后台进程里:固定的 4 KiB 缓冲区,用 region 锁住内存。socket 放在 0700 目录里,但不检查对端身份。macOS 上用 PT_DENY_ATTACH 防调试并关闭 core dump。默认 3600 秒锁定 |
| kbs2 | MIT,2026-03 已归档 | age 密钥用口令包裹;后台进程用 getpeereid 检查调用方用户 |
| gopass | MIT | 每个密钥一个 gpg 或 age 文件,git 同步,多收件人。增删成员要重加密全部文件,但被移除的人仍保有 git 历史。收件人列表的哈希存在 git 之外,发生变化时要用户确认,防止被人偷偷加收件人 |
| KeePassXC | GPL,只能参考 | 单个 KDBX4 文件,Argon2,内存 64 MiB。Windows Hello:用 Hello 密钥签一个随机挑战,签名的 SHA-256 作为 AES-GCM 密钥。Touch ID:用 BiometryCurrentSet 保护钥匙串条目。Linux:Polkit 检查加上内存里的密钥 |
| Rowel | GPL,只能参考 | 每个 lockbox 一个 SQLCipher 库,每条记录再加一层加密。Argon2id 64 MiB / t3 / p4。同步按行取最后写入者,删除标记保留 90 天 |
| oak-keyring | MIT | SQLCipher,每条记录用 XChaCha20 加密并带版本号;冲突由用户选择保留哪边 |
| 1Password | 闭源 | 密码经 PBKDF2 650k 次,再和 Secret Key 合成解锁密钥,登录用 SRP。每个保险库一把密钥,用每个成员的 RSA 公钥分别包裹;条目用 AES-256-GCM |
| Bitwarden | GPL | 主密钥 → 用户对称密钥 → 每条目一把 64 字节密钥;组织密钥用 RSA 包裹 |
| Secretive | MIT | 安全飞地里的 P-256 密钥,配合生物识别。按 PID 认调用方,PID 被复用时有竞争问题 |
1. 保险库格式
| 单个文件(KDBX) | 每条目一个文件(gopass/age) | 行级加密的 SQLite | |
|---|---|---|---|
| 同步冲突 | 任何并发修改都冲突 | 只有改同一条目时才冲突 | 直接同步会损坏,必须导出包 |
| 元数据泄露 | 大小和修改时间 | 条目数、大小、修改时间 | 大小 |
推荐做法:
- 每台设备只往自己的加密操作日志里追加:
vault/<id>/ops/<device>/<seq>,并定期生成快照。 - 每个文件只有一个写入者,所以 iCloud、Syncthing、Dropbox 都不会产生冲突副本,git 也不会冲突。
- 文件名用随机 ID,内容按固定大小分档补齐,减少元数据泄露。
- 本地的 SQLite 只是搜索缓存,随时可以从日志重建。
2. 密钥结构
- 账户解锁密钥(AUK):Argon2id(主密码) 和 Secret Key 合在一起,经 HKDF 派生。参数用 m=64 MiB、t=3、p=4,写在文件头里。内存控制在 128 MiB 以内,否则以后 iOS 的 AutoFill 扩展可能跑不动。
- 每个成员:一把 X25519 加密密钥和一把 Ed25519 签名密钥,每台设备再有各自的密钥。所有私钥都用 AUK 包裹。
- 每个保险库:每个"纪元"(epoch)一把随机 256 位密钥,分别用每个成员的 X25519 公钥包裹(age 收件人或 HPKE)。条目用 XChaCha20-Poly1305 加密,关联数据里绑定 vault ID、item ID 和 epoch。
- 生物识别解锁(包裹的是 AUK):
- macOS:安全飞地里的 P-256 密钥,带
privateKeyUsage和biometryCurrentSet,用 ECIES 包裹。包裹时不用验证,解开时要 Touch ID。security-frameworkcrate 支持这些操作。 - Windows:Windows Hello 签挑战,再对签名做 HKDF。
- Linux:主密码解锁后,密钥留在内存里,取用时过一道 Polkit 检查;TPM2 可选。
- macOS:安全飞地里的 P-256 密钥,带
3. 不用服务器的共享
- 成员日志:只追加、哈希链接的签名操作序列,操作包括加人、删人、改角色、换钥。每条操作都必须由当时有效的管理员签名。并发的管理员操作按确定的规则排序。每台设备记住自己见过的最新日志头。
- 改动的作者认证:每个操作文件都用 Ed25519 签名,读的时候拒绝没有写权限的成员签名的文件。这一步必须有:共享保险库密钥做 AEAD 分辨不出是谁写的,age 本身也不认证发送者。
- 移除成员:开新的纪元、换新密钥,重加密现有条目,并提示其余成员轮换那个人看得到的密码。没有服务器,就没法撤回别人已经读过的内容。
- 防回滚:每台设备记住每个写入者的最高序号和对应哈希,日志被截断或倒退时能发现。但别人有意不同步给你的更新发现不了,没有服务器做不到。
- 公钥真实性:加人时当面核对指纹或扫二维码,类似聊天软件的安全码。1Password 和 Bitwarden 是靠服务器做这件事的。
- 参考:Ink & Switch 的 Keyhive(Apache-2.0,pre-alpha)。
4. 给 agent 用的后台进程
- macOS / Linux:socket 放在 0700 目录里,文件权限 0600,并检查
getpeereid返回的是本用户。 - macOS 调用方身份:用
LOCAL_PEERTOKEN拿 audit token,再用SecCodeCheckValidity校验 Team ID。不靠 PID。 - Linux 调用方身份:
SO_PEERCRED,内核支持的话再加SO_PEERPIDFD。 - Windows:命名管道,DACL 只允许当前用户的 SID,设
first_pipe_instance和reject_remote_clients。调用方身份用GetNamedPipeClientProcessId加 Authenticode 校验。 - 签名校验的局限:只能证明调用方是我们自己的 CLI,证明不了是哪个 agent 在调用。所以给 agent 的是 Rowel 式的限定授权:每次要弹窗批准;密钥直接注入到命令或请求里,agent 看不到值;全程记审计日志;保险库一锁,授权全部作废。
- 内存:用
zeroize/secrecy加锁定内存;防调试、关 core dump。Windows 上用 VirtualLock 加 DPAPI。 - 自动锁定:空闲超时、电脑睡眠或锁屏、会话达到最长时长,任一条件触发就锁。
5. 同步方式
| 方式 | 优点 | 缺点 |
|---|---|---|
| git | 有历史,合并干净 | 完整历史都泄露;被移除的成员保有旧数据 |
| iCloud Drive | Mac 和 iOS 原生支持,Windows 有客户端 | Linux 没有;文件可能被移出本地、按需下载 |
| Syncthing | 好用 | iOS 没有官方客户端 |
| Dropbox | 能用 | 会生成冲突副本,但我们每个文件只有一个写入者,正好避开 |
合并规则:按字段取最后写入者,时间戳用混合逻辑时钟(HLC),平局时比设备 ID。并发修改用版本向量检测,被覆盖的版本放进条目历史。不需要完整的 CRDT 库,备注字段以后可以考虑。
6. Rust crate(除注明外都是 MIT/Apache)
| 用途 | crate |
|---|---|
| 加密 | age 0.12、chacha20poly1305 0.11、argon2 0.6(2026-08 发了新大版本)、hkdf、hpke、ed25519-dalek、x25519-dalek |
| 内存 | zeroize、secrecy、region |
| macOS | security-framework 3.7、objc2-local-authentication |
| Windows | windows(KeyCredentialManager)、tokio 命名管道 |
| 存储和其他 | keyring、rusqlite、pinentry |
要你决定的事
- Secret Key(双密钥派生)要不要默认开? 推荐开,因为保险库文件会放在 Dropbox 或 git 远端。代价是加新设备时要用应急套件或扫二维码。
- Apple Developer ID:macOS 上用 Touch ID 解锁,必须是签名过的 App bundle,并带
keychain-access-groups权限。 - 成员被移除后,他能看到的旧密码算不算泄露? 推荐算,并提示其余成员轮换。