iOS 照片匯出隱私驗收:清除 GPS 中繼資料

Security ·約 8 分鐘閱讀

iOS 照片匯出隱私驗收:清除 GPS 中繼資料

照片分享畫面沒有顯示拍攝地點,匯出的檔案卻仍可能帶有座標。驗收 iOS 照片匯出功能時,應檢查接收方最終取得的檔案,而不是 App 內的預覽圖。在雲端 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。執行成功只代表這一次的輸出通過檢查。若正式 App 有裁切、壓縮或分享延伸功能等路徑,應在各路徑產生最終檔案後進行檢查,而不是以這支指令碼取代 App 實際的匯出實作。

加入反向測試案例

只有「清除後通過」不足以證明斷言有效。使用相同的讀取邏輯檢查輸入素材時,必須偵測到 GPS 字典;檢查輸出檔案時,該字典則必須不存在。如果輸入原本就沒有座標,兩邊都通過的測試便沒有鑑別力。為避免透過日誌洩露資訊,不要列印座標值或完整中繼資料。

另外,保留一份未經清除、直接複製的檔案作為反向測試案例,確認檢查器會拒絕它。複製時應直接複製檔案位元組,不要經過可能改寫中繼資料的影像工具。這樣既能驗證清除步驟,也能確認驗收程式沒有把「讀取失敗」誤判為「沒有 GPS」。

重新編碼後還要檢查什麼

移除定位資訊不代表影像功能全都正常。解碼並重新編碼可能改變破壞性壓縮品質、色彩呈現或其他非定位中繼資料。尺寸相同,也不代表每個像素都相同。將不同來源的照片納入回歸測試集,至少涵蓋直拍、橫拍及不同的方向標記;並人工查看最終檔案是否有旋轉錯誤、偏色或明顯失真。

檢查項目 通過條件
定位資訊 最終檔案不存在 GPS 字典
影像尺寸 符合該匯出路徑預期的寬度與高度
顯示方向 依輸出方向標記正確顯示
檔案格式 接收方取得預期格式的檔案
讀取失敗 判定測試失敗,不視為「沒有 GPS」

範例指令碼要求尺寸與輸入相同,適用於不縮放的匯出路徑。若產品會裁切或壓縮,應將尺寸斷言改為該路徑明確的預期值。若需要保留色彩設定或特定著作權欄位,也應分別為這些欄位編寫測試;不要為了保留它們而原封不動地複製整份原始中繼資料字典。

納入雲端 Mac 的驗收流程

將測試素材、App 實際產生的檔案與檢查指令碼分開存放。流水線應先執行匯出,再讀取最終交付路徑上的檔案;檔案不存在、無法解碼或無法讀取中繼資料時,都應直接判定失敗。並行工作應各自使用獨立的輸出目錄,避免上一次執行留下的檔案造成誤判通過。

最後再進行一次人工核對:透過 App 完成實際的分享或儲存操作,並以接收方取得的檔案重新檢查,而非只看中間快取。這能找出「編輯預覽已清除資訊,分享時卻重新引用原始檔案」之類的路徑錯誤。通過後刪除暫時匯出的檔案;測試報告只記錄 GPS 字典是否存在、尺寸、方向及失敗步驟,不記錄座標。如此一來,驗收結論既能重現,也不會在排查問題的日誌中再次洩露原本要清除的資訊。

常見問題

畫面沒有顯示拍攝地點,代表匯出的照片沒有 GPS 資訊嗎?

不代表。畫面預覽與檔案匯出是不同流程,必須讀取最終匯出檔案,確認其中沒有 GPS 字典。

清除 GPS 後,可以用檔案雜湊值驗證結果嗎?

不能只靠雜湊值。重新編碼通常會改變檔案位元組;應分別檢查 GPS 字典、尺寸、方向及實際顯示效果。

租期自由選

租用專屬實體 Mac mini,驗證你的下一步

VMCommit 提供 M4 專屬實體 Mac mini,可按日、週、月或季租用。選擇機型和節點後,即可在控制台查看即時供應狀況。

立即租用 Mac mini