外观
ADR-005 本地加密存储
状态
提案,待总控决策。本文定义隔离与失败语义,不认定任何数据库或密钥封装组合已经审计或适合生产。
背景
客户端需要保存身份、联系人验证状态、协议会话、消息、草稿、Outbox、设置和恢复状态。数据既要支持事务、增量查询、迁移和崩溃恢复,也必须满足 Profile、真实空间与伪装空间的强隔离。
已验证的仓库事实
截至 2026-09-23,当前仓库没有 iOS 工程、数据库 schema 或依赖锁定文件;GRDB、SQLCipher、Keychain 和 Secure Enclave 组合尚未完成构建、许可、性能、迁移或真机验证。
说明书 MUST
- 不得使用未加密 SwiftData 保存消息。
- 本地数据库密钥存入 Keychain,并由 Secure Enclave 保护的密钥封装;设备锁定后数据库不可读取。
- 每个服务器 Profile 使用独立数据库、Keychain 命名空间、身份和通知路由。
- Decoy 使用独立容器,不挂载或复用真实数据。
- 主/假密码 KDF 输出只用于验证和解封随机数据主密钥,不直接作为数据库密钥。
- 迁移失败不得删除原始数据库。
- 格式化优先删除数据库密钥与身份/设备私钥封装材料,再处理数据库、WAL/SHM 和其他残留。
建议决策
优先验证“GRDB + SQLCipher + iOS Data Protection + Keychain 中的封装材料 + Secure Enclave 背书的解封密钥”方案。每个真实 Profile 和 Decoy 分别使用独立数据库文件、随机数据库密钥、Keychain service/account 命名空间和附件目录。
该建议的成立条件是:依赖许可兼容、SQLCipher 构建与更新路径可维护、Swift 6 并发边界可证明、锁定态和 WAL/SHM 行为符合要求、独立安全评审通过。任一条件不满足时回到方案比较,不以临时明文存储兜底。
选项比较
选项 A GRDB 加 SQLCipher
- 优点:SQLite 事务与迁移成熟;可明确控制连接、WAL、查询和每 Profile 文件;适合 Outbox 原子写入。
- 缺点:需要维护 SQLCipher 构建、密钥注入、迁移和许可证/SBOM;扩展与备份时容易误复制 WAL/SHM。
- 待验证:iOS 26、Swift 6、目标设备性能、锁定态访问、崩溃恢复、依赖供应链。
选项 B 其他经过审计的加密数据库
- 优点:可能降低集成或运维成本。
- 缺点:必须证明逐 Profile 文件隔离、事务、分页、全文搜索、迁移回滚、密钥轮换和取证残留能力。
- 待验证:产品成熟度、许可、导出/恢复格式、故障可观测性。
不采用 未加密数据库加字段级零散加密
原因:schema、索引、草稿、状态字段、临时表和日志容易泄露;无法自然满足数据库整体锁定与可靠迁移。字段级加密只可作为额外分层,不替代加密数据库。
存储布局
建议使用随机内部标识,不从 VMax 地址、实例域或联系人信息派生路径:
text
Application Support/
spaces/
real/<opaque-profile-id>/
store.sqlite
store.sqlite-wal
store.sqlite-shm
attachments/
cache/
migration/
decoy/<opaque-decoy-id>/
store.sqlite
...- Profile registry 只保存打开选择所需的最小不透明索引,不保存显示名、完整地址、服务器域或密钥。
- 不把多个 Profile 放进同一数据库后仅依赖行级过滤。
- 文件使用适当 Data Protection class;具体 class 必须通过后台通知、设备锁定和可用性测试后决定。
- 临时导出、迁移副本和恢复暂存使用同等或更严格保护,并有明确清理责任。
密钥层级建议
text
用户口令 --内存困难 KDF--> KEK/验证材料
│
└─ 解封随机 App Data Master Key
Secure Enclave 背书的设备绑定密钥
│
└─ 保护 Keychain 中的 Profile 封装材料
每 Profile 随机 Database Key ──> SQLCipher 数据库
每身份/设备私钥 ──> 独立封装对象这里的箭头描述职责,不冻结具体算法、KDF 参数或封装格式。Secure Enclave 通常不直接保存任意对称密钥,实际应使用经验证的平台封装路径;API 能力见 ADR-002。设备绑定封装不得写入可移植备份。
Keychain 隔离
- service/account 由版本化中性前缀、空间类别和随机 Profile ID 组成,不包含
real、fake_password、地址或域名等可暴露语义。 - 每个 Profile 的数据库密钥、身份私钥封装、设备私钥封装和 Push 路由凭据分项保存,以便精确轮换和擦除。
- 访问控制和 accessibility 需要真机验证;目标是设备锁定时无法解封真实数据库。
- 读取 API 返回短生命周期敏感容器,禁止
Data被日志、崩溃报告或通用 Codable 路径复制。 - 删除 Profile 后枚举并核对所有预期项;禁止用宽泛前缀误删其他 Profile。
数据库边界
建议单库内至少区分:领域记录、协议不透明状态、Outbox、迁移元数据和本地设置。敏感搜索索引属于 S2 内容,必须留在加密库内。
Outbox 消息记录、不可变 envelope 和幂等键应在同一事务提交。网络层只读取待提交 envelope,不获得数据库密钥或其他会话状态。Crypto actor 生成 envelope 与推进协议状态的原子性必须与最终协议实现共同设计,不能由数据库 adapter 猜测。
服务端已确定使用 PostgreSQL 和 Redis/队列,但它们位于客户端信任边界之外。Redis/服务端队列只负责服务端短期异步工作,不能作为 iOS Outbox 的唯一状态来源,也不能参与本地 Profile 隔离或恢复判定。iOS 不直连 PostgreSQL 或 Redis。
打开与关闭
- AppShell 保持锁定,不预开真实库。
- AppUnlock 只返回抽象空间结果。
- 真实解锁成功后,KeyProtection 解封目标 Profile 所需材料。
- DatabaseFactory 校验文件、schema 版本和迁移前置条件后打开单个 Profile。
- ProfileSession 启动网络、Outbox 和密码 actor。
- 切换、锁定或后台策略要求关闭时,先停止新操作和任务,再检查点、清理内存并关闭连接。
任何阶段失败均保持锁定或进入恢复态,不创建同路径空白数据库,不自动删除“损坏”文件。
迁移与密钥轮换
- 迁移先验证可用空间、版本跨度和备份/快照策略;使用事务或影子文件,失败保留原库。
- 不允许未知 schema 静默打开或降级写入。
- 密钥轮换采用新文件/受控重加密还是 SQLCipher 原地 rekey,需以崩溃恢复和取证测试决定。
- 更新中断后必须可判定“旧库完整”或“新库完整”,不能选中半迁移文件。
- 每个 Profile 独立迁移;一个 Profile 失败不得打开另一个 Profile 的数据作为替代。
删除与格式化边界
- 普通删除 Profile 与安全格式化使用不同命令和授权路径,但共享精确资源清单与幂等删除 primitive。
- 格式化提交点优先是删除可解封真实数据库和私钥的关键封装材料;随后清理文件残留。
- WAL/SHM、附件、草稿、缓存、临时文件、迁移副本、诊断日志和本地通知必须列入清单。
- Files、iCloud Drive、AirDrop 或其他 App 外部
.vmaxbak不在本地格式化范围。 - 原子性、中断恢复和用户可见语义由 ADR-011 决定;本 ADR 不自行冻结。
验证计划
- 真机锁定:打开库后锁屏、冷启动锁屏、重启未首次解锁、后台唤醒,验证不可读边界。
- 文件取证:正常使用、崩溃、迁移、rekey、删除后检查数据库、WAL/SHM、临时文件和备份副本中的明文与密钥。
- Profile 隔离:相同业务 ID、多 Profile 并发切换、错误路径注入、Keychain 枚举与删除范围检查。
- 故障注入:磁盘满、I/O 错误、进程强杀、schema 损坏、旧版本数据库和未知版本。
- 并发:Swift 6 数据竞争检查、actor 取消、连接泄漏与关闭超时。
- 性能:说明书的 10 万条单会话、100 万条总库、增量加载和本地搜索基线。
- 供应链:依赖哈希、SBOM、许可证、漏洞扫描、可重复构建和更新演练。
待总控决策
- 是否采用选项 A;推荐先做两周内可丢弃原型,再决定依赖冻结。
- SQLCipher/GRDB 的具体版本、集成方式和许可证接受;需许可与供应链评审。
- Keychain accessibility、Data Protection class 和 Secure Enclave 封装 API;需真机矩阵验证。
- 数据库密钥轮换采用影子库还是原地 rekey;需强杀与取证结果选择。
- 是否在首版启用本地全文搜索;若启用,索引容量与泄露面需单独测试。
- 双密码 KDF、槽位封装和格式化提交点由 ADR-011 决定,本文不冻结。