概述
做为一个项目驱动的公司,在管理APP描述文件的时候,时常会遇到描述文件过期的问题,产生的影响是导致客户端应用无法使用,这时候用户会不知所措,给用户的体验造成影响,甚至会影响APP的流量。而大多数解决方案,是利用市场上的重签名工具对应用重新签名发布,或者利用Xcode重新打包发布,这对开发者来说极不友好。我们绝不能亡羊补牢,必须在风险到来前提前准备防御,在用户无感知的情况下修复问题。出于此目的,应用重签名技术便有了它的意义,如果你是还没有研究过或者正在研究重签名技术的话,那这篇文章会非常适合你。
前置条件
iOS应用重签名具有一定的前置条件和软件依赖,并不是跨平台的,至少这套方案不能在 Window 上使用。
依赖条件
MacOS操作系统
Apple开发者账号
/usr/libexec/PlistBuddy (MacOS自带)
security (MacOS自带)
codesign (MacOS自带)
zip
unzip
上述依赖条件为系统软件,如果 zip 和 unzip 软件不存在的话可以通过 HomeBrew 进行安装.
brew install zip;
brew install unzip;
iPA 简述
做为一个移动开发者应该知道 Android 的应用安装包为*.apk,而iOS的应用安装包则为*.ipa,事实上这些安装包都是可以通过unzip命令进行解压缩的压缩包,提取出来的文件是应用所需要的资源文件、应用配置文件、应用描述文件和应用二进制文件等,其中应用描述文件则是我们这次操作的目标。
除了应用描述文件需要关注外,在iPA中还存在一类应用,那就是 appex。这类应用为宿主应用的拓展应用,appex同样存在自己的描述文件,如果宿主应用描述文件更新成功,但是appex应用没有更新描述文件的话,同样是签名失败的。
总结,如果应用需要重签名的话,则需要判断是否存在appex应用,如果存在则需要对每一个 appex 目录进行重签名,最后对整体目录进行重签名,如果不存在的话则只需要对主目录进行重签名即可。
我们可以在爱思助手下载应用的iPA包,然后通过可视化软件进行解压。
描述文件中 BundleId 的能力
每一个 BundleId 都会有对应的 Capabilities,签名目标所拥有的能力一定不能比当前新描述文件所具备的能力少,如果新描述文件所具备的能力少于目标签名的对象,则签名同样失败。
重签名核心操作
重签名的核心操作是 codesign,该命令是 MacOS 下自带的命令,主要用于代码签名。
签名命令:
codesign -f -s "DEVELOPER_TEAM" "PLISTFILE_PATH" "SIGN_DIRECTORY"
其中 -f 代表 force 强制更新,-s 代表 sign。
-f, --force
When signing, causes codesign to replace any existing signature on the path(s) given. Without
this option, existing signatures will not be replaced, and the signing operation fails.
-s, --sign identity
Sign the code at the path(s) given using this identity. See SIGNING IDENTITIES below.
操作简述
分析完毕后,设计思路应该是:
解压目标
ipa包到临时目录
将应用下的embedded.mobileprovision替换为新的描述文件
将新描述文件转换为info.plist文件到临时目录
从上面操作后的info.plist中提取TeamName, Entitlements信息
最后利用重签名的核心操作对目录进行重签名
上述过程中的第2,3,4,5步,如果appex应用存在也需要重新同样的操作,如果不存在则不需要执行
签名完毕后,利用zip命令对应用重新打包成ipa文件
删除临时目录生成的垃圾文件
到此为止重签名的操作就完成了,我们利用 Shell 脚本将操作实现。
第一步:创建临时目录
创建临时目录,用来保存操作过程中产生的垃圾
# 创建临时目录
ROOT_PATH=$(pwd)
DIR_TMP_PATH="${ROOT_PATH}/temp"
# 删除旧目录并创建临时目录
rm -rf $DIR_TMP_PATH
mkdir $DIR_TMP_PATH
第二步:解压目标iPA
将需要重签名ipa包目录,解压到临时文件
# 从用户输入的参数中获取 iPA 地址
PARAM_IPA_PATH=$1
# 解压到临时目录
unzip -d $DIR_TMP_PATH $PARAM_IPA_PATH
第三步:封装重签名操作过程
无论是宿主应用还是拓展应用,其重签名的操作过程其实是一样的,我们把其封装成方法
# 从描述文件中提取完整plist文件
_getPlistFile(){
local _path=$1
local _name=$2
local originMobileprovisionPath="${_path}/embedded.mobileprovision"
local tempEntitle="${DIR_ENTITLEMENTS_TMP_PATH}/${_name}_temp.plist"
local entitle="${DIR_ENTITLEMENTS_TMP_PATH}/${_name}.plist"
security cms -D -i "$originMobileprovisionPath" > "${tempEntitle}"
# 提取 Entitlements 字段
/usr/libexec/PlistBuddy -x -c 'Print:Entitlements' $tempEntitle > $entitle
}
# 重签名拓展应用
reSign(){
local _path=$1
local _appexName=$2
local _tmpMbArray=(${_appexName/./ })
local _tmpMbName=${_tmpMbArray[0]}
# 把新的描述文件替换旧版描述文件
local _mbPath="${_path}/embedded.mobileprovision";
rm -rf _mbPath
for index in $(seq 0 ${#PARAM_APPEXMOBILEPROVISION[@]})
do
local appexMb=${PARAM_APPEXMOBILEPROVISION[index]}
local _tmpStrArray=(${appexMb })
local last=${#_tmpStrArray[@]}
((last-=1))
if [ $last -ge 0 ]
then
local _newAppexMb=${_tmpStrArray[last]}
if [[ $_newAppexMb =~ $_tmpMbName ]];
then
# 复制新的文件到目标目录
cp $appexMb $_mbPath
break
fi
fi
done
_getPlistFile $_path $_tmpMbName
local _plistPath="${DIR_ENTITLEMENTS_TMP_PATH}/${_tmpMbName}.plist";
echo $PARAM_DEVELOPTEAM
echo $_plistPath
echo $_path
codesign -f -s "${PARAM_DEVELOPTEAM}" --entitlements "${_plistPath}" "${_path}"
}
第四步:递归遍历所有目录
我们需要对所有文件夹进行判断,并且进行重签名操作
# 递归遍历目录
recursivePath(){
local _path=$1;
for item in $(ls "$_path")
do
local subPath="${_path}/${item}";
if [[ ${item} =~ '.appex' ]];
then
# 对应用拓展进行重签名
reSign "${subPath}" $item
else
if [ -d "$subPath" ];
then
recursivePath $subPath
fi
fi
done
}
# 宿主应用进行重签名操作
reSign "宿主应用描述文件路径" "宿主应用根目录"
第五步:打包新的iPA包
重签名操作完成后,我们新的目录重新进行压缩打包。
cd "新的文件目录中,必须到 /Payload 目录级"
zip -r "New.ipa" ./Payload
mv ./New.ipa "输出目录"
第六步:删除垃圾文件
所有操作完成后,就可以删除垃圾文件,这样我们的重签名操作就完成了。
# 移除垃圾文件
rm -rf $DIR_TMP_PATH
拓展延伸
为什么需要设计 Shell 脚本,因为方便和 Jenkins 等平台对接,我们利用脚本可以在服务端进行定时判断,可以在应用过期一个月前提前通知到开发者,这样开发者只需要提前一个月上传新的描述文件,系统将会自动完成更新。
利用 PlistBuddy 命令可以设计出更多个性化的功能,例如可以自定义版本号,APP名称等。