iOS 照片导出隐私验收:清除 GPS 元数据并复检文件

Security ·约 8 分钟阅读

iOS 照片导出隐私验收:清除 GPS 元数据并复检文件

照片分享界面不显示拍摄地点,导出的文件却仍可能带着坐标。做 iOS 图片导出功能时,验收对象应当是接收方最终拿到的文件,而不是应用内的预览图。在云端 Mac 上准备一张含 GPS 信息的测试照片,就能把这项隐私要求变成重复执行的文件检查。

先确定导出边界

一条常见路径是:选择照片、编辑或缩放、生成临时文件,再把文件交给分享流程。每一步都可能改变元数据的处理方式。只检查选择器返回的图片,无法证明最终文件已清理;只检查分享前的缩略图也不行。

测试夹具要由团队自行制作,并确认原文件确实包含 GPS 字典。不要拿真实用户照片做持续集成样本,也不要把带坐标的夹具放进公开仓库。可以把夹具存放在受控的测试资产目录,记录预期格式与像素尺寸;测试结束后清理生成的文件。

本文的通过条件是「最终文件没有 GPS 字典」,不是「导出文件与原图逐字节相同」。重新编码后,文件哈希通常会变化。

用 ImageIO 生成并复查文件

下面的 Swift 脚本读取输入图像,解码后按原文件类型重新编码。它只显式传递方向,不把输入文件的元数据字典整体交给输出端。脚本随后重新打开输出文件,检查 GPS 字典、尺寸、方向和格式。先在隔离目录保存为 redact.swift:

import Foundation
import ImageIO
import CoreGraphics

guard CommandLine.arguments.count == 3 else {
    fatalError("Usage: redact input output")
}

let input = URL(fileURLWithPath: CommandLine.arguments[1])
let output = URL(fileURLWithPath: CommandLine.arguments[2])
guard input.standardizedFileURL != output.standardizedFileURL else {
    fatalError("Input and output must differ")
}
guard let source = CGImageSourceCreateWithURL(input as CFURL, nil),
      let type = CGImageSourceGetType(source),
      let image = CGImageSourceCreateImageAtIndex(source, 0, nil),
      let original = CGImageSourceCopyPropertiesAtIndex(source, 0, nil)
        as? [CFString: Any],
      let destination = CGImageDestinationCreateWithURL(
        output as CFURL, type, 1, nil
      ) else {
    fatalError("Cannot read input or create output")
}

let orientation = original[kCGImagePropertyOrientation] ?? 1
let options: [CFString: Any] = [
    kCGImagePropertyOrientation: orientation
]
CGImageDestinationAddImage(destination, image, options as CFDictionary)
guard CGImageDestinationFinalize(destination),
      let result = CGImageSourceCreateWithURL(output as CFURL, nil),
      let finalImage = CGImageSourceCreateImageAtIndex(result, 0, nil),
      let metadata = CGImageSourceCopyPropertiesAtIndex(result, 0, nil)
        as? [CFString: Any] else {
    fatalError("Cannot verify output")
}

guard metadata[kCGImagePropertyGPSDictionary] == nil,
      CGImageSourceGetType(result) == type,
      finalImage.width == image.width,
      finalImage.height == image.height,
      String(describing: metadata[kCGImagePropertyOrientation] ?? 1)
        == String(describing: orientation) else {
    fatalError("Export verification failed")
}
print("GPS removed; format, dimensions and orientation checked")

在云端 Mac 的终端运行:

swiftc -framework Foundation -framework ImageIO \
  -framework CoreGraphics redact.swift -o redact
./redact fixtures/with-gps.jpg output/without-gps.jpg

先创建 output 目录,并用已核实含 GPS 字典的 JPEG 替换示例输入。运行成功只说明这一次输出通过检查。正式应用若有裁剪、压缩或分享扩展路径,应把检查接在各路径产生的最终文件之后,而不是以脚本代替应用的实际导出实现。

给结果加上反向用例

只有「清理后通过」不足以证明断言有效。用同一套读取逻辑检查输入夹具:它必须检出 GPS 字典;再检查输出:字典必须不存在。如果输入本来就没有坐标,两边都通过的测试没有鉴别力。为避免日志泄露,不要打印坐标值或完整元数据。

还要保留一份不经清理的复制文件作为反向用例,确认检查器会拒绝它。复制时直接复制文件字节,不要经过会改写元数据的图片工具。这样既验证清理步骤,也验证验收器没有把「读取失败」误当成「没有 GPS」。

重新编码后还要看什么

移除定位信息不等于图片功能全部正常。解码并重新编码可能改变有损压缩质量、色彩表现或其他非定位元数据。尺寸相同,也不代表像素逐点相同。把不同来源的照片放进回归集,至少覆盖竖拍、横拍及不同方向标记;人工查看最终文件是否旋转、偏色或明显失真。

检查项 通过条件
定位信息 最终文件不存在 GPS 字典
图像尺寸 与该导出路径预期的宽高一致
显示方向 按输出方向标记显示正确
文件格式 接收方拿到的是预期格式
读取失败 测试失败,不按「无 GPS」处理

示例脚本要求尺寸与输入一致,适合不缩放的导出路径。若产品会裁剪或压缩,应把尺寸断言改成该路径的明确预期。若需要保留色彩配置或特定版权字段,也应为这些字段分别写测试;不要为了保留它们而把原始元数据字典原样复制过去。

接入云端 Mac 的验收流程

将夹具、应用实际生成的文件和检查脚本分开放置。流水线先执行导出,再读取最终交付路径上的文件;文件不存在、无法解码或元数据读取失败,都应直接失败。并行任务使用各自的输出目录,避免上一次运行留下的文件制造假通过。

最后做一次人工核对:从应用完成真实分享或保存操作,用接收方拿到的文件复检,而不只看中间缓存。这能发现「编辑预览已清理、分享时却重新引用原文件」一类路径错误。通过后删除临时导出物;测试报告只记录是否存在 GPS 字典、尺寸、方向及失败步骤,不记录坐标。这样验收结论既能复现,也不会在排查日志里再次暴露要清理的信息。

常见问题

界面不显示拍摄地点,是否就说明导出的照片没有 GPS 信息?

不能。界面预览与导出文件是两条路径,必须读取最终导出文件的元数据,确认其中不存在 GPS 字典。

移除 GPS 信息后,能否只比较导出文件的哈希值?

不能。重新编码通常会改变文件字节;应分别检查 GPS 字典、图像尺寸、方向和实际显示效果。

按需要提交租期

用独享物理 Mac mini 验证下一步

VMCommit 提供 M4 独享物理 Mac mini,支持按天、周、月或季租用。选择机型与节点后,可在控制台查看实时可用性。

立即租用 Mac mini