外观
VMax iOS 客户端模块边界
1. 文档状态与范围
- 状态:架构提案;Swift + SwiftUI 技术方向已确认,其余标注待决的设计未冻结。
- 适用范围:首版 iOS 客户端。客户端语言与 UI 框架确定为 Swift + SwiftUI;iOS 26+、Swift 6 为当前参考路线,最低版本及平台密码能力仍按 ADR-002 验证和决策。
- 负责内容:客户端模块、运行状态、数据与信任边界、Profile 隔离、离线 Outbox、通知、错误恢复,以及密码引擎的接口隔离。
- 不负责内容:生产密码协议实现、密码套件冻结、服务端内部实现、开源许可结论、发布安全声明。
- 关联决策:ADR-002、ADR-005、ADR-007;双密码与格式化的安全语义由 ADR-011 最终承接。ADR 未决时,本文只定义不得突破的边界。
2. 依据与证据等级
2.1 说明书 MUST
以下要求直接来自 v1.1 说明书,应作为实现验收约束,而不是本文新作出的技术选择:
- 每个服务器 Profile 使用独立数据库、Keychain 命名空间、身份和通知路由。
- 切换 Profile 前关闭旧 WebSocket、停止旧后台工作、清理可清理的会话密钥,并重建依赖容器。
- 真实空间与 Decoy Profile 使用不同数据容器;Decoy 不得挂载或复用真实身份、联系人、会话、服务器配置、密码会话或 Push 路由。
- UI 不直接读取密码验证值;解锁边界只返回
real、decoy或invalid之类的抽象结果。 - 断网消息进入本地 Outbox;恢复后按顺序、幂等重试,不产生重复消息。
- App 进入后台时遮蔽多任务快照;通知与日志不得泄露消息正文、密钥、完整地址、APNs token、口令或可逆口令材料。
- 假密码触发格式化默认关闭;仅精确验证成功才可触发。普通输错、空输入、Face ID 失败、取消、退后台或系统终止都不得触发。
- 格式化优先销毁关键密钥,再清理数据库、WAL/SHM、附件、草稿、缓存、临时文件、诊断日志和本地通知;不得宣称物理覆写 NAND,也不删除 App 外部备份。
- 未知协议版本和不安全降级必须拒绝;生产密码协议须基于公开规范、测试向量和独立审计。
2.2 已验证的仓库事实
截至 2026-09-23,本 worktree 中 ios/ 只有空白 README,尚无 Swift 源码、工程文件、持久化模型或可运行测试;本文所述模块均未实现,也未经过真机、API、密码学或迁移验证。四份本任务文档在修改前均为空白占位。
2.3 建议方案
本文建议使用“进程级壳层 + 单一活动空间容器 + Profile 作用域依赖”结构。建议内容需要通过 ADR、原型、依赖许可审查和测试后才能冻结。
2.4 已确认的跨线路输入
- iOS 客户端确定使用 Swift + SwiftUI,不再比较其他客户端语言或 UI 框架。
- 服务端确定使用 Go,以 Docker 方式交付,持久化使用 PostgreSQL,Redis 与队列承担服务端异步/短期工作,S3 兼容对象存储承担经批准的密文对象存储。
- 产品不建设 Web 管理后台。客户端不得依赖隐藏的后台页面或人工后台写库来完成身份、消息、恢复或 Profile 操作。
- 上述服务端栈是客户端的外部约束,不是客户端依赖:iOS 只通过版本化 OpenAPI、WebSocket 和经定义的密文对象接口通信,不直连 PostgreSQL、Redis、队列或 S3。
- Redis/服务端队列不能替代本地 Outbox;S3 对象存储不能接触消息明文、附件明文、私钥或备份密码。
3. 总体依赖方向

放大查看矢量图。上半部表示编译期抽象依赖,下半部表示运行时所有权;虚线表示适配或外部通信,不是 UI 直连数据库。该结构是架构提案,尚未实现。
text
SwiftUI Features
│ 只依赖用例、只读视图状态和命令
▼
Application / Use Cases
│ 编排事务、生命周期与恢复,不实现密码原语
▼
Domain
│ 纯领域类型、状态机、端口协议
▼
Infrastructure Adapters
├─ Persistence
├─ Networking
├─ Crypto adapter
├─ Key protection
├─ Push / notifications
└─ Privacy logging依赖只能向下指向稳定抽象。Infrastructure 可实现 Domain 端口,但 Domain 不导入 GRDB、SQLCipher、URLSession、UserNotifications、Security 或具体密码库。SwiftUI View 不持有数据库连接、协议会话或 Keychain 查询能力。
4. 运行容器与状态所有权
4.1 进程级 AppShell
AppShell 在解锁前即可存在,只持有无敏感状态的系统适配器:生命周期观察、受隐私约束的日志、受保护快照、解锁入口、通知点击的封存句柄和容器工厂。它不得预先打开真实数据库、建立真实服务器连接或恢复真实密码会话。
建议的顶层状态:
text
bootstrapping
locked
unlocking
openingReal(profileID)
realActive(profileID)
openingDecoy
decoyActive
switchingProfile(from, to)
erasing
recoveryRequired(reason)状态迁移由单一 AppStateCoordinator 串行化。UI 只能发送意图并观察去敏后的状态,不能绕过协调器直接创建 Profile 容器。
4.2 SpaceContainer
同一时刻最多存在一个活动 SpaceContainer:真实空间或伪装空间。两者必须由不同工厂路径创建,避免通过布尔开关把真实容器“伪装”成空白界面。
RealSpaceContainer:可创建一个活动ProfileSession,但必须在成功主密码解锁后才能构造。DecoySpaceContainer:只使用 Decoy 自身的存储、路由和自然空白模型;构造函数不接收真实 Profile registry、真实数据库 URL、真实通知路由或真实密码引擎。LockedSpace:不持有数据库句柄、明文领域模型或活动网络任务。
4.3 ProfileSession
ProfileSession 是单个服务器 Profile 的唯一资源所有者,建议包含:
ProfileIdentityStoreProfileDatabaseCryptoSessionActorOutboxActorInboxProcessorRealtimeConnectionProfileNotificationRouterProfileTaskRegistryProfilePrivacyLogger
所有长生命周期任务登记到 ProfileTaskRegistry。关闭顺序建议为:阻止新命令 → 取消网络与后台任务 → 等待有界收敛 → 持久化安全检查点 → 清理内存敏感材料 → 关闭数据库 → 销毁依赖容器。任何步骤失败都不得回退到继续使用旧 Profile;进入锁定或恢复态。
5. 模块职责
| 模块 | 拥有的职责 | 明确禁止 |
|---|---|---|
| App | 启动、路由、生命周期、依赖组装 | 读取口令、直接查询业务库 |
| AppUnlock | 本地凭据输入与抽象路由结果、限速 | 服务端认证、记录槽位命中、决定业务界面 |
| DecoyMode | 独立空白空间与自然空状态 | 引用真实 Profile、显示伪装或擦除语义 |
| ProfileRegistry | 保存可用 Profile 的最小索引和选择状态 | 保存私钥、消息、联系人正文 |
| Identity | 身份与设备领域状态、联系人验证事件 | 自行实现签名算法 |
| Messaging | 消息命令、状态机、Outbox/Inbox 协调 | 直接调用具体密码库或 UI |
| Crypto | ProtocolEngine 端口、会话 actor、版本拒绝策略 | 在本文冻结密码套件、提供未经审计生产实现 |
| Persistence | Profile 作用域事务、迁移、查询与文件布局 | 跨 Profile 查询、明文敏感库 |
| Networking | OpenAPI 客户端、WebSocket、重试分类 | 解密内容、替代 Outbox 的持久化职责 |
| Push | APNs 注册、无内容唤醒、Profile 路由 | 在 payload 或通知扩展中放消息摘要/密钥 |
| BackupRestore | 加密导出、暂存校验、显式恢复提交 | 原地覆盖活动库、复用设备绑定密钥封装 |
| SecureErase | 执行经 ADR-011 定义的幂等擦除计划 | 因普通错误或网络结果触发、承诺物理覆写 |
| Logging | 类型化隐私日志与导出前脱敏 | 接收任意可字符串化敏感对象 |
服务端的 Go 进程、PostgreSQL、Redis/队列和 S3 适配器全部位于客户端信任边界之外。服务端内部拓扑变化不得要求 SwiftUI Feature 变更;兼容性只能通过共享接口版本、能力声明和迁移策略表达。
6. 数据与信任边界
6.1 数据分类
| 级别 | 示例 | 允许位置 |
|---|---|---|
| S0 公开 | 协议版本、公开能力标识 | 内存、配置、经审查日志 |
| S1 可关联元数据 | Profile ID、实例域、截断消息 ID | Profile 加密库;日志仅短期不可逆别名 |
| S2 内容 | 消息、草稿、联系人备注、搜索索引 | 已解锁内存与对应 Profile 加密库 |
| S3 密钥材料 | 身份/设备私钥、会话状态、数据库密钥 | Keychain/受保护封装、密码 actor 内存、加密库中的允许部分 |
| S4 凭据 | 主密码、假密码、备份密码 | 受控输入缓冲和 KDF 调用期间;不得持久化或跨模块传播 |
敏感领域类型不提供通用字符串描述或自动编码。日志 API 应以允许列表字段为输入,对 S2-S4 类型在编译或测试层拒绝。
6.2 文件与 Keychain 命名空间
建议每个真实 Profile 使用不可从显示地址推导的随机内部 ID,并各自拥有数据库、WAL/SHM、附件、缓存、临时目录和迁移状态目录。Keychain 的 service/account/access-group 组合应同时隔离“空间种类”和 Profile ID;Decoy 使用另一套固定前缀与独立密钥。
禁止把多个 Profile 放入同一数据库后仅依赖 profile_id 过滤。该做法会把过滤遗漏升级为跨 Profile 泄露,也扩大格式化和恢复的删除半径。
6.3 密码引擎接口
客户端只依赖版本化端口,例如:
swift
protocol ProtocolEngine: Sendable {
func createSession(_ request: SessionRequest) async throws -> SessionHandle
func encrypt(_ plaintext: SensitiveBytes, in session: SessionHandle) async throws -> Envelope
func decrypt(_ envelope: Envelope, in session: SessionHandle) async throws -> VerifiedMessage
func checkpoint(_ session: SessionHandle) async throws -> OpaqueSessionState
func destroy(_ session: SessionHandle) async
}这只是隔离形状,不是生产协议认可。Envelope、OpaqueSessionState 和测试向量格式由协议/序列化 ADR 决定。生产 adapter 必须拒绝未知版本与未获批准套件;开发用 Fake adapter 必须在构建和运行时有醒目标识,不能进入发布构建。
7. 离线 Outbox
7.1 持久化模型
Outbox 与消息领域记录必须在同一 Profile 数据库事务中创建。一次用户发送建议原子写入:稳定 message_id、幂等键、收件设备引用、待加密/已加密阶段所需的最小数据、尝试次数、下一次重试时间和可恢复错误码。
不得跨 Profile 复用幂等键、队列或 worker。Profile 关闭后,对应 Outbox worker 必须停止。
7.2 状态机
text
draft
-> preparing
-> ready(envelope persisted)
-> submitting
-> accepted(server timestamp persisted)
-> delivered
-> read optional encrypted receipt
preparing/submitting -> retryScheduled
preparing/submitting -> blockedByIdentityChange
preparing/submitting -> terminalFailure- 网络超时或连接重置属于结果未知:保持原
message_id与幂等键重试,不生成新消息。 - 4xx 认证、身份密钥变化、协议不兼容、内容超限等不得无限重试;映射为可解释的阻断或终止状态。
- 重试顺序以会话内稳定序列为主;不同会话可受限并发。具体是否允许后续消息越过失败消息属于待总控决策。
- 只有数据库事务确认服务器接受后,UI 才显示“已发送”。推送成功不是送达证据;接收端成功解密并 ACK 才是“已送达”。
7.3 崩溃与恢复
启动时把遗留 submitting 视为结果未知,重新使用同一幂等键查询或提交。加密阶段是否可安全重放取决于最终协议引擎:不得在没有引擎保证时重复推进 ratchet。推荐边界是由 Crypto actor 在一次事务性 API 中生成并持久化不可变 envelope,再由网络层重复提交同一字节。
8. 通知边界
- APNs payload 和 Push Gateway 事件仅承担无内容唤醒与随机路由引用,不含消息正文、联系人名、服务器显示名或可读预览。
- Notification Service Extension 不打开真实消息数据库、不持有身份/会话密钥、不执行生产消息解密。说明书中的“仅处理已加密小载荷”不等于授权扩展展示解密预览。
- 主 App 在成功解锁并拉取、验证、解密后,是否生成本地通知预览由用户设置与当前空间决定;默认采用最小暴露。
- 进入 Decoy 后清除 VMax 可见通知和 badge。后续唤醒不得把真实摘要投射到 Decoy UI。
- APNs token 只交给所属 Profile 的路由 adapter;token 轮换时逐 Profile 注册。日志和诊断导出永不包含 token 全值。
- 通知点击在锁定态只封存不透明意图;解锁并确认目标 Profile 后才解析业务路由。禁止通知直接激活或泄露另一个 Profile。
9. 错误恢复策略
| 故障 | 安全失败状态 | 恢复动作 |
|---|---|---|
| Profile 数据库打开失败 | 保持锁定,不创建空白真实库覆盖原库 | 保留原文件,导出脱敏诊断,走迁移/恢复流程 |
| 迁移失败或磁盘满 | 回滚事务或保留旧版本,只读阻断 | 释放空间后重试;不得删除原库 |
| 实例指纹变化 | 阻断网络与 Outbox | 展示安全事件,要求重新核验 |
| 未知协议/套件 | 拒绝解密或发送 | 升级能力或新建受支持会话,不降级 |
| WebSocket 中断 | 保持本地状态 | 回退到有界重连和拉取;不据此判定消息失败 |
| APNs 不可用 | 不影响消息正确性 | 前台/后台允许时拉取,显示通知能力降级 |
| 恢复包被篡改或密码错误 | 不改动现有 Profile | 删除暂存明文/密钥,允许受限重试 |
| Profile 切换中断 | 下次启动保持锁定 | 检查关闭/打开日志,只恢复一个容器 |
| 格式化中断 | 进入专用恢复态,不尝试打开真实库 | 按 ADR-011 的幂等擦除记录完成或证明未开始 |
错误对象只暴露稳定错误码和用户可执行动作;底层错误中的路径、SQL、URL、token、密钥标识和消息内容不得进入 UI 或导出日志。
10. 验证矩阵
- Profile 隔离:为两个真实 Profile 和一个 Decoy 注入相同业务 ID,证明数据库、Keychain、搜索、通知、分享和诊断均无交叉结果。
- 生命周期:在打开、切换、加密、提交、迁移和擦除的每一步强制终止,验证重启后最多激活一个容器。
- Outbox:断网、超时、服务器已接收但响应丢失、乱序 ACK、重复 Push、并发点击发送;断言服务端与本地均只产生一个逻辑消息。
- 通知:锁定、真实空间、Decoy、Profile 已删除、token 轮换、APNs 不可用;抓取 payload、扩展存储和系统通知内容。
- 数据保护:设备锁定时尝试打开数据库与读取 Keychain;真机验证 Data Protection 和 Secure Enclave 行为。
- 隐私:扫描日志、崩溃包、分析事件、App Switcher、剪贴板、分享扩展和诊断导出。
- 性能与无障碍:在说明书基线与最大辅助字号、VoiceOver、Reduce Motion、高对比度下真机验证。
11. 待总控决策
D-01 iOS 最低版本与后量子能力
- 选项 A:首版仅 iOS 26+,使用可用的平台能力;优点是能力面单一,缺点是设备覆盖较小。
- 选项 B:支持更低系统并提供兼容 adapter;优点是覆盖更广,缺点是容易形成密码能力分叉且不得静默降级。
- 推荐:A,理由是与说明书参考路线一致,并减少安全关键分支。
- 验证:见 ADR-002;必须在目标 Xcode/SDK 和最低档真机确认 API、可用性标注、性能与失败语义。
D-02 本地加密数据库与密钥封装
- 选项 A:GRDB + SQLCipher + Keychain/Secure Enclave 封装。
- 选项 B:其他经过独立审查、支持每 Profile 独立文件与事务迁移的加密存储。
- 推荐:先验证 A;它更贴合说明书,但许可、构建、迁移、Data Protection 和真机锁定行为均未验证。
- 验证:见 ADR-005,使用文件取证、锁定态访问、WAL/SHM、迁移中断和性能测试。
D-03 Outbox 的会话内阻塞策略
- 选项 A:严格顺序,前一消息阻断后续;语义清晰,但单个坏消息会卡住会话。
- 选项 B:允许明确分类的终止失败被后续消息越过;可用性更好,但协议会话必须证明可安全推进。
- 推荐:在协议引擎提供可证明的 envelope 持久化语义后采用 B,否则使用 A。
- 验证:跨语言协议向量、乱序/重放测试及服务器幂等集成测试。
D-04 通知预览
- 选项 A:永远只显示“有新消息”。
- 选项 B:解锁后的主 App 可按用户选择生成本地内容预览,扩展仍不解密。
- 推荐:首版 A,隐私边界最清晰;B 可后续单独评审。
- 验证:锁定/Decoy/后台/系统重启矩阵和系统通知取证。
D-05 双密码和格式化原子性
- 选项与最终语义由 ADR-011 决定。
- 本文推荐采用可恢复、幂等的擦除计划,并以关键密钥销毁为提交点;不得在 ADR-011 完成前开放格式化设置入口。
- 验证:每一步强杀、Keychain 与文件残留检查、100 次错误输入零误触、独立安全评审。