· compliance· 约 28 分钟精读
五份对外申报,零份隐私清单:端侧离线 App 说到与做到的对账
TagWorks、SlingGuard 与 QuantumLab 把"数据不出设备"写进五种载体:三种随包发布,一种在商店后台,一种只是界面与提审散文。本文拆开两款导出 ipa 与一份归档,读 Info.plist 键集、Mach-O 链接闭包与签名授权项,再和源码调用面对账,记下四处偏差与一个整份缺席。
一款应用说自己”不收集数据”,这句话究竟写在哪里?
答案不是一个文件,而是五种互不知道彼此存在的载体。密旋科技(KeyRotate Technology)的三款 iOS 应用——量子物理教育沙盒 QubitLab、电气安全检测记录工具 TagWorks、吊索具现场核验工具 SlingGuard——都把”数据不出设备”作为产品边界。这话对不对不必靠自述:随包发布的申报,装机后任何人都能读出来。
本文只做一件朴素的事:把仓库里现存的两份 SlingGuard 导出包与一份 QuantumLab 归档拆开,读出清单文件、链接闭包与签名授权项,再和工程文件、源码调用面、界面文案对账。有几处对得上,有几处对不上,还有一处整份缺席。
左右滑动查看完整图表
一、五种载体,三种可核对性
| 载体 | 写在哪个介质上 | 由谁写入 | 装机后可否复查 |
|---|---|---|---|
| 隐私用途文案 | 包内 Info.plist 的 NS 键 | 构建参数注入 | 可以,读键即可 |
| 隐私清单 | 包内 PrivacyInfo.xcprivacy | 开发者随源码提交 | 可以,看包内有无此文件 |
| 加密出口申报 | 包内 Info.plist 的 ITS 键,或后台问卷 | 开发者二选一 | 可以,看键是否存在 |
| 隐私标签 | 只在 App Store Connect 后台 | 账号持有人手工勾选 | 不可以,不在包里 |
| 描述性文本 | 界面文案、商店描述、提审回复、政策页 | 人手写成 | 界面可查,其余看存档 |
最值得注意的是最右列:五种载体里只有三种能被”把包拆开”这个动作复现。SlingGuard 的提审脚本对这一点留过一句直白的注释——其余配置都已完成,唯一剩下的闸门是 App Privacy 声明,而它不在公开 API 里,必须由账号持有人在后台一次性发布。也就是说,本地脚本写得再全,对账也天然有一个洞:有一份申报,工具读不到。
二、用途文案:缺了会崩,多了不响
系统对这类申报采取封闭默认:访问相机、麦克风、相册等受保护数据前必须静态声明意图,缺少用途串的应用会被系统终止,App Review 同样会拒绝[4][9]。申报面因此有个别扭的性质:**少写会响,多写不会。**先看三款的构建参数与调用面。
| 工程 | 用途文案 | 源码里的对应调用 |
|---|---|---|
| TagWorks | 相机 1 条 | 有:ScanView.swift 内 authorizationStatus、requestAccess |
| SlingGuard | 0 条 | 无:QRScannerView.swift 全文 137 行只 import SwiftUI |
| QuantumLab | 0 条 | 无:AVFoundation 仅用于本地课程资源解析 |
三行都对得上。问题在已经发出去的包上。
仓库里现存两份 SlingGuard 导出产物,同为 1.0(构建 1),打包相隔十二天。两份 Info.plist 各 30 个键,唯一的 NS 前缀键都是 NSCameraUsageDescription,两句文案却不一样:一份写”用相机拍摄设备标签与缺陷状态”,另一份写”用相机扫描设备上的二维码、条形码并记录缺陷”。再看可执行文件:AVFoundation、AVCaptureSession、AVCaptureDevice 三个符号在两份二进制里出现次数都是 0,从 Mach-O 加载命令里抽出的系统框架一份七个、另一份八个(Combine、CoreGraphics、Foundation、PDFKit、StoreKit、SwiftUI、UIKit,后者多一个 CoreImage),里面没有相机框架。两份 Info.plist 只有 1,446 与 1,461 字节,签名信息完整。
包在对外宣称要用相机,包里的代码没有链接相机框架。 从界面上那个带取景框的 “Scan Tag” 入口,到真有一条相机通路,中间的框架、授权与解码路径都没被走过,用途文案却先写上了。代价不是隐私而是准确性:读得到相机文案、读不到相机框架的包,会让企业侧的权限评估对上一个不存在的能力。
苹果对目的串还有一条更硬的建议:如果应用没有访问某个资源的正当理由,就把相关 API 的引用从二进制里去掉[10]。SlingGuard 后来走的正是这条,只是删除发生在工程文件那一侧——.delivery-backups/ 下 2026-09-20 与 09-21 的快照里,工程文件第 30 行还挂着这条相机文案,今天的 project.yml 里整行已不在。当前配置与源码终于一致,历史包内那句承诺留在原地,不会再有人读到。
还有两类”没有申报”是正确的设计。TagWorks 的检测记录可以挂现场照片,取证走 PhotosPicker:选择器跑在另一个进程里,应用只拿到用户点中的那几项,因此不需要相册用途文案——三份工程里 PHPhotoLibrary 与写回相册的接口都是 0 次。审核指南 5.1.1(iii) 正好推荐这条路径:能用出进程选择器或分享面板,就不要去申请对相册、通讯录的完整访问[5]。导入 CSV 与恢复归档同理,三处 fileImporter 后面都跟着一次 startAccessingSecurityScopedResource,作用域限于用户当场选中的那一个文件。
这三条通道不进申报的账,却进数据的账:申报面管”越权访问”,不管”用户主动交付”。
三、隐私清单:零份,和一份 CA92.1
第二类随包申报是 PrivacyInfo.xcprivacy。苹果列出的 required-reason API 有五类:文件时间戳、系统启动时间、磁盘空间、活动键盘列表、UserDefaults——被列名是因为它们可能被用来给设备打指纹,而文档写明无论用户是否授予跟踪权限,打指纹都不被允许;自 2024 年 5 月 1 日起,用了这些 API 却没在清单里说明理由的构建不会被接受[1][2]。把三款的调用面对到这五类上:
| API 类别 | 代表符号 | 源码实测 | 需要申报 |
|---|---|---|---|
| 文件时间戳 | creationDate、stat | 0 处 | 否 |
| 系统启动时间 | systemUptime | 0 处 | 否 |
| 磁盘空间 | statfs、volumeAvailableCapacity | 0 处 | 否 |
| 活动键盘 | activeInputModes | 0 处 | 否 |
| UserDefaults | UserDefaults | TagWorks 19 行、QuantumLab 7 行、SlingGuard 0 行 | 前两款 |
这个分布有点意外:同是一台设备上的台账,SlingGuard 全写成 JSON 文件,UserDefaults 命中为 0,可执行文件里连 NSUserDefaults 这个符号都读不出来;TagWorks 与 QuantumLab 用了 @AppStorage,落进这一类。端侧存储选型决定对外申报义务——这件事通常被记在备份与可迁移性的账上,很少被记在合规文书名下。
理由码也不是自由裁量。清单对 UserDefaults 给了几个档位:CA92.1 是读写只有本应用能访问的数据,1C8F.1 是同一 App Group 内的共享,另一档只留给第三方 SDK 的包装函数[2]。选哪一档要看签名:SlingGuard 包内描述文件解出的授权项只有五项——应用标识、测试分发标记、团队标识、get-task-allow 为假、钥匙串访问组,没有 App Group,因此能填的只有 CA92.1。而那行钥匙串访问组是默认带的:三份源码里 SecItem、CryptoKit 都是 0 次。被授予过,不等于被用过。
账落到一个数字上:三个工程、三份现存产物,PrivacyInfo.xcprivacy 是 0 份。 QuantumLab 那份归档的 296 个文件里,192 个是关卡视频、96 个是封面图,剩下的是 plist、签名资源与主程序,逐个数字典名。要区分两件事:平台说未申报的构建不会被接受,而这三款都分发出去了——“上传是否被拦”取决于校验工具识别出什么,它不是合规评审;把”过审”读成”申报完整”,是按缺件处理前不该犯的错误。补齐也不复杂:
<key>NSPrivacyTracking</key> <false/>
<key>NSPrivacyTrackingDomains</key> <array/>
<key>NSPrivacyCollectedDataTypes</key> <array/>
<key>NSPrivacyAccessedAPITypes</key> <array> <!-- UserDefaults: CA92.1 --> </array>这份文件还有个用处:它是苹果要求你亲手填写的表格,写的是”不跟踪、无跟踪域名、零收集、只用 UserDefaults 且只给自己读写”——把一句营销语义换成四份可复查的取值,尽管可信度也只到”你自己这么写”为止。
四、加密申报:一个键,和一份每版重答的问卷
ITSAppUsesNonExemptEncryption 的口径是:应用连同它链接进去的第三方库一起不使用加密,或只使用豁免范围内的加密,就把这个布尔键设为 NO;如果 Info.plist 里没有这个键,每次上传新版本,App Store Connect 都会把出口合规问卷再问一遍[6][7]。
实测三份工程里只有 TagWorks 显式写了 NO;SlingGuard 与 QuantumLab 的构建参数没有它,两份产物与一份归档里也读不到。前者把判定固化进源码与产物,评审时可以逐版 diff;后者把同一个判断每版交给人重答,答案不落进任何可追溯的文件。
“不写”是不是错的?就链接闭包看,写 NO 有依据。三份产物能读出的链接框架全部来自系统:QuantumLab 是 AVFAudio、AVFoundation、AudioToolbox、Combine、CoreFoundation、CoreGraphics、CoreMedia、Foundation、StoreKit、SwiftUI、UIKit 共 11 个,SlingGuard 是 7 到 8 个。三者都没有 CFNetwork,二进制里 NSURLSession、WebKit、CoreLocation、AdSupport、identifierForVendor 都是 0 次;工程里没有 CocoaPods 或 Swift Package 记录,源码里也没有自研加密调用。苹果的说明是:使用操作系统内置的加密(例如通过系统网络栈走 HTTPS)通常属于豁免范围,自有加密实现才需要提交文档换取合规码[7]。
这里有一处口径必须写清楚,因为密旋的主营业务恰好涉及密码产品。这个键判定的是”这一个包连同其链接闭包是否使用非豁免加密”,不是”这家公司的技术栈是否涉及密码学”——“申报为不使用非豁免加密”与”公司在做量子随机数、抗量子算法与密钥管理”可以同时为真。反向口径也要记住:这条申报走美国出口条例视角,与商用密码管理条例项下的义务是两套体系,一份商店问卷不能当作另一套体系的凭证。
还有一点常被”离线”叙事盖住:三个包都链接了 StoreKit,内购、恢复购买与凭据校验经商店基础设施完成。准确表述因此不是”不存在网络通路”,而是”不存在由应用代码发起的对外网络”。这个限定词在 TagWorks 的商店物料里已经写对了——“无账号、无需网络即可使用”——到了界面与政策页就被收紧成全称判断。
五、隐私标签:平台把”收集”定义成”出设备”
隐私标签是五种载体里唯一不在包里的一个。它的定义口径写在苹果的说明里:所谓”收集”,指以某种方式把数据传出设备,使开发者或其第三方伙伴能够访问,且保留时间超过实时处理这次请求所必需的时长;第三方伙伴指的是分析工具、广告网络、集成进应用的 SDK 或其他外部供应商[8]。
这个定义对端侧应用很关键,方向和直觉相反:本地存一张检验员签名位图、一台资产的检测照片、一份写满读数的台账,只要不离开设备、不在服务端留存,按这个口径都不算”收集”。界面上那句”不存储”反而比平台要求更严——那是一条自我约束,不是法定门槛。
“零采集”在本地能拿到的支撑是这样的:三款源码里 URLSession、WKWebView、openURL、CLLocationManager、CNContactStore、HealthKit、ASAuthorization 命中数全是 0,连一个网址字符串都没有;包体的链接闭包里没有网络框架,没有广告标识与供应商标识符,也没有第三方 SDK 可让清单机制去核对。唯一的外部通道是 StoreKit。
但这份申报的可信度止步于”开发者这么勾选”。清单存在不证明清单为真,也不证明运行时不会用别的方式把数据送出去。真正的差别在维护成本:用途文案与清单写错,会被构建、上架或复核某个环节报出来;隐私标签改一次不需要提交新版本,随时可改,也就随时可错。
六、第五种载体:界面上的绝对句
散文层是五种载体里唯一一处”承诺范围可以随语言与版本漂移”的,实测到三处。
第一处是 QuantumLab 设置页末尾的”儿童隐私与安全合规承诺”卡。它的文本不走本地化键,而是视图里的一个字面量三元分支:简体一份、繁体一份、其余全部落到英文一份——应用支持 8 种界面语言,这张卡只有 3 份文本。三份口径不一致:简体版写”不设远程服务器,无任何外部数据回传""绝不收集、不存储、不传输任何个人身份信息、地理位置或使用分析数据”;繁体版第二条缩成”絕不收集、不傳輸任何個人資訊”,“不存储""地理位置""使用分析数据”都不在,末条也只剩”嚴格遵守兒童隱私保護法”,不再提 Apple 儿童类别。付费墙与家长中心的两句走了本地化表,八种语言各写一遍 “100% Offline & COPPA Safe”。
第二处是绝对量词与链接闭包的落差。“无任何外部数据回传”是全称判断,而产物里链接了 StoreKit;付费墙功能行、家长中心徽章行与设置页承诺卡都写着 “zero external servers”,而付费墙本身就是一次与商店基础设施的交互。准确说法要加上主语与范围:应用代码不发起对外网络,内购经 App Store 完成。
第三处是”儿童类别”这个说法。1.3 条对儿童类目的约束是:不得包含跳出应用的链接与购买入口,除非安排在家长门之后;5.1.4 条要求面向儿童的应用谨慎处理个人数据[5]。QuantumLab 的提交材料把主分类记作 Education,并未声明进入儿童类目,还写着自我限制:这份草稿不是提审回执、审核结果或隐私声明,也不声称 “full legal/COPPA compliance”。同一份代码库里,提审材料刻意不做的声明,界面文案替它做了。
顺带记一处事实:儿童类目最在意的”外链”,这三款确实没有——源码里 openURL、UIApplication.shared.open 与网址字符串全为 0,唯一的 Link( 命中是站内导航用的 NavigationLink。但另一面是:5.1.1(i) 要求隐私政策既写进 App Store Connect 的元数据字段,也要”在应用内以易于访问的方式”提供[5]。后台字段与商店描述里都填了政策地址,装机之后想在应用里点开,目前没有入口——应用内唯一接近隐私政策的文本,就是那张写满全称判断的承诺卡。
最后是版本号。QuantumLab 界面写”版本 1.0.0 (构建 1)“,SlingGuard 写”1.0 (Mature Industrial Edition)“,两处都是硬编码,而 SlingGuard 工程里的构建号已经是 3;只有 TagWorks 从 Bundle.main 的版本键动态读取。界面版本号也是对外申报,本来就该由产物自己报出来。
提审回复稿是同一层的另一半:TagWorks 那份草稿顶部自带一条状态说明(2026-09-21),本稿含历史构建事实、不得原样提交,复用前必须逐句换成当次归档的证据。这类散文没有版本控制也没有回读接口,最省事的处置方式是让前面几本账的机器输出把它拼出来。
七、把对账做成命令,而不是做成记忆
上面每一处偏差都不是判断出来的,是数出来的。可复现的取数动作只有六步:
| 步 | 取什么 | 判据 |
|---|---|---|
| 1 | 包内文件清单 | PrivacyInfo.xcprivacy 是否存在 |
| 2 | Info.plist 键集 | NS 用途键是否等于申请面应有的键;ITS 键是否存在 |
| 3 | 可执行文件加载命令 | 链接闭包里是否混进网络、位置、通讯录、日历、健康框架 |
| 4 | 二进制符号普查 | NSUserDefaults 等 required-reason 符号是否命中 |
| 5 | 描述文件授权项 | 有无 App Group 与推送环境,决定理由码可选档位 |
| 6 | 界面文本普查 | 绝对量词、法律类别名、版本串是否走本地化台账 |
前 5 步都是纯读:unzip -l 列包、plutil -p 读 plist、otool -L 读链接闭包、codesign -d --entitlements - 读签名;第 6 步只能靠一份 grep 清单加人工判断。第 2 与第 3 步要一起看才有意义:文案申请了相机而链接闭包里没有相机框架,或者反过来,是同一个失效模式的两半。
还有一条现成的纪律可以接上:TagWorks 的商店物料写着”最终 App Privacy 声明必须以待提交二进制的审计结果为准”。它的可执行形式就是每次提审前把六步跑一遍,并把结果与后台那份读不到的标签一起存档。本文列的是应当补的流程,不是当前已具备的门禁。
八、为什么值得做到这一步
一周前,Google DeepMind 公布了 AlphaProof Code 对 libsignal 的形式化验证:约 650 万行 Lean 证明覆盖 PQXDH 握手与三重棘轮,把前向保密、事后保密与握手机密性证到 AArch64 机器码上;为检验判据本身,研究者把两个早已修复的真实缺陷注回代码,系统给出了”这版不安全”的证明[11]。报道也说清了范围:被验证的是那份库的编译产物,不是整个 Signal 应用。
值得端侧团队读的不是”AI 会写证明”,而是它把保证分成三档:自述(写给人看的承诺)、复查(把承诺回接到可重跑的判据)、证明(穷尽实现里所有相关路径)。第三档对 libsignal 用掉四周与六百万行脚本,端侧应用负担不起;第一档几乎免费,所以界面上才会出现三份文本不一致的承诺卡。划算的是中间那档:它不保证实现无缺陷,只保证每句承诺都被重新查一遍,查不动了就改窄措辞。
九、能力边界
对账读的是包,不是行为,边界要说清楚。
- 静态链接闭包能证明某个框架没被链进来,不能证明运行时不会以别的方式访问网络;动态加载与系统组件的自发行为不在这条判据里。
- 清单与标签都是自我声明:清单存在不证明清单为真,标签随时可在后台改,且不需要提交新版本。
- 描述文件的授权项由苹果签发,可信度高于自述,但它记录”被授予了什么”而非”用了什么”。
- 上架通过不是申报完整的证据,两者的判定口径不同。
- 备份归档里的
appVersion有一处来自默认参数而非 Bundle,它不是构建身份的证明;构建身份该由产物自己报。 - 本文只按平台文档描述申报项与判定口径,不构成法律意见;出口条例与儿童隐私法下的具体义务应另行评估。
十、结语
端侧离线是一个架构判断,不是一句形容词。它的好处是可以被数出来:包里有几份清单、链接闭包里有几个框架、Info.plist 里有几个键、界面文案里有几个字面量,这些都是整数。
实测结果大体一致:没有一处由应用代码发起的网络,没有第三方 SDK,没有设备标识符,权限只申请了相机一项且只在真正需要扫码的那款里申请——这是一份干净的调用面。同一份实测也记下:隐私清单整份缺席,加密申报只有一款固化进产物,一句相机文案在两个历史包里指向一个没有链接的框架,界面承诺卡用三份长度不同的文本写着比架构更宽的全称判断。
修这些不需要架构变更:补两份四行取值的清单,把加密申报写成一行构建参数,让界面版本号跟着包走,把三处绝对量词改成有主语的限定判断,给隐私政策补一个应用内入口。端侧的承诺要长期守住,靠的不是一次说清楚,而是每次发布都能重新数一遍。
参考资料
- [1] Apple. Describing use of required reason API. 文档
- [2] Apple. NSPrivacyAccessedAPIType(五类 API 与理由码). 文档
- [3] Apple. TN3183: Adding required reason API entries to your privacy manifest. 技术说明
- [4] Apple. QA1937: Resolving the Privacy-Sensitive Data App Rejection. 技术问答
- [5] Apple. App Store Review Guidelines §1.3、§5.1.1、§5.1.4. 指南
- [6] Apple. ITSAppUsesNonExemptEncryption. 文档
- [7] Apple. Complying with Encryption Export Regulations. 文档
- [8] Apple. App privacy details on the App Store. 说明
- [9] Apple. NSCameraUsageDescription. 文档
- [10] Apple. 5.1.1(ii) Write clear purpose strings. 帮助
- [11] Google DeepMind. Introducing AlphaProof Code(2026-11-10). 博客
- [12] Apple. PhotosPicker. 文档