# Face ID 启动锁 — 设计文档 **日期**:2026-05-30(W2) **作者**:link2026 + Claude **关联卖点**:#4 隐私三件套(系统级加密 + Face ID + 永久删除) **优先级**:P1(CLAUDE.md §6 / §8 / §11,原排期 W5 末,提前实现) --- ## 1. 一句话定位 可选的 Face ID/Touch ID 启动锁(默认关)。开启后,冷启动与「后台超过 1 分钟再回前台」都需要系统认证才能进入 App;失败可用设备密码兜底。完全基于系统 `LocalAuthentication`,不自造任何密码学(对齐红线 §10.2)。 --- ## 2. 设计决策(已与用户确认) | 决策点 | 选择 | |---|---| | 锁屏时机 | 冷启动 + 后台超过宽限才重锁 | | 后台宽限 | 60 秒 | | 认证策略 | `.deviceOwnerAuthentication`(Face ID/Touch ID 优先,自动跳设备密码兜底,避免锁死) | | 默认状态 | 关(§6) | | 开关位置 | 「我的」Tab 现有的 Face ID 卡,改为可交互 Toggle | | 任务切换器隐私遮罩 | 加,**仅锁开启时生效**(进 `.inactive`/`.background` 盖品牌遮罩,防多任务快照泄露;默认关用户无感) | **关于 §6「截屏黑屏防护…不做」**:那条针对的是**截图防护**(iOS 无官方 API);本设计的任务切换器遮罩是 `.inactive` 盖视图,是官方支持的标准做法,性质不同。 --- ## 3. 架构 ``` KangkangApp └─ WindowGroup { AppLockContainer { RootView() } } ← 仅包一层,RootView 零改动(§10.7) │ ┌─────────────┴──────────────────────────────┐ │ AppLockContainer │ │ @Environment(\.scenePhase) │ │ 渲染 content │ │ .overlay { if isLocked → LockScreen}│ │ .overlay { else if showsCover → PrivacyCover}│ │ onAppear → handleAppear(); │ │ onChange(scenePhase) → handleScenePhase() │ └─────────────────────────────────────────────┘ │ 读写 ┌─────────────┴──────────────────────────────┐ │ AppLock.shared (@MainActor @Observable) │ ← Security/AppLock.swift │ enabled ←→ UserDefaults("faceIDLockEnabled")│ │ isLocked / showsPrivacyCover │ │ biometryAvailable / biometryLabel │ │ gracePeriod = 60s,lastBackgroundedAt │ │ authenticate() / enableWithAuth() / disable()│ └──────────────────────────────────────────────┘ ``` 单例写法与项目既有 `ModelDownloadService.shared` 一致(`@MainActor @Observable final class` + `static let shared`)。 --- ## 4. 触发逻辑(状态机) | scenePhase / 事件 | 行为 | |---|---| | 容器 `onAppear`(冷启动) | `enabled` 为真且尚未冷启动锁过 → `isLocked = true` + 触发认证 | | `.background` | `lastBackgroundedAt = now`;`showsPrivacyCover = enabled` | | `.inactive`(任务切换器) | `showsPrivacyCover = enabled && !isLocked` | | `.active` | 隐藏遮罩;若 `enabled && !isLocked && 离开 > 60s` → `isLocked = true`;若 `isLocked` → 触发认证;清空 `lastBackgroundedAt` | | 认证成功 | `isLocked = false` | | 认证失败/取消 | 保持锁定,锁屏提供「解锁」按钮重试(`isAuthenticating` 防重入,不重复弹窗) | 冷启动时 scenePhase 初值为 `.active` 不触发 `onChange`,由 `handleAppear()` 负责冷启动锁;两路触发由 `isAuthenticating` 守卫去重。 --- ## 5. 能力探测与兜底 - `refreshAvailability()`:`LAContext.canEvaluatePolicy(.deviceOwnerAuthentication)` → `biometryAvailable`;读 `biometryType` 决定文案(Face ID / Touch ID / 密码)。 - 设备未设密码/无生物识别 → `biometryAvailable = false`,「我的」开关置灰,副标题「本设备未设置 Face ID 或密码」。 - 认证全程系统弹窗;失败/取消不抛错给 UI,只是停留锁屏。 --- ## 6. 文件清单 | 文件 | 改动 | |---|---| | `康康/Security/AppLock.swift` | **新增**:单例 + LAContext 封装 + 触发逻辑 | | `康康/Security/AppLockContainer.swift` | **新增**:包裹层 + scenePhase 驱动 + 两个 overlay | | `康康/Security/LockScreenView.swift` | **新增**:`LockScreenView` + `PrivacyCoverView` | | `康康/App/KangkangApp.swift` | `RootView()` → `AppLockContainer { RootView() }` | | `康康/Features/Me/MeView.swift` | 静态 Face ID 卡 → 可交互 Toggle 卡 | | `康康.xcodeproj/project.pbxproj` | 加 `INFOPLIST_KEY_NSFaceIDUsageDescription`(Debug + Release) | 工程用文件系统同步组,新增 `Security/` 下的源文件自动纳入编译,无需手改 pbxproj 注册。 --- ## 7. UI 锁屏(`LockScreenView`,全遮罩,走 Tj tokens): ``` 🔒 (lock glyph) 康康 已锁定 你的健康档案已加密保护 [ Face ID 解锁 ] ← onAppear 自动触发一次认证;按钮文案随设备能力变 ``` 隐私遮罩(`PrivacyCoverView`):品牌色底 + app 名,无交互,仅用于遮挡多任务快照。 「我的」Face ID 卡:Toggle 开启时先认证一次(成功才置 `enabled`),关闭直接关。副标题动态:「已开启 · Face ID」/「关闭」/「本设备未设置 Face ID 或密码」。 --- ## 8. 红线对齐(CLAUDE.md §10) - 不自造密码学,只用系统 `LocalAuthentication` ✅ - 默认关,可选开关 ✅ - 不引云 ✅ - 不重构 Tab/RecordSheet 骨架,只加一层包裹 ✅ - 清单内功能(§6/§8/§11 明列 Face ID 启动锁)✅ --- ## 9. 测试与验收 - 单元测试价值低(核心是系统弹窗 + scenePhase),不强求;`AppLock` 的宽限判定逻辑可抽纯函数测(可选)。 - **真机验收**:① 开关开启走 Face ID;② 杀进程冷启动需认证;③ 后台 <60s 回来不锁、>60s 回来锁;④ 多任务切换器快照被遮罩;⑤ 关 Face ID 录入(模拟失败)能跳设备密码;⑥ 默认关时全程无感。 - 模拟器:Features → Face ID → Enrolled / Matching Face 可模拟。