Skip to content

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 不直接读取密码验证值;解锁边界只返回 realdecoyinvalid 之类的抽象结果。
  • 断网消息进入本地 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. 总体依赖方向

iOS 静态依赖方向、运行时单一空间容器、Profile 资源所有权与外部服务边界

放大查看矢量图。上半部表示编译期抽象依赖,下半部表示运行时所有权;虚线表示适配或外部通信,不是 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 的唯一资源所有者,建议包含:

  • ProfileIdentityStore
  • ProfileDatabase
  • CryptoSessionActor
  • OutboxActor
  • InboxProcessor
  • RealtimeConnection
  • ProfileNotificationRouter
  • ProfileTaskRegistry
  • ProfilePrivacyLogger

所有长生命周期任务登记到 ProfileTaskRegistry。关闭顺序建议为:阻止新命令 → 取消网络与后台任务 → 等待有界收敛 → 持久化安全检查点 → 清理内存敏感材料 → 关闭数据库 → 销毁依赖容器。任何步骤失败都不得回退到继续使用旧 Profile;进入锁定或恢复态。

5. 模块职责

模块拥有的职责明确禁止
App启动、路由、生命周期、依赖组装读取口令、直接查询业务库
AppUnlock本地凭据输入与抽象路由结果、限速服务端认证、记录槽位命中、决定业务界面
DecoyMode独立空白空间与自然空状态引用真实 Profile、显示伪装或擦除语义
ProfileRegistry保存可用 Profile 的最小索引和选择状态保存私钥、消息、联系人正文
Identity身份与设备领域状态、联系人验证事件自行实现签名算法
Messaging消息命令、状态机、Outbox/Inbox 协调直接调用具体密码库或 UI
CryptoProtocolEngine 端口、会话 actor、版本拒绝策略在本文冻结密码套件、提供未经审计生产实现
PersistenceProfile 作用域事务、迁移、查询与文件布局跨 Profile 查询、明文敏感库
NetworkingOpenAPI 客户端、WebSocket、重试分类解密内容、替代 Outbox 的持久化职责
PushAPNs 注册、无内容唤醒、Profile 路由在 payload 或通知扩展中放消息摘要/密钥
BackupRestore加密导出、暂存校验、显式恢复提交原地覆盖活动库、复用设备绑定密钥封装
SecureErase执行经 ADR-011 定义的幂等擦除计划因普通错误或网络结果触发、承诺物理覆写
Logging类型化隐私日志与导出前脱敏接收任意可字符串化敏感对象

服务端的 Go 进程、PostgreSQL、Redis/队列和 S3 适配器全部位于客户端信任边界之外。服务端内部拓扑变化不得要求 SwiftUI Feature 变更;兼容性只能通过共享接口版本、能力声明和迁移策略表达。

6. 数据与信任边界

6.1 数据分类

级别示例允许位置
S0 公开协议版本、公开能力标识内存、配置、经审查日志
S1 可关联元数据Profile ID、实例域、截断消息 IDProfile 加密库;日志仅短期不可逆别名
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
}

这只是隔离形状,不是生产协议认可。EnvelopeOpaqueSessionState 和测试向量格式由协议/序列化 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 次错误输入零误触、独立安全评审。

系统初步设计 · 待决事项不代表批准 · 安全方案尚未完成独立审计