打包环境:
- 操作系统:Centos7.0
- 编译脚本:Ant
- 涉及到的工具:shell命令、aapt、signapk(签名)、axmlio(解析库)、zipalign、apksigner(签名)
整理的背影以及说初衷
- 解决当前BUG:Android studio 从2.3升级到3.0后 现自动化打包后台无法对APK进行再签名
- 很久以前做过一次Ant的自动化,而目前自动化打包是接手项目,也恰是使用ant而非gradle,解决BUG同时,借此机会做一次总结
- 从年初开始,工作原因更多的接触Linux,对shell命令使用增多
- 开发了这么多年 但对打包签名只停留在模糊的认识
Ready 前置知识
-
Android 签名剖析
-
Ant 语法规则
-
什么是aapt
-
linux shell简单命令
一、Android 签名剖析
参考资料:http://zzqhost.com/2017/06/17/Android签名原理剖析/
-
消息摘要 -Message Digest
- 简称摘要,请看英文翻译,是摘要,不是签名,网上几乎所有APK签名分析的文章都混淆了这两个概念。
- 消息摘要的特性,很适合来验证数据的完整性,比如在网络传输过程中下载一个大文件BigFile,我们会同时从网络下载BigFile和BigFile.md5,BigFile.md5保存BigFile的摘要,我们在本地生成BigFile的消息摘要,和BigFile.md5比较,如果内容相同,则表示下载过程正确。
- 注意,消息摘要只能保证消息的完整性,并不能保证消息的不可篡改性。
-
MD5, SHA-0, SHA-1, SHA-256
- 这些都是摘要生成算法,和签名没有半毛钱关系。如果非要说他们和签名有关系,那就是签名是要借助于摘要技术。
- SHA-256是SHA-1的升级版,现在Android签名使用的默认算法都已经升级到SHA-256了
-
数字签名 - Signature
- 数字签名就是信息的发送者用自己的私钥对消息摘要加密产生一个字符串,加密算法确保别人无法伪造生成这段字符串,这段数字串也是对信息的发送者发送信息真实性的一个有效证明。
- 数字签名是 非对称密钥加密技术 + 数字摘要技术 的结合。
-
数字证书 - Certificate
- 数字证书是一个经 证书授权中心 数字签名 的包含公开密钥拥有者信息以及公开密钥的文件。CERT.RSA包含了一个数字签名以及一个数字证书。
-
签名三兄弟
- 从我们解压出来的APK目录里面可以看到有一个META-INF目录中
-
MANIFEST.MF:
所有文件的摘要信息,但不包括它们仨自己 -
CERT.SF:
保存的是MANIFEST.MF的摘要值,以及MANIFEST.MF中每一个摘要项的摘要值 -
CERT.RSA:
保存的是利用密钥对CERT.SF进行加密生成的数字签名和签名时用到的数字证书,数字证书保存的就是公钥和签名算法
-
- 从我们解压出来的APK目录里面可以看到有一个META-INF目录中
-
签名工具
- jarsigner 在JAVA的bin目录下
-verbose:签名命令标识符。
-keystore:后面跟着的是你签名使用的密钥文件(keystore)的绝对路径。
-storepass:后面跟着的是你密钥文件(keystore)的密码
-signedjar:此后有三个参数:
参数一:签名后生成的apk文件所要存放的路径。
参数二:未签名的apk文件的存放路径。
参数三:你的证书名称,通俗点说就是你keystore文件的别名,就是在你eclipse进行签名打包时的Alias的值。
例:jarsigner -verbose -keystore mystore.jks -storepass passwork -signedjar signed.apk unsigned.apk aliasname
- signapk
例:signapk 签名文件 密码 别名 别名密码 signed.apk unsigned.apk
- IDE(这就不用说了)
- jarsign和signapk区别:
很多文章都说 他们的区别是在于签名时使用的文件不一样,前者使用的是keystore文件,后者是用pk8,x509.pem文件,但我在使用当中发现 其实 都是可以使用keystore的,原打包ant脚本中就是使用的signapk 如下,很是不解,但也不重要。
最后经验证是因为signapk对as3.0打出的包进行再签名时 APK无法运行,改用apksigner,接下来 就开始介绍 apksigner。
<exec dir="${apk.obj.dir}" executable="${utils.dir}/signapk" >
<arg value="${keystore.uri}" />
<arg value="${keystore.password}" />
<arg value="${keystore.alias}" />
<arg value="${keystore.alias.password}" />
<arg value="${apk.obj.dir}/${apkname}-unsigned.apk" />
<arg value="${apk.obj.dir}/${apkname}-unaligned.apk" />
</exec>
- apksigner
介绍apksigner之前 先说一下 什么是V1与V2签名 这很关键。
从Android 7.0开始, 谷歌增加新签名方案 V2 Scheme (APK Signature);
但Android 7.0以下版本, 只能用旧签名方案 V1 scheme (JAR signing)
V1与V2的区别:
V1签名:
来自JDK(jarsigner), 对zip压缩包的每个文件进行验证, 签名后还能对压缩包修改(移动/重新压缩文件)
对V1签名的apk/jar解压,在META-INF存放签名文件(MANIFEST.MF, CERT.SF, CERT.RSA),
其中MANIFEST.MF文件保存所有文件的SHA1指纹(除了META-INF文件), 由此可知: V1签名是对压缩包中单个文件签名验证
V2签名:
来自Google(apksigner), 对zip压缩包的整个文件验证, 签名后不能修改压缩包(包括zipalign),
对V2签名的apk解压,没有发现签名文件,重新压缩后V2签名就失效, 由此可知: V2签名是对整个APK签名验证
V2签名优点很明显:
签名更安全(不能修改压缩包)
签名验证时间更短(不需要解压验证),因而安装速度加快
注意: apksigner工具默认同时使用V1和V2签名,以兼容Android 7.0以下版本
由此可见 我们需要使用打包后台再打包 是一定不能使用V2签名的,关于这个官方也必未强制要求使用V2 给出了一定的配置方法 :
-
在IDE中可以进行选择
在gradel中可以配置:
signingConfigs {
debug {
v1SigningEnabled true
v2SigningEnabled true
}
release {
v1SigningEnabled true
v2SigningEnabled true
}
}
- 在使用apksigner时也给出了参数设置
--v1-signing-enabled <true | false>
--v2-signing-enabled <true | false>
说完了V1与V2 我们回头来看apksigner的使用,
工具目录在SDK\build-tools\版本号\lib下,执行
java -jar apksigner.jar sign
参数说明:
- –ks 你的jks路径 //jks签名证书路径
- –ks-key-alias 你的alias //生成jks时指定的alias
- –ks-pass pass:你的密码 //KeyStore密码
- –key-pass pass:你的密码 //签署者的密码,即生成jks时指定alias对应的密码
- –out output.apk //输出路径
- input.apk //被签名的apk
示例:
java -jar apksigner.jar sign --ks key.jks --ks-key-alias releasekey --ks-pass pass:pp123456 --key-pass pass:pp123456 --out output.apk input.apk
段落总结:以上是关于签名的资料整合,也是自动化打包的核心,当前自动化打包工具无法对AS3.0再打包的关键也在于此,关键就在于signapk过时了,而AS3.0的升级又改变很大,想要自动化,就需要将signapk也升级,他的取代方案就是使用apksigner。当中我也怀疑过是不是V2导致的问题,后经验证,与它无关,使用AS3.0强制使用V1打包 也一样不可用,或许打包通过,但在安装时报如下错误:
Failure [INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION: Failed reading AndroidManifest.xml in android.util.jar.StrictJarFile@6fa28f: META-INF/MANIFEST.MF has invalid digest for AndroidManifest.xml in AndroidManifest.xml]
说到这里思路就清晰了,接下来的工作就是将signapk替换为apksigner,并结合使用在ANT当中。
二、Ant 语法规则:
此项目从2015年以来 一直使用Ant,之前也有做过,而这次问题的主因并不在ant是否过时,as3.0是否一定要用最新的gradle的问题,所以继续在ANT基础上修改,以前并未做过总结,就又到处查了一遍资料,太麻烦,所以就着这次机会做个最终了结。
参考资料:
https://blog.csdn.net/leeandmins/article/details/50481450
https://blog.csdn.net/megatronkings/article/details/48012125
http://tianlihu.iteye.com/blog/741239
- 安装和配置
1、下载ant,[http://mirror.esocc.com/apache//ant/binaries/apache-ant-1.9.1-bin.zip]
2、配置ANT_HOME环境变量 指向下载包中的bin目录
3、运行 ant 命令 (window下应该是运行ant.bat) 得到如下结果,代表安装成功。
从结果可以看出 他需要用到build.xml,通俗的讲 build.xml就ant的核心代码文件,build.xml是默认的ant执行文件
ant
在当前目录下的build.xml运行Ant,执行缺省的target。
ant -buildfile build-test.xml
在当前目录下的build-test.xml运行Ant,执行缺省的target。
ant -buildfile build-test.xml clean
在当前目录下的build-test.xml运行Ant,执行一个叫做clean的target。
ant -buildfile build-test.xml -Dbuild=build/classes clean
在当前目录下的build-test.xml运行Ant,执行一个叫做clean的target,并设定build属性的值为build/classes。
- build.xml文件的基本结构
<?xml version="1.0" encoding="UTF-8"?>
<project name="test" default="build">
<property name="file.dir" value="D://"/>
<property file="local.properties" />
<loadproperties srcFile="project.properties" />
<import file="rules.xml" optional="true" />
<target name="build">
<echo>runing...</echo>
</target>
<target name="debug" depends="build">
<echo level="info">${file.dir} debugging...</echo>
</target>
</project>
project 根标签。
name属性表示项目名称,没什么作用;default属性表示默认执行命令,cmd命令行中使用ant和ant default属性值(本例是ant build) 两种方式等效。
property 定义类标签。
可以定义一些常量值,需要注意:定义后理论不能再修改(其实可以通过第三方库修改)。比如第3行定义了一个file.dir的变量,值为”D://“,引用时使用 ${file.dir}调用。第4行,是引入一个properties文件(里面定义了很多property),相当于导包。
loadproperties 引用标签。
功能和第4行<property file=""/>等同,表示引入一个properties定义集群。好处是便于封装和管理。
import 引入标签。
和loadproperties不同的是,import是引入另一个构建文件,包括变量和执行命令。
target 执行标签。
可以在cmd命令行中直接ant + target执行,比如以上脚本可以执行: ant build 和 ant debug。target标签中有个depends属性,表示执行命令依赖。如果要执行debug命令,会自动先执行depends里面的命令。以上脚本执行 ant debug,实际是执行了 ant build 和 ant debug
echo 日志标签。
表示日志输出,能在cmd命令中打印显示,level属性表示:日志级别。 比较特殊的是echo中可以引用变量,用法同变量调用方式${name}。
- ant的常用语法
1、文件语句
文件操作是ant中最常用的基本操作,包括创建、复制、删除、遍历等。由于ant涉及最多的就是文件操作,所以它的api相对来说非常丰富,让我们来逐一介绍和学习。
创建:mkdir标签。 传入一个文件路径,直接创建出一个文件目录。然而不知为何ant没有提供创建文件的功能。
<mkdir dir="D:/test"/>
删除:delete标签。删除文件或文件夹
<delete file="D:/test/example.txt"/>
<delete dir="D:/test"/>
移动:move标签。包括文件重命名、文件移动、文件目录移动。
<!-- 重命名 -->
<move file="D:/test/example1.txt" tofile="D:/test/example2.txt"/>
<!-- 移动文件至新目录,新目录会自动创建 -->
<move file="D:/test/example2.txt" todir="D:/test2"/>
<!-- 文件夹移动 -->
<move dir="D:/test/example2.txt" todir="D:/test2"/>
复制:copy标签。文件复制。
<!-- 文件复制,指定新文件名 -->
<copy file="D:/test/example.txt" tofile="D:/test/example2.txt"/>
<!-- 文件复制,指定新文件目录 -->
<copy file="D:/test/example.txt" todir="D:/test/new/"/>
<!-- 文件夹复制,指定新文件夹 -->
<copy dir="D:/test/" todir="D:/test/new/"/>
2、条件语句
condition标签,配合istrue或者isfalse使用。
<condition property="check">
<istrue value="false" />
</condition>
<target name="build" if="check">
<echo>build running...</echo>
</target>
在执行名为build的target任务时,由于target中含有if的标签,所以需要判断名为check的条件语句的值,但是istrue=false的语句表示条件不符合,echo并不会执行。如果改成istrue=true,echo将执行。当然以上语句等价于:
<condition property="check">
<isfalse value="true" />
</condition>
<target name="build" if="check">
<echo>build running...</echo>
</target>
需要注意下,istrue和isfalse两种标签不能同时存在。
除了直接使用istrue指定条件语句的值,还能动态地使用equals比较变量,比如:
<property name="id" value="99"></property>
<condition property="check">
<equals arg1="${id}" arg2="100"/>
</condition>
<target name="build" if="check">
<echo>build running...</echo>
</target>
3、循环语句
ant本身并没有提供循环语句,但是我们可以借助于ant-contrib.jar使用循环语句,举个简单的例子:
<property name="ant-contrib" value="E:\\Android\\android-sdk\\tools\\lib\\ant-contrib-1.0b3.jar"></property>
<taskdef name="foreach" classname="net.sf.antcontrib.logic.ForEach" classpath="${ant-contrib}"/>
<target name="build">
<foreach list="1,2,3,4,5,6,7,8,9" param="number" delimiter="," target="log"/>
</target>
<target name="log">
<echo>foreach running: ${number}</echo>
</target>
4、自定义语句
ant的魅力所在之处就是强大的自定义语句,比如上面的foreach语句。ant官方库只定义了一些简单的语句,但是在实际项目中远远不足以满足我们的需要,比如新建一个文件。这里我们就用自定义语句来实现下。
ant的原理是每个语句标签映射一个java类文件,每个标签里的属性则映射java类的变量,有点类似spring中xml映射javabean。每个ant标签映射的java类文件不是随意编写的,有一定的规范。
在ant安装目录下的lib文件目录中有个名为ant.jar的包,这个就是ant的规范标准库,自定义语句Java类都需要依赖它来编译,同时每个语句必须继承其中名为Task.java的基类,复写execute方法执行自定义操作。
package com.ant.test;
import java.io.File;
import java.io.IOException;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Task;
public class FileCreater extends Task{
private String fileName;
public void setName(String fileName){
this.fileName = fileName;
}
@Override
public void execute() throws BuildException {
try {
new File(fileName).createNewFile();
} catch (IOException e) {
log("create file '" + fileName + "' failed!");
}
log("create file '" + fileName + "' successful!");
super.execute();
}
}
上面定义了创建文件的自定位标签,把这个java文件打成jar包,然后就可以在build.xml使用了,xml内容如下:
<property name="fileJar" value="D:/file.jar"></property>
<taskdef name="filecreater" classname="com.ant.test.FileCreater" classpath="${fileJar}"/>
<target name="build">
<filecreater name="D:/test.txt"/>
</target>
以上执行的操作是创建一个路径为D:/test.txt的文件。filecreater是映射FileCreater.java的自定义标签,name属性传入文件路径名,会自动 反射调用FileCreater.java中的setName方法注入参数值。在FileCreater.java中有个log打印输出方法,可以在cmd中输出,极大方便我们的调试。
最后 如下代码是替换signapk为apksigner的主要代码
<!-- <exec dir="${apk.obj.dir}" executable="${utils.dir}/signapk" >
<arg value="${keystore.uri}" />
<arg value="${keystore.password}" />
<arg value="${keystore.alias}" />
<arg value="${keystore.alias.password}" />
<arg value="${apk.obj.dir}/${apkname}-unsigned.apk" />
<arg value="${apk.obj.dir}/${apkname}-unaligned.apk" />
</exec> -->
<exec executable="java">
<arg line="-jar ${utils.dir}/lib/apksigner.jar sign --ks ${keystore.uri} --ks-key-alias ${keystore.alias} --ks-pass pass:${keystore.password} --key-pass pass:${keystore.password} --v2-signing-enabled false --out ${apk.obj.dir}/${apkname}-unaligned.apk ${apk.obj.dir}/${apkname}-unsigned.apk"/>
</exec>
总结 以上两部分是这次任务的主要内容,除了涉及这两块知识外 再就有些零散的appt知识点和linux shell命令,我做一下简单整理
三、什么是aapt
参考资料:
https://blog.csdn.net/electricity/article/details/6540247
项目用到的不多,而这块的知识点内容比较多 限于篇幅过长 在此就简单列出 此次用到的一些点
打包好的apk中移除文件
aapt r[emove] [-v] file.{zip,jar,apk} file1 [file2 ...]
添加文件到打包好的apk中
aapt a[dd] [-v] file.{zip,jar,apk} file1 [file2 ...]
实例:
<target name="build-apk">
<sequential>
<copy file="${apk.obj.dir}/${apkfile}" tofile="${apk.obj.dir}/${apkname}-unsigned.apk" overwrite="true" />
<echo level="info">remove AndroidManifest</echo>
<exec dir="${apk.obj.dir}" executable="${utils.dir}/aapt">
<arg value="r" />
<arg value="${apk.obj.dir}/${apkname}-unsigned.apk" />
<arg value="AndroidManifest.xml" />
</exec>
<exec dir="${apk.obj.dir}" executable="${utils.dir}/aapt">
<arg value="a" />
<arg value="${apk.obj.dir}/${apkname}-unsigned.apk" />
<arg value="AndroidManifest.xml" />
</exec>
</sequential>
</target>
四、linux shell简单命令
参考资料:https://www.cnblogs.com/yinheyi/p/6648242.html
LOCAL_PATH=`dirname $0`
cd $LOCAL_PATH
if [ "$1" == "" ]; then
utils/ant/bin/ant help
else
echo $@
utils/ant/bin/ant $@
fi
总结完回头一看 篇幅真的挺长,呵呵呵 没办法 项目总结 内容是多一点,对于markdown的排版也不是很熟练,整篇来看可能有些臃肿,结构不清后继再优化吧,熟能生巧,写文章总结虽说也是门技术活,但也是熟练工种,总结是越发的重要,会越来越好的。