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 作为二进制依赖添加时,也需要签名。
参照:
-
Privacy updates for App Store submissions - Latest News - Apple Developer
-
Privacy updates for App Store submissions - Latest News - Apple Developer
问题以及方案
通过上面的内容我们可以看出:
-
第三方的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。
paapi.txt
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
。
paapidetect.sh
#!/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
设置一次扫描文件个数。
-
参照