· engineering· 约 18 分钟精读
现场证据照片的元数据边界与端侧最小化工程:Exif 定位收敛、像素重编码与离线台账的取证取舍
端侧证据照片在进入离线台账前后,携带的信息并不相同。本文以密旋科技 TagWorks 与 SlingGuard 的端侧照片证据链为样本,逐字段拆解 Exif 定位、机身序列与内嵌缩略图各自的取证价值与隐私负担,梳理出进程选择器、像素重编码、UUID 命名与能力最小化四条实现线,并把"重编码在提升隐私的同时抹掉取证线索"这一取舍,如实写成可核对的能力边界。
一、一张现场照片,携带的不只是画面
先看一个具体的作业场景。一名特种电气作业人员在地下室配电间完成一台手持电动工具的绝缘检验,按照 AS/NZS 3760:2022 的要求,他需要在记录里附上目视检验的证据照片[1]。他掏出手机,选中相册里刚拍的那张,照片随后进入一本 100% 离线、没有账户也没有后端的端侧台账,并在交付时进入一份 PDF 报告。
大概没有人会把”照片”和”隐私”放在一起想。但从文件格式的角度看,一张由手机相机产生的 JPEG,除了画面像素之外,通常还捆着一组被称为元数据的信息块:Exif(Exchangeable image file format,现行版本由 CIPA DC-008 定义[2])写入相机厂商、机身型号与序列号、镜头参数、曝光参数、拍摄时刻,以及在定位开启时写入的 GPS 纬经度与海拔;与之平行的是 XMP(ISO 16684-1[3]);再加上一张用于快速预览的内嵌缩略图,以及由编辑或传输软件写入的 Software 标记。
这些字段不参与成像,却真实附着在文件里,并且默认跟着文件一起被复制、转发、归档。对一份要交给甲方或监管方的合规档案来说,这组字段同时踩着两条线:它一边是取证价值——拍摄时刻与拍摄地点,是”这张照片确实在作业现场拍的”这类主张最常见的佐证;另一边是隐私负担——作业者、作业场所乃至设备持有人的信息,就这样被装进一份要对外交付的文件里。
本文以密旋科技 TagWorks 与 SlingGuard 的端侧证据链为样本,讨论这个取舍如何在工程上落地:哪些字段留下、哪些字段丢弃、哪些字段从一开始就不获取,以及处理完之后必须如实承认的能力边界。
二、元数据的双面性
把字段拆开看,会发现没有一个字段是纯粹取证或纯粹负担的。下表逐项做一次盘点。
| 元数据字段 | 取证价值 | 风险与负担 |
|---|---|---|
| Exif GPS 经纬度 | 佐证拍摄地点落在作业现场 | 泄露具体点位;设备若兼作家用,可能指向居住地址 |
| 机身型号与序列号 | 佐证拍摄设备 | 构成稳定的设备指纹,可跨档案关联同一台设备 |
| 拍摄时刻 DateTimeOriginal | 佐证作业时间线 | 依附于可改的设备墙钟;与作业者作息相关 |
| 内嵌缩略图 | 与主图做一致性核对 | 部分实现里,缩略图保留的是主图被裁切前的版本 |
| Software 编辑标记 | 佐证图像处理链 | 暴露所用软件与版本 |
| XMP 结构化标注 | 承载结构化描述 | 可能携带编辑历史与作者信息 |
这张表给出的判断是:最小化不是”把元数据一概删掉”,而是为每个字段单独回答一个问题——它对本台账的法定目的而言是必要的,还是仅仅因为”默认就在那里”。存储限制与数据最小化原则在个人信息保护规范中有明确表述(例如欧盟通用数据保护条例第 5 条[4]),落到照片这种载体上,就意味着要有能力说清”为什么这个字段在这里”。
三、端侧最小化的四条实现线
这四条线有明确的先后次序。最优的一层是”能力上取不到”——应用根本没有声明位置用途,也就无从获得坐标;其次是”采集时就被限制”——只能拿到用户勾选的项;再次是”落盘时被重编码”——源容器不参与搬运;最后一层才是”落盘之后再做清理”。次序之所以重要,是因为每一层都会留下自己的痕迹:运行期清理过的文件,在存储介质层面可能仍有残留,而残留正是想要避免的状态。下面按这个次序逐条展开。
(一)采集入口:出进程选择器,而不是相册全量授权
TagWorks 的证据照片入口是 SwiftUI 的 PhotosPicker,底层是 PHPickerViewController[5]。这个控件的一个关键性质是”出进程”:选取动作发生在系统进程里,应用侧只收到用户明确勾选的若干项,而不是整个相册的读取权限。与之对应,项目的工程配置中只声明了相机用途说明(NSCameraUsageDescription),没有声明相册读取用途字符串(NSPhotoLibraryUsageDescription)。这不是遗漏,而是把”应用不该看见什么”变成编译期就可以核对的能力约束:未被勾选的照片,应用在结构上取不到。
需要注意这里的分寸:这不是一句”更尊重用户”的口号,而是一条可以被静态检查的能力边界——把用途说明交给构建配置去声明,把未声明的能力交还给操作系统去拦截。
(二)像素重编码:搬运画面,不搬运容器
真正决定元数据去留的一步,发生在解码与写盘之间。TagWorks 的导入流程是:把所选项按 Data 取出,交给 UIImage(data:) 解码为像素缓冲,再经 PhotoStore.save() 以 image.jpegData(compressionQuality: 0.8) 重新编码,最后原子写入 Documents/Photos[6]。工程语义上,UIImage 是解码后的像素数据,不携带源文件的 Exif 容器;jpegData 生成的是一个由像素与朝向构成的新 JPEG。
用集合语言把这层关系写清楚:设源容器元数据集合为 ,落盘文件仅由像素与朝向构成,
于是 与源容器元数据集合的交集为空。落到实际效果上:源文件里的 GPS、机身序列、源拍摄时刻、内嵌缩略图与 Software 标记,并没有被”搬”过来。得到的图像是一份”操作者在本次记录中选定并落盘的像素记录”,而不是一份从相机原始文件继承而来的证据副本。这个区别必须讲清楚,因为它决定了后续能主张什么、不能主张什么。
(三)文件名与索引:登记册只存引用
落盘文件名默认取 UUID,而不是原始文件名或时间戳;存储层还会拒绝包含路径分隔与 ”..” 的名字(isSafeFileName 校验),避免越权读写。这一步的实际作用是:既防止把原文件名里可能夹带的个人信息(例如”某项目-某公司-姓名-日期.jpg”)写进档案,也让文件名本身不再泄露可推断的时间或地点线索。登记册(TestRecord.photoFiles)保存的是文件名数组,正文 JSON 因而不被像素数据撑大,备份也因为只搬运引用而保持可迁移。
(四)能力最小化:不声明的能力,用不了
比”丢弃”更彻底的是”从不获取”。Exif GPS 被写入的前提,是应用能够拿到位置;而 TagWorks 的工程配置中没有声明任何位置用途字符串,也没有链接 CoreLocation。缺少这项能力,应用在结构上就取不到经纬度,也就不存在”先写进照片、再从照片里删除”这种方案——后者本身会制造一个本不该出现的中间态。
同一条原则也体现在一次代码清理上:项目曾包含一个没有任何调用点的 PrivacyPreservingTelemetry.swift,一旦被调用,它会生成每安装标识与本地遥测缓存;该文件已从应用目标中移除。100% 离线沙盒的重点不是”承诺不发送”,而是”没有可发送的路径”——没有账户、没有后端、没有遥测端点。SlingGuard 的工程说明把这条路线写成一句话:离线优先、零后端、零分析、无用户账户;其签名与图像捕获同样走本地渲染,不产出对外通道。
四、代价:被最小化掉的取证能力
最小化不是免费的。下表把代价写清楚,比含糊带过更有用。
| 能力 | 端侧现状 | 后果 |
|---|---|---|
| 用 GPS 证明拍摄地点 | 不获取位置,照片不含经纬度 | 地点佐证只能依赖记录中的资产标识与选填备注 |
| 用 Exif 时间戳佐证拍摄时刻 | 落盘时刻取自可改的设备墙钟 | 需与台账的时间纪律机制配合,才能界定可信区间 |
| 用源文件特征证明”未经处理” | 落盘即重编码,源文件不保留 | 能证明”落盘后未被改动”,不能证明”选入前未被处理” |
第三行是关键:重编码在去掉元数据的同时,也去掉了相机传感器噪声指纹一类线索,还去掉了经由元数据注入的空间。这既是隐私收益,也是取证损失——一笔需要明说的取舍,而不是包装成”两头都拿到了”。
还有一层常被忽略的代价,落在报告的可读性上。当照片在 PDF 里只以数量出现时,接收方看到的是”这条记录附了两张证据照片”,而不是照片本身。这在多数法定台账里是够用的——记录要素由测量值、目视结论、签署人与时间构成;但它意味着,一旦需要逐张核对,就必须回到那台设备上打开原始档案。这是离线优先的必然结果:没有云端副本,也就没有远端可查的副本。
五、失败与清理:最小化不等于可以不管
照片证据链上,最小化只解决”存什么”,另一半是”出错怎么办”。TagWorks 在这几处做了明确处理:
- 导入失败要可见:传输、解码、写入三类失败分别回报,避免出现”看起来已经附上、实际没有写入”。
- 取消要清理孤儿:为未保存的记录导入的照片,在取消时被清除,不留无主文件。
- 恢复要校验引用:备份恢复在做任何变更之前校验照片名与引用完整性,失败时回滚被覆盖的照片。
- 报告只列数量:导出报告在证据行标注照片数量(例如 Photos: 2),PDF 不内嵌像素副本,减少档案体积与二次扩散面。
这几条合起来是一个朴素的约定:能被删除的只有明确存在的文件,能被校验的只有明确引用的文件。做不到这一点的”最小化”,只是在制造不可解释的残留。
这里还要区分两件常被混为一谈的事:一件事是”删除了某个文件”,另一件事是”从未写入某个字节”。前者依赖删除逻辑本身可靠,并且要处理删除失败、重复删除与引用悬空这些分支;后者不依赖任何运行期的正确性。照片链上的元数据之所以按后一种方式处理,正是因为前者在存储介质层面并不总能给出确定的结论——介质上的残留与文件系统里的目录项并不是一回事,这也正是媒体清理规范要把”清除”与”销毁”分开讨论的原因[7]。对一台运行在个人设备上的应用来说,能做的是把不需要的字节挡在写入之前,而不是宣称能把已经写入的字节彻底抹掉。
六、结语:把边界写进交付物
回到开头那张地下室配电间的照片。它进入端侧台账之后,携带的信息比相机交给它的少了很多:没有 GPS 坐标、没有机身序列、没有源拍摄时刻、没有内嵌缩略图,只有一个 UUID 文件名和一份由像素与朝向构成的 JPEG;它的索引留在登记册里,它的数量出现在报告的证据行上。这件事的另一面是,档案也失去了若干本可用于自证的材料。
对应的能力边界,应当如实写下来:
- 能证明的:落盘像素在设备内部未被改动;照片来自操作者在本条记录中明确选定的一项;档案不含 GPS、机身序列与源软件标记。
- 不能证明的:照片拍摄的真实时刻与真实地点;照片在被选入应用之前是否被处理过。
- 不该做的:因为拿不到位置就伪造坐标;把重编码后的图像对外称作”相机原始文件”。
端侧离线工具的隐私立场,最终不是一句”数据不出设备”就能承载的。它需要落到每一个字段的取舍上,落到每一次失败的清理上,也落到把”证明过什么、没证明什么”一并写进档案的诚实上。
参考文献
[1] Standards Australia. AS/NZS 3760:2022 In-service safety inspection and testing of electrical equipment. https://www.standards.org.au/standards-catalogue/sa-snz/electrotechnology/el-036/as-nz-s-3760-2022 [2] CIPA. DC-008-Translation-2023 Exchangeable image file format for digital still cameras: Exif Version 2.32. https://www.cipa.jp/std/documents/e/DC-X008-Translation-2023-E.pdf [3] ISO. ISO 16684-1:2019 Graphic technology — Extensible metadata platform (XMP) specification — Part 1. https://www.iso.org/standard/75163.html [4] European Union. Regulation (EU) 2016/679 (GDPR), Article 5 — Principles relating to processing of personal data. https://eur-lex.europa.eu/eli/reg/2016/679/oj [5] Apple Developer Documentation. PhotosPicker. https://developer.apple.com/documentation/photokit/photospicker [6] Apple Developer Documentation. UIImage.jpegData(compressionQuality:). https://developer.apple.com/documentation/uikit/uiimage/jpegdata(compressionquality:) [7] NIST. SP 800-88 Rev.1 Guidelines for Media Sanitization. https://csrc.nist.gov/pubs/sp/800/88/r1/final [8] UK Health and Safety Executive. Lifting Operations and Lifting Equipment Regulations 1998 (LOLER). https://www.hse.gov.uk/work-equipment-machinery/loler.htm [9] US Occupational Safety and Health Administration. 29 CFR 1910.184 Slings. https://www.osha.gov/laws-regs/regulations/standardnumber/1910/1910.184