Android 新一代多渠道打包神器
作者 :李涛
ApkChannelPackage是一种高速多渠道打包工具。同一时候支持基于V1签名和V2签名进行多渠道打包。插件本身会自己主动检測Apk使用的签名方法,并选择合适的多渠道打包方式。对使用者来说全然透明。
概述
众所周知,由于国内Android应用分发市场的现状。我们在公布APP时,一般须要生成多个渠道包。上传到不同的应用市场。
这些渠道包须要包括不同的渠道信息,在APP和后台交互或者数据上报时,会带上各自的渠道信息。这样,我们就能统计到每一个分发市场的下载数、用户数等重要数据。
普通的多渠道打包方案
既然我们须要进行多渠道打包。那我们就看下最常见的多渠道打包方案。
Android Gradle Plugin
Gradle Plugin本身提供了多渠道的打包策略:
首先,在AndroidManifest.xml中加入渠道信息占位符:
<meta-data
android:name="InstallChannel" android:value="${InstallChannel}" />
然后。通过Gradle Plugin提供的productFlavors
标签。加入渠道信息:
productFlavors{
"YingYongBao"{
manifestPlaceholders = [InstallChannel : "YingYongBao"]
}
"360"{
manifestPlaceholders = [InstallChannel : "360"]
}
}
这样。Gradle编译生成多渠道包时,会用不同的渠道信息替换AndroidManifest.xml中的占位符。我们在代码中。也就能够直接读取AndroidManifest.xml中的渠道信息了。
可是。这样的方式存在一些缺点:
1)每生成一个渠道包。都要又一次执行一遍构建流程,效率太低,仅仅适用于渠道较少的场景。
2)Gradle会为每一个渠道包生成一个不同的BuildConfig.java类,记录渠道信息,导致每一个渠道包的DEX的CRC值都不同。普通情况下,这是没有影响的。可是假设你使用了微信的Tinker热补丁方案,那么就须要为不同的渠道包打不同的补丁,这全然是不能够接受的。(由于Tinker是通过对照基础包APK和新包APK生成差分补丁,然后再把补丁和基础包APK一起合成新包APK。这就要求用于生成差分补丁的基础包DEX和用于合成新包的基础包DEX是全然一致的。即:每一个基础渠道包的DEX文件是全然一致的,不然就会合成失败)
ApkTool
ApkTool是一个逆向分析工具。能够把APK解开,加入代码后。又一次打包成APK。因此,基于ApkTool的多渠道打包方案分为下面几步:
复制一份新的APK
通过ApkTool工具,解压APK(apktool d origin.apk)
删除已有签名信息
加入渠道信息(能够在APK的不论什么文件加入渠道信息)
通过ApkTool工具,又一次打包生成新APK(apktool b newApkDir)
又一次签名
经过測试,这样的方案全然是可行的。
长处:
不须要又一次构建新渠道包,仅须要复制改动就能够了。而且由于是又一次签名。所以同一时候支持V1和V2签名。
缺点:
ApkTool工具不稳定,以前遇到过升级Gradle Plugin版本号后,低版本号ApkTool解压APK失败的情况。
生成新渠道包时。须要又一次解包、打包和签名,而这几步操作又是相对照较耗时的。
经过測试:生成企鹅电竞10个渠道包须要16分钟左右,尽管比Gradle Plugin方案降低非常多耗时。
可是若须要同一时候生成上百个渠道包,则须要几个小时,显然不适合渠道非常多的业务场景。
那有没有一种方案:能够在加入渠道信息后。不须要又一次签名那?首先我们要了解一下APK的签名和校验机制。
数据摘要、数字签名和数字证书
在进一步学习V1和V2签名之前,我们有必要学习一下签名相关的基础知识。
数据摘要
数据摘要算法是一种能产生特定输出格式的算法,其原理是依据一定的运算规则对原始数据进行某种形式的信息提取,被提取出的信息就是原始数据的消息摘要,也称为数据指纹。
普通情况下,数据摘要算法具有下面特点:
不管输入数据有多大(长),计算出来的数据摘要的长度总是固定的。比如:MD5算法计算出的数据摘要有128Bit。
普通情况下(不考虑碰撞的情况下),仅仅要原始数据不同,那么其相应的数据摘要就不会同样。同一时候,仅仅要原始数据有不论什么改动,那么其数据摘要也会全然不同。
即:同样的原始数据必有同样的数据摘要。不同的原始数据,其数据摘要也必定不同。
不可逆性。即仅仅能正向提取原始数据的数据摘要。而无法从数据摘要中恢复出原始数据。
著名的摘要算法有RSA公司的MD5算法和SHA系列算法。
数字签名和数字证书
数字签名和数字证书是成对出现的,两者不可分离(数字签名主要用来校验数据的完整性。数字证书主要用来确保公钥的安全发放)。
要明白数字签名的概念,必须要了解数据的加密、传输和校验流程。普通情况下,要实现数据的可靠通信,须要解决下面两个问题:
1.确定数据的来源是其真正的发送者。
2.确保数据在传输过程中。没有被篡改,或者若被篡改了。能够及时发现。
而数字签名。就是为了解决这两个问题而诞生的。
首先。数据的发送者须要先申请一对公私钥对,并将公钥交给数据接收者。
然后,若数据发送者须要发送数据给接收者。则首先要依据原始数据,生成一份数字签名,然后把原始数据和数字签名一起发送给接收者。
数字签名由下面两步计算得来:
1.计算发送数据的数据摘要
2.用私钥对提取的数据摘要进行加密
这样,数据接收者拿到的消息就包括了两块内容:
1.原始数据内容
2.附加的数字签名
接下来。接收者就会通过下面几步,校验数据的真实性:
- 用同样的摘要算法计算出原始数据的数据摘要。
- 用预先得到的公钥解密数字签名。
- 对照签名得到的数据是否一致,假设一致,则说明数据没有被篡改,否则数据就是脏数据了。
由于私钥仅仅有发送者才有,所以其它人无法伪造数字签名。这样通过数字签名就确保了数据的可靠传输。
综上所述。数字签名就是仅仅有发送者才干产生的别人无法伪造的一段数字串,这段数字串同一时候也是对发送者发送数据真实性的一个有效证明。
想法虽好。可是上面的整个流程,有一个前提。就是数据接收者能够正确拿到发送者的公钥。
假设接收者拿到的公钥被篡改了,那么坏人就会被当成好人,而真正的数据发送者发送的数据则会被视作脏数据。
那怎么才干保证公钥的安全性那?这就要靠数字证书来攻克了。
数字证书是由有公信力的证书中心(CA)颁发给申请者的证书,主要包括了:证书的公布机构、证书的有效期、申请者的公钥、申请者信息、数字签名使用的算法,以及证书内容的数字签名。
可见,数字证书也用到了数字签名技术。
仅仅只是签名的内容是数据发送方的公钥。以及一些其它证书信息。
这样数据发送者发送的消息就包括了三部分内容:
- 原始数据内容
- 附加的数字签名
- 申请的数字证书。
接收者拿到数据后,首先会依据CA的公钥,解码出发送者的公钥。然后就与上面的校验流程全然同样了。
所以,数字证书主要攻克了公钥的安全发放问题。
因此,包括数字证书的整个签名和校验流程例如以下图所看到的:
V1签名和多渠道打包方案
V1签名机制
默认情况下,APK使用的就是V1签名。解压APK后,在META-INF文件夹下,能够看到三个文件:MANIFEST.MF、CERT.SF、CERT.RSA。它们都是V1签名的产物。
当中。MANIFEST.MF
文件内容例如以下所看到的:
它记录了APK中全部原始文件的数据摘要的Base64编码,而数据摘要算法就是SHA1
。
CERT.SF
文件内容例如以下所看到的:
SHA1-Digest-Manifest-Main-Attributes
主属性记录了MANIFEST.MF
文件全部主属性的数据摘要的Base64编码。SHA1-Digest-Manifest
则记录了整个MANIFEST.MF
文件的数据摘要的Base64编码。
其余的普通属性则和MANIFEST.MF
中的属性一一相应,分别记录了相应数据块的数据摘要的Base64编码。比如:CERT.SF
文件里skin_drawable_btm_line.xml相应的SHA1-Digest,就是下面内容的数据摘要的Base64编码。
Name: res/drawable/skin_drawable_btm_line.xml
SHA1-Digest: JqJbk6/AsWZMcGVehCXb33Cdtrk=
\r\n
这里要注意的是:最后一行的换行符是不可缺少,须要參与计算的。
CERT.RSA
文件包括了对CERT.SF
文件的数字签名和开发人员的数字证书。
RSA就是计算数字签名使用的非对称加密算法。
V1签名的详细流程可參考SignApk.java,整个签名流程例如以下图所看到的:
整个签名机制的终于产物就是MANIFEST.MF、CERT.SF、CERT.RSA三个文件。
V1校验流程
在安装APK时。Android系统会校验签名。检查APK是否被篡改。代码流程是:PackageManagerService.java
-> PackageParser.java
,PackageParser
类负责V1签名的详细校验。
整个校验流程例如以下图所看到的:
若中间不论什么一步校验失败。APK就不能安装。
OK。了解了V1的签名和校验流程。
我们来看下。V1签名是怎么保证APK文件不被篡改的?
首先。假设破坏者改动了APK中的不论什么文件,那么被篡改文件的数据摘要的Base64编码就和MANIFEST.MF
文件的记录值不一致,导致校验失败。
其次。假设破坏者同一时候改动了相应文件在MANIFEST.MF
文件里的Base64值,那么MANIFEST.MF
中相应数据块的Base64值就和CERT.SF文件里的记录值不一致。导致校验失败。
最后,假设破坏者更进一步,同一时候改动了相应文件在CERT.SF
文件里的Base64值,那么CERT.SF
的数字签名就和CERT.RSA
记录的签名不一致,也会校验失败。
那有没有可能继续伪造CERT.SF
的数字签名那?理论上不可能。由于破坏者没有开发人员的私钥。那破坏者是不是能够用自己的私钥和数字证书又一次签名那。这倒是全然能够。
综上所述,不论什么对APK文件的改动。在安装时都会失败。除非对APK又一次签名。可是同样包名,不同签名的APK也是不能同一时候安装的。
APK文件结构
由上述V1签名和校验机制可知。改动APK中的不论什么文件都会导致安装失败!
那怎么加入渠道信息那?仅仅能从APK的结构入手了。
APK文件本质上是一个ZIP压缩包,而ZIP格式是固定的,主要由三部分构成。例如以下图所看到的:
第一部分是内容块,全部的压缩文件都在这部分。每一个压缩文件都有一个local file header
,主要记录了文件名称、压缩算法、压缩前后的文件大小、改动时间、CRC32值等。
第二部分称为中央文件夹,包括了多个central directory file header
(和第一部分的local file header
一一相应),每一个中央文件夹文件头主要记录了压缩算法、凝视信息、相应local file header
的偏移量等。方便高速定位数据。
最后一部分是EOCD,主要记录了中央文件夹大小、偏移量和ZIP凝视信息等,其详细结构例如以下图所看到的:
依据之前的V1签名和校验机制可知。V1签名仅仅会检验第一部分的全部压缩文件,而不理会后两部分内容。
因此,仅仅要把渠道信息写入到后两块内容就能够通过V1校验,而EOCD的凝视字段无疑是最好的选择。
基于V1签名的多渠道打包方案
既然找到了突破口,那么基于V1签名的多渠道打包方案就应运而生:在APK文件的凝视字段,加入渠道信息。
整个方案包括下面几步:
- 复制APK
- 找到EOCD数据块
- 改动凝视长度
- 加入渠道信息
- 加入渠道信息长度
- 加入魔数
加入渠道信息后的EOCD数据块例如以下所看到的:
这里加入魔数的长处是方便从后向前读取数据,定位渠道信息。
因此,读取渠道信息包括下面几步:
- 定位到魔数
- 向前读两个字节。确定渠道信息的长度LEN
- 继续向前读LEN字节,就是渠道信息了。
通过16进制编辑器,能够查看到加入渠道信息后的APK(小端模式),例如以下所看到的:
6C 74 6C 6F 76 75 7A 68
是魔数,04 00
表示渠道信息长度为4。6C 65 6F 6E
就是渠道信息leon了。
0E 00
就是APK凝视长度了,正好是15。
虽说整个方案非常清晰,可是在找到EOCD数据块
这步遇到一个问题。
假设APK本身没有凝视。那最后22字节就是EOCD。可是若APK本身已经包括了凝视字段,那怎么确定EOCD的起始位置那?这里借鉴了系统V2签名确定EOCD位置的方案。
整个计算流程例如以下图所看到的:
整个方案介绍完了。该方案的最大长处就是:不须要解压缩APK。不须要又一次签名。仅仅须要复制APK。在凝视字段加入渠道信息。
每一个渠道包仅需几秒的耗时,非常适合渠道较多的APK。
可是好景不长,Android7.0之后新增了V2签名,该签名会校验整个APK的数据摘要。导致上述渠道打包方案失效。
所以假设想继续使用上述方案,须要关闭Gradle Plugin中的V2签名选项,禁用V2签名。
V2签名和多渠道打包方案
为什么须要V2签名
从前面的V1签名介绍,能够知道V1存在两个弊端:
1)MANIFEST.MF
中的数据摘要是基于原始未压缩文件计算的。因此在校验时,须要先解压出原始文件,才干进行校验。而解压操作无疑是耗时的。
2) V1签名仅仅校验APK第一部分中的文件,缺少对APK的完整性校验。因此,在签名后,我们还能够改动APK文件,比如:通过zipalign进行字节对齐后,仍然能够正常安装。
正是基于这两点,Google提出了V2签名,攻克了上述两个问题:
- V2签名是对APK本身进行数据摘要计算,不存在解压APK的操作,降低了校验时间。
- V2签名是针对整个APK进行校验(不包括签名块本身),因此对APK的不论什么改动(包括加入凝视、zipalign字节对齐)都无法通过V2签名的校验。
关于第一点的耗时问题。这里有一份实验室数据(Nexus 6P、Android 7.1.1)可供參考。
APK安装耗时对照 | 取5次平均耗时(秒) |
---|---|
V1签名APK | 11.64 |
V2签名APK | 4.42 |
可见,V2签名对APK的安装速度还是提升不少的。
V2签名机制
不同于V1,V2签名会生成一个签名块,插入到APK中。
因此。V2签名后的APK结构例如以下图所看到的:
APK签名块位于中央文件夹之前。文件数据之后。V2签名同一时候改动了EOCD中的中央文件夹的偏移量,使签名后的APK还符合ZIP结构。
APK签名块的详细结构例如以下图所看到的:
首先是8字节的签名块大小,此大小不包括该字段本身的8字节;其次就是ID-Value序列。就是一个4字节的ID和相应的数据;然后又是一个8字节的签名块大小。与開始的8字节是相等的;最后是16字节的签名块魔数。
当中,ID为0x7109871a相应的Value就是V2签名块数据。
V2签名块的生成可參考ApkSignerV2。总体结构和流程例如以下图所看到的:
首先。依据多个签名算法,计算出整个APK的数据摘要,组成左上角的APK数据摘要集。
接着,把最左側一列的数据摘要、数字证书和额外属性组装起来,形成相似于V1签名的“MF”文件(第二列第一行);
其次。再用同样的私钥,不同的签名算法,计算出“MF”文件的数字签名,形成相似于V1签名的“SF”文件(第二列第二行)。
然后,把第二列的相似MF文件
、相似SF文件
和开发人员公钥
一起组装成通过单个keystore签名后的v2签名块(第三列第一行)。
最后,把多个keystore签名后的签名块组装起来,就是完整的V2签名块了(Android中同意使用多个keystore对apk进行签名)。
上述流程比較繁琐。简而言之,单个keystore签名块主要由三部分组成,各自是上图中第二列的三个数据块:相似MF文件
、相似SF文件
和开发人员公钥
,其结构例如以下图所看到的:
除此之外,Google也优化了计算数据摘要的算法,使得能够并行计算。例如以下图所看到的:
数据摘要的计算包括下面几步:
首先,将上述APK中文件内容块、中央文件夹、EOCD依照1MB大小切割成一些小块。
然后,计算每一个小块的数据摘要。基础数据是0xa5 + 块字节长度 + 块内容。
最后,计算总体的数据摘要。基础数据是0x5a + 数据块的数量 + 每一个数据块的摘要内容。
这样,每一个数据块的数据摘要就能够并行计算。加快了V2签名和校验的速度。
V2校验流程
Android Gradle Plugin2.2之上默认会同一时候开启V1和V2签名,同一时候包括V1和V2签名的CERT.SF文件会有一个特殊的主属性。例如以下图所看到的:
该属性会强制APK走V2校验流程(7.0之上)。以充分利用V2签名的优势(速度快和更完好的校验机制)。
因此,同一时候包括V1和V2签名的APK的校验流程例如以下所看到的:
简而言之:优先校验V2,没有或者不认识V2,则校验V1。
这里引申出另外一个问题:APK签名时,仅仅有V2签名,没有V1签名行不行?
经过尝试,这样的情况是能够编译通过的。而且在Android 7.0之上也能够正确安装和执行。可是7.0之下。由于不认识V2,又没有V1签名。所以会报没有签名的错误。
OK,明白了Android平台对V1和V2签名的校验选择之后,我们来看下V2签名的详细校验流程(PackageManagerService.java
-> PackageParser.java
-> ApkSignatureSchemeV2Verifier.java
),例如以下图所看到的:
当中。最强签名算法是依据该算法使用的数据摘要算法来对照产生的,比方:SHA512 > SHA256。
校验成功的定义是至少找到一个keystore相应的签名块,而且全部签名块都依照上述流程校验成功。
下面我们来看下V2签名是怎么保证APK不被篡改的?
首先。假设破坏者改动了APK文件的不论什么部分(签名块本身除外)。那么APK的数据摘要就和“MF”数据块中记录的数据摘要不一致,导致校验失败。
其次。假设破坏者同一时候改动了“MF”数据块中的数据摘要。那么“MF”数据块的数字签名就和“SF”数据块中记录的数字签名不一致,导致校验失败。
然后,假设破坏者使用自己的私钥去加密生成“SF”数据块。那么使用开发人员的公钥去解密“SF”数据块中的数字签名就会失败;
最后。更进一步,若破坏者甚至替换了开发人员公钥,那么使用数字证书中的公钥校验签名块中的公钥就会失败,这也正是数字证书的作用。
综上所述,不论什么对APK的改动,在安装时都会失败,除非对APK又一次签名。可是同样包名,不同签名的APK也是不能同一时候安装的。
到这里,V2签名已经介绍完了。可是在最后一步“数据摘要校验”这里,隐藏了一个点,不知道有没有人发现?
由于。我们V2签名块中的数据摘要是针对APK的文件内容块、中央文件夹和EOCD三块内容计算的。可是在写入签名块后。改动了EOCD中的中央文件夹偏移量,那么在进行V2签名校验时,理论上在“数据摘要校验”这步应该会校验失败啊!可是为什么V2签名能够校验通过那?
这个问题非常重要,由于我们下面要介绍的基于V2签名的多渠道打包方案也会改动EOCD的中央文件夹偏移量。
事实上也非常easy,原来Android系统在校验APK的数据摘要时。首先会把EOCD的中央文件夹偏移量替换成签名块的偏移量。然后再计算数据摘要。而签名块的偏移量不就是v2签名之前的中央文件夹偏移量嘛!。。,因此,这样计算出的数据摘要就和“MF”数据块中的数据摘要全然一致了。详细代码逻辑,可參考ApkSignatureSchemeV2Verifier.java的416 ~ 420行。
基于V2签名的多渠道打包方案
在上节V2签名的校验流程中。有一个非常重要的细节:Android系统仅仅会关注ID为0x7109871a的V2签名块。而且忽略其它的ID-Value。同一时候V2签名仅仅会保护APK本身,不包括签名块。
因此,基于V2签名的多渠道打包方案就应运而生:在APK签名块中加入一个ID-Value,存储渠道信息。
整个方案包括下面几步:
- 找到APK的EOCD块
- 找到APK签名块
- 获取已有的ID-Value Pair
- 加入包括渠道信息的ID-Value
- 基于全部的ID-Value生成新的签名块
- 改动EOCD的中央文件夹的偏移量(上面已介绍过:改动EOCD的中央文件夹偏移量,不会导致数据摘要校验失败)
- 用新的签名块替代旧的签名块,生成带有渠道信息的APK
实际上,除了渠道信息,我们能够在APK签名块中加入不论什么辅助信息。
通过16进制编辑器。能够查看到加入渠道信息后的APK(小端模式),例如以下所看到的:
6C 65 6F 6E
就是我们的渠道信息leon
。向前4个字节:FF 55 11 88
就是我们加入的ID。再向前8个字节:08 00 00 00 00 00 00 00
就是我们的ID-Value的长度,正好是8。
整个方案介绍完了,该方案的最大长处就是:支持7.0之上新增的V2签名,同一时候兼有V1方案的全部长处。
多渠道包的强校验
那么怎样保证通过这些方案生成的渠道包,能够在全部Android平台上正确安装那?
原来Google提供了一个同一时候支持V1和V2签名和校验的工具:apksig。它包括一个apksigner命令行和一个apksig类库。当中前者就是Android SDK build-tools下面的命令行工具。而我们正是借助后面的apksig来进行渠道包强校验。它能够保证渠道包在apk Minsdk ~ 最高版本号之间都校验通过。详细代码可參考VerifyApk.java
多渠道打包工具对照
眼下市面上的多渠道打包工具主要有packer-ng-plugin和美团的Walle。下表是我们的ApkChannelPackage和它们之间的简单对照。
多渠道打包工具对照 | ApkChannelPackage | packer-ng-plugin | Walle |
---|---|---|---|
V1签名方案 | 支持 | 支持 | 不支持 |
V2签名方案 | 支持 | 不支持 | 支持 |
带有凝视的APK | 支持 | 不支持 | 不支持 |
依据已有APK生成渠道包 | 支持 | 不支持 | 不支持 |
命令行工具 | 不支持 | 支持 | 支持 |
强校验 | 支持 | 不支持 | 支持 |
这里我之所以同一时候支持V1和V2签名方案,主要是操心兴许Android平台加强签名校验机制,导致V2多渠道打包方案行不通,能够无痛切换到V1签名方案。兴许我也会尽快支持命令行工具。
ApkChannelPackage插件接入
详细的接入流程可參考APKChannelPackage插件接入文档。
相关推荐
Unity编译Android的原理解析和apk打包分析
【腾讯云的1001种玩法】安卓加固在腾讯云上的使用(附反编译结果)
Android动态库压缩壳的实现
此文已由作者授权腾讯云技术社区公布,转载请注明文章出处
原文链接:https://www.qcloud.com/community/article/146038
获取很多其它腾讯海量技术实践干货。欢迎大家前往腾讯云技术社区