iOS隐私清单API检测

背景

WWDC23 上苹果引入了常用第三方 SDK 的新隐私声明和签名,并宣布开发者需要在其应用的隐私声明中声明使用一组 API 的批准理由。这些变更有助于开发者更好地了解第三方 SDK 如何使用数据、保护软件依赖关系,并为用户提供额外的隐私保护。

从3月13日开始:如果你向 App Store Connect 上传新应用或更新应用,并且使用了需要批准理由的 API,苹果将通过电子邮件通知您您的应用隐私声明中缺少理由。这是在 App Store Connect 现有通知的基础上增加的。

从5月1日开始:你需要在上传新应用或更新应用至 App Store Connect 时,包括列出的 API 的批准理由。如果您未按允许的理由使用 API,请寻找替代方案。如果您添加了一个位于常用第三方 SDK 列表中的新第三方 SDK,这些 API、隐私声明和签名要求将适用于该 SDK。确保使用包含隐私声明的 SDK 版本,并注意,当 SDK 作为二进制依赖添加时,也需要签名。

参照:

问题以及方案

通过上面的内容我们可以看出:

  • 第三方的SDK需要添加隐私清单,对于活跃的开源库来说开源作者(团队)都已经做好了调整,我们只需要更新一下版本即可,但是对于那些不活跃的开源库来说,我们需要fork一下项目,自行添加隐私清单。当然对于维护私有库的团队来说这也是个体力活,幸好我们可以先优先处理苹果官方清单列举的部分;

  • 当SDK作为二进制依赖(其他方式来看目前并不需要)添加时,需要签名,具体的签名可以参照[WWDC2023视频](Verify app dependencies with digital signatures – WWDC23 – Videos – Apple Developer)。

通过查看具体的隐私清单要求得知,我们主要需要添加两项内容,隐私数据以及API调用,隐私数据和我们之前在App Store审核中的App隐私数据部分是一致的,对于这部分数据似乎只能自行判断并添加相关内容,但是对于API的调用还是可以通过脚本做相应的检测,毕竟我们并不能在编码时还能够记住是否使用了相关的API。

参照:

Privacy manifest files | Apple Developer Documentation

API检测

为了能够保证我们在更新代码之后判断是否有调用相关的隐私清单API,我写了一个脚本用来检测项目中API的调用和PrivacyInfo.xcprivacy文件中的声明是否一致,可以将以下文件放在项目目录下,在终端运行或者添加到Xcode script中。

注意:Xcode执行报错:Operation not permitted,前往Build Settings,User Script Sandboxing值修改为No。

NSPrivacyAccessedAPIType:NSPrivacyAccessedAPICategoryFileTimestamp
NSFileCreationDate
.creationDateKey
NSFileModificationDate
fileModificationDate
NSURLContentModificationDateKey
.contentModificationDateKey
NSURLCreationDateKey
.creationDateKey
getattrlist
getattrlistbulk
fgetattrlist
st_atimespec
st_blksize
st_blocks
st_ctimespec
st_dev
st_flags
st_gen
st_gid
st_ino
st_lspare
st_mode
st_mtimespec
st_nlink
st_qspare
st_rdev
st_size
st_uid
fstat
fstatat
lstat
getattrlistat
NSPrivacyAccessedAPIType:NSPrivacyAccessedAPICategorySystemBootTime
systemUptime
mach_absolute_time
NSPrivacyAccessedAPIType:NSPrivacyAccessedAPICategoryDiskSpace
NSURLVolumeAvailableCapacityKey
.volumeAvailableCapacityKey
NSURLVolumeAvailableCapacityForImportantUsageKey
.volumeAvailableCapacityForImportantUsageKey
NSURLVolumeAvailableCapacityForOpportunisticUsageKey
.volumeAvailableCapacityForOpportunisticUsageKey
NSURLVolumeTotalCapacityKey
.volumeTotalCapacityKey
NSFileSystemFreeSize
.systemFreeSize
NSFileSystemSize
.systemSize
statfs
statvfs
fstatfs
fstatvfs
getattrlist
fgetattrlist
getattrlistat
NSPrivacyAccessedAPIType:NSPrivacyAccessedAPICategoryActiveKeyboards
activeInputModes
NSPrivacyAccessedAPIType:NSPrivacyAccessedAPICategoryUserDefaults
UserDefaults

paapi.txt文件主要包含隐私清单中列举的API

#!/bin/bash
# PrivacyInfo.xcprivacy file path
privacy_info_file_path=""
number_of_process=4
# The number of files processed each time
number_of_files=10
# Parsing named parameters
while [[ "$#" -gt 0 ]]; do
    case $1 in
        --ppath|-pp) privacy_info_file_path="$2"; shift ;;
        --nprocess|-np) number_of_process="$2"; shift ;;
        --nfiles|-nf) number_of_files="$2"; shift ;;
        *) echo "Unknown parameter passed: $1"; exit 1 ;;
    esac
    shift
done
# Specify the current directory as the search directory
search_directory="."
api_file_path="paapi.txt"
# Check if the file exists
if [ ! -f "$api_file_path" ]; then
  echo "Error: paapi.txt file not found in the current directory."
  exit 1
fi
api_type=""
result_type=""
error_found=0 
# Read each line from the file and perform a search operation
while IFS= read -r search_text; do
  # Check if the search string starts with "NSPrivacyAccessedAPIType:*"
  if [[ $search_text == NSPrivacyAccessedAPIType:* ]]; then
      api_type="${search_text#*:}"
      # Reset result_type when the type changes
      result_type=""
      echo "APIType: ${api_type}"
  else
    # Check if the search string is not empty or does not consist only of spaces
    if [ -n "$(echo "$search_text" | tr -d '[:space:]')" ]; then
      # Process the search string to preserve spaces
      formatted_search_text=$(printf "%s" "$search_text")
      # Initialize an empty string to collect results
      all_results=""
      all_results_echo=""
      # Use find command to search and grep to match the search string
      result=$(find "$search_directory" ( -path "./Pods" -o -path "./Tests" ) -prune -o 
      -type f ( -name "*.h" -o -name "*.m" -o -name "*.mm" -o -name "*.swift" ) 
      -print0 | xargs -0 -P 4 -n 10 grep -H "$search_text")
      if [ -n "$result" ]; then
        # Check if the corresponding Type is in the PrivacyInfo.xcprivacy file
        if [ -z "$privacy_info_file_path" ]; then
          privacy_info_file_path=$(find "$search_directory" ( -path "./Pods" -o -path "./Tests" ) -prune -o 
          -type f -name "*xcprivacy" -print -quit)
        fi
        if [ -n "$privacy_info_file_path" ]; then
          if [ -z "$result_type" ]; then
            # Assign value when result_type is empty
            result_type=$(grep -H "$api_type" "$privacy_info_file_path")
          fi
        fi
        # Accumulate results
        all_results="$all_results$result"
        # Accumulate result output
        if [ -n "$all_results_echo" ]; then
          # Only add a newline if all_results is not empty
          all_results_echo="${all_results_echo}n"
        fi
        all_results_echo="$all_results_echo$result"
      fi
      # Check if any results were accumulated
      if [ -n "$all_results" ]; then
        echo "Files using '${search_text}':"
        echo "$all_results_echo"
        if [ -z "$result_type" ]; then
          error_found=1
          echo "Error: PrivacyInfo.xcprivacy file did not include NSPrivacyAccessedAPIType:${api_type}."
        else
          echo "Success: PrivacyInfo.xcprivacy has included NSPrivacyAccessedAPIType:${api_type}."
        fi
      else
        echo "'${search_text}' was not used."
      fi
    fi
  fi
done < "$api_file_path"
# Check if any errors were found
if [ "$error_found" -eq 1 ]; then
  exit 1
fi

paapidetect.sh检测项目中是否调用隐私清单中的API并检查PrivacyInfo.xcprivacy文件中是否有包含对应的NSPrivacyAccessedAPIType

  • 支持输出相关隐私清单API是否调用以及输出调用的部分;

  • 支持检测PrivacyInfo.xcprivacy文件中是否有包含对应的Type,对应Reason需要自行判断;

  • 支持设置调用参数。

    • --ppath-pp设置PrivacyInfo.xcprivacy文件相对路径,如果不设置会默认查找项目目录下的首个.xcprivacy文件,因此此脚本并不适合检测有多个.xcprivacy文件的项目,例如你想一次性检测工程中使用pod导入的所有依赖库。当然并不建议如此操作,此脚本更建议放在各个依赖库下,检测工作交给各依赖库来做,而且脚本中排除了Pod以及Test文件夹的扫描。当然如果你仅仅是想看一下工程以及依赖库下对于隐私API的使用,可以移除脚本中的( -path "./Pods" -o -path "./Tests" ) -prune -o

    • --nprocess-np设置进程个数,--nfiles-nf设置一次扫描文件个数。

参照

iOS17 隐私协议适配详解 –