关于做者:
李涛,腾讯Android工程师,14年加入腾讯SNG增值产品部,期间主要负责手Q动漫、企鹅电竞等项目的功能开发和技术优化。业务时间喜欢折腾新技术,写一些技术文章,我的技术博客:www.ltlovezh.com 。java
ApkChannelPackage是一种快速多渠道打包工具,同时支持基于V1和V2签名进行渠道打包。插件自己会自动检测Apk使用的签名方法,并选择合适的多渠道打包方式,对使用者来讲彻底透明。android
Github地址:
https://github.com/ltlovezh/ApkChannelPackagegit
众所周知,由于国内Android应用分发市场的现状,咱们在发布APP时,通常须要生成多个渠道包,上传到不一样的应用市场。这些渠道包须要包含不一样的渠道信息,在APP和后台交互或者数据上报时,会带上各自的渠道信息。这样,咱们就能统计到每一个分发市场的下载数、用户数等关键数据。github
既然咱们须要进行多渠道打包,那咱们就看下最多见的多渠道打包方案。算法
Gradle Plugin自己提供了多渠道的打包策略:安全
首先,在AndroidManifest.xml中添加渠道信息占位符:微信
<meta-data android:name="InstallChannel" android:value="${InstallChannel}" />
而后,经过Gradle Plugin提供的productFlavors
标签,添加渠道信息:app
productFlavors{ "YingYongBao"{ manifestPlaceholders = [InstallChannel : "YingYongBao"] } "360"{ manifestPlaceholders = [InstallChannel : "360"] } }
这样,Gradle编译生成多渠道包时,会用不一样的渠道信息替换AndroidManifest.xml中的占位符。咱们在代码中,也就能够直接读取AndroidManifest.xml中的渠道信息了。编辑器
可是,这种方式存在一些缺点:工具
每生成一个渠道包,都要从新执行一遍构建流程,效率过低,只适用于渠道较少的场景。
Gradle会为每一个渠道包生成一个不一样的BuildConfig.java类,记录渠道信息,致使每一个渠道包的DEX的CRC值都不一样。通常状况下,这是没有影响的。可是若是你使用了微信的Tinker热补丁方案,那么就须要为不一样的渠道包打不一样的补丁,这彻底是不能够接受的。(由于Tinker是经过对比基础包APK和新包APK生成差分补丁,而后再把补丁和基础包APK一块儿合成新包APK。这就要求用于生成差分补丁的基础包DEX和用于合成新包的基础包DEX是彻底一致的,即:每个基础渠道包的DEX文件是彻底一致的,否则就会合成失败)
ApkTool是一个逆向分析工具,能够把APK解开,添加代码后,从新打包成APK。所以,基于ApkTool的多渠道打包方案分为如下几步:
通过测试,这种方案彻底是可行的。
优势:
不须要从新构建新渠道包,仅须要复制修改就能够了。而且由于是从新签名,因此同时支持V1和V2签名。
缺点:
那有没有一种方案,能够在添加渠道信息后,不须要从新签名那?
首先咱们要了解一下APK的签名和校验机制。
在进一步学习V1和V2签名以前,咱们有必要学习一下签名相关的基础知识。
数据摘要算法是一种能产生特定输出格式的算法,其原理是根据必定的运算规则对原始数据进行某种形式的信息提取,被提取出的信息就是原始数据的消息摘要,也称为数据指纹。
通常状况下,数据摘要算法具备如下特色:
不管输入数据有多大(长),计算出来的数据摘要的长度老是固定的。例如:MD5算法计算出的数据摘要有128Bit。
通常状况下(不考虑碰撞的状况下),只要原始数据不一样,那么其对应的数据摘要就不会相同。同时,只要原始数据有任何改动,那么其数据摘要也会彻底不一样。即:相同的原始数据必有相同的数据摘要,不一样的原始数据,其数据摘要也必然不一样。
不可逆性,即只能正向提取原始数据的数据摘要,而没法从数据摘要中恢复出原始数据。
著名的摘要算法有RSA公司的MD5算法和SHA系列算法。
数字签名和数字证书是成对出现的,二者不可分离(数字签名主要用来校验数据的完整性,数字证书主要用来确保公钥的安全发放)。
要明白数字签名的概念,必需要了解数据的加密、传输和校验流程。通常状况下,要实现数据的可靠通讯,须要解决如下两个问题:
而数字签名,就是为了解决这两个问题而诞生的。
首先,数据的发送者须要先申请一对公私钥对,并将公钥交给数据接收者。
而后,若数据发送者须要发送数据给接收者,则首先要根据原始数据,生成一份数字签名,而后把原始数据和数字签名一块儿发送给接收者。
数字签名由如下两步计算得来:
这样,数据接收者拿到的消息就包含了两块内容:
接下来,接收者就会经过如下几步,校验数据的真实性:
由于私钥只有发送者才有,因此其余人没法伪造数字签名。这样经过数字签名就确保了数据的可靠传输。
综上所述,数字签名就是只有发送者才能产生的别人没法伪造的一段数字串,这段数字串同时也是对发送者发送数据真实性的一个有效证实。
想法虽好,可是上面的整个流程,有一个前提,就是数据接收者可以正确拿到发送者的公钥。若是接收者拿到的公钥被篡改了,那么坏人就会被当成好人,而真正的数据发送者发送的数据则会被视做脏数据。那怎么才能保证公钥的安全性那?这就要靠数字证书来解决了。
数字证书是由有公信力的证书中心(CA)颁发给申请者的证书,主要包含了:证书的发布机构、证书的有效期、申请者的公钥、申请者信息、数字签名使用的算法,以及证书内容的数字签名。
可见,数字证书也用到了数字签名技术。只不过签名的内容是数据发送方的公钥,以及一些其它证书信息。
这样数据发送者发送的消息就包含了三部份内容:
接收者拿到数据后,首先会根据CA的公钥,解码出发送者的公钥。而后就与上面的校验流程彻底相同了。
因此,数字证书主要解决了公钥的安全发放问题。
所以,包含数字证书的整个签名和校验流程以下图所示:
默认状况下,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三个文件。
在安装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也是不能同时安装的。
由上述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签名的多渠道打包方案就应运而生:在APK文件的注释字段,添加渠道信息。
整个方案包括如下几步:
这里添加魔数的好处是方便从后向前读取数据,定位渠道信息。
所以,读取渠道信息包括如下几步:
经过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签名。
从前面的V1签名介绍,能够知道V1存在两个弊端:
MANIFEST.MF
中的数据摘要是基于原始未压缩文件计算的。所以在校验时,须要先解压出原始文件,才能进行校验。而解压操做无疑是耗时的。
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的安装速度仍是提高很多的。
不一样于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也优化了计算数据摘要的算法,使得能够并行计算,以下图所示:
数据摘要的计算包括如下几步:
这样,每一个数据块的数据摘要就能够并行计算,加快了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的中央目录偏移量。
其实也很简单,原来Android系统在校验APK的数据摘要时,首先会把EOCD的中央目录偏移量替换成签名块的偏移量,而后再计算数据摘要。而签名块的偏移量不就是v2签名以前的中央目录偏移量嘛!!!,所以,这样计算出的数据摘要就和“MF”数据块中的数据摘要彻底一致了。具体代码逻辑,可参考ApkSignatureSchemeV2Verifier.java的416 ~ 420行。
在上节V2签名的校验流程中,有一个很重要的细节:Android系统只会关注ID为0x7109871a的V2签名块,而且忽略其余的ID-Value,同时V2签名只会保护APK自己,不包含签名块。
所以,基于V2签名的多渠道打包方案就应运而生:在APK签名块中添加一个ID-Value,存储渠道信息。
整个方案包括如下几步:
实际上,除了渠道信息,咱们能够在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和它们之间的简单对比。
这里我之因此同时支持V1和V2签名方案,主要是担忧后续Android平台增强签名校验机制,致使V2多渠道打包方案行不通,能够无痛切换到V1签名方案。后续我也会尽快支持命令行工具。
目前Gradle Plugin 2.2以上默认开启V2签名,因此若是想关闭V2签名,可将下面的v2SigningEnabled
设置为false。
signingConfigs { release { ... v1SigningEnabled true v2SigningEnabled false } debug { ... v1SigningEnabled true v2SigningEnabled false } }
1.在根工程的build.gradle
中,添加对打包Plugin的依赖:
dependencies { classpath 'com.android.tools.build:gradle:2.2.0' classpath 'com.leon.channel:plugin:1.0.1'}
2.在主App工程的build.gradle
中,添加对ApkChannelPackage Plugin的引用:
apply plugin: 'channel'
3.在主App工程的build.gradle
中,添加读取渠道信息的helper类库依赖:
dependencies { compile 'com.leon.channel:helper:1.0.1'}
4.在gradle.properties文件中,配置渠道文件名称
channel_file=channel.txt
其中channel.txt即为包含渠道信息的文件,需放置在根工程目录下,一行一个渠道信息。
5.渠道包信息配置
如果直接编译生成多渠道包,则经过channel标签配置:
channel{ //多渠道包的输出目录,默认为new File(project.buildDir,"channel") baseOutputDir = new File(project.buildDir,"xxx") //多渠道包的命名规则,默认为:${appName}-${versionName}-${versionCode}-${flavorName}-${buildType} apkNameFormat ='${appName}-${versionName}-${versionCode}-${flavorName}-${buildType}'}
其中,多渠道包的命名规则中,可以使用如下字段:
如果根据已有基础包生成多渠道包,则经过rebuildChannel
标签配置:
rebuildChannel { baseDebugApk = 已有Debug APK baseReleaseApk = 已有Release APK//默认为new File(project.buildDir, "rebuildChannel/debug")debugOutputDir = Debug渠道包输出目录 //默认为new File(project.buildDir, "rebuildChannel/release")releaseOutputDir = Release渠道包输出目录 }
这里要注意一下,已有APK的名字必须包含base
字符串,这样插件生成多渠道包时,会用当前的渠道替换base
字符串,造成新的渠道包。
6.生成多渠道包
若没有经过Gradle Plugin的 productFlavors
配置多渠道,那么经过如下Task
channelDebug
、channelRelease
分别负责生成Debug和Release的多渠道包。
如果配置了productFlavors
,那么对应的Task则是channelFlavorXDebug
、channelFlavorXRelease
,FlavorX表示在productFlavors
中配置的渠道名称。
除此以外,若是是根据已有基础包生成多渠道包,那么对应的Task则是reBuildChannel
。
7.读取渠道信息
经过helper类库中的ChannelReaderUtil
类读取渠道信息。
String channel = ChannelReaderUtil.getChannel(getApplicationContext());
更多精彩内容欢迎关注腾讯 Bugly的微信公众帐号:
腾讯 Bugly是一款专为移动开发者打造的质量监控工具,帮助开发者快速,便捷的定位线上应用崩溃的状况以及解决方案。智能合并功能帮助开发同窗把天天上报的数千条 Crash 根据根因合并分类,每日日报会列出影响用户数最多的崩溃,精准定位功能帮助开发同窗定位到出问题的代码行,实时上报能够在发布后快速的了解应用的质量状况,适配最新的 iOS, Android 官方操做系统,鹅厂的工程师都在使用,快来加入咱们吧!