应用程序的编译和打包
文章是在word上写好后,复制到csdn的,csdn不支持live writer,每次编辑都很伤脑筋,最终的效果也很差。有知道方法的朋友告知一下,感谢。
同步发布在http://www.cnblogs.com/lxs-android/archive/2013/01/15/2861822.html
Android应用程序的编译和打包
Android中应用程序的编译可以如下几种方式:
- 借助于系统编译
我们在本书的基础篇中对Android系统的编译框架进行过分析。它利用Android.mk文件将众多小项目组织起来,并且提供了非常方便的函数来编译出各种可执行文件,库,和应用程序等等。虽然我们完全可以借助于系统编译来完成应用程序的编译,不过这种方式并不多见。一方面,这需要开发工程师对整个系统的编译框架有一定的认识,另一方面,还需要下载整个源码项目,每次编译的时间也会非常长。
- 借助于IDE工具
所以一般的纯应用程序(非系统级应用)开发,都会借助于IDE工具,比如Eclipse就是使用最广泛的一种。Android为Eclipse提供了ADT组件,从而让用户可以像操作Visual Studio一样开发Android应用程序。
- 命令行编译
工程师在ADT的帮助下,几乎不用做多余的操作就可以完成编译。不过,这样造成的一个副作用是很多人对于应用程序的编译、打包、签名等等基础过程都完全不清楚。因而这章的内容我们将讲解隐藏在"ADT"背后的这些细节。
Ant
完成一个软件项目编译需要哪些工具?编译器是毋庸置疑的,比如GCC,而且理论上这就足够了(在没有其它资源需要处理的情况下)。不过,随着工程源码的不断膨胀,单纯的使用GCC显然无法满足要求——Android工程有成千上万个文件,不可能手工对这些源码都执行GCC命令吧?所以必须有强大的工具来管理这些零碎的文件,由小而大地组织成最终的系统img,这就是make的意义所在。
那么从命令行编译一个Apk,是不是也用make?
可以这样子做,但事实上Google却并没有选择这种方式,而是采用了另外一个工具——Ant。
Ant 是"Another Neat Tool"的缩写,由Apache开发。从"Another"可以看出,它是基于某个其它工具而开发的,而且多半是针对这个原有工具的缺点来做改进的(Neat)。事实上也确是如此,Ant的开发者原先供职于Sun公司,在开发著名的JSP/Servlet(即后来的Tomcat)时,发现传统的make方法需要依赖于操作系统环境,对他的工作造成了不少的影响。
因此Ant采用Java语言开发,并且以XML文件(默认为build.xml)来描述编译过程和依赖关系。这相对于Makefile来说更简洁易懂,也更有扩展性,所以在Java工程中得到了广泛应用。
下面列出Ant的几个常见命令, 其它更多用法我们这里不做详细解释,有兴趣的读者可以参见它的官方网站:
ant release
编译一个release版本的项目
ant debug
编译一个debug版本的项目
ant installd
安装一个已经compiled过的debug包
ant installr
安装一个已经compiled过的release包
ant installt
安装一个已经compiled过的测试包,同时安装被测试应用的.apk文件
ant <build_target> install
编译并安装一个程序包
ant clean
清理一个项目, 或者如果你使用了ant all clean, 则所有相关项目都会被清理。
特别提醒:如果你是在Windows下开发Apk应用程序,那么要注意JDK的安装路径。因为默认情况下,JDK安装在"Program Files"目录,这中间的空格将使Ant无法正常运行。解决的办法有两个:
- set JAVA_HOME= "c:\Progra~1\Java\<jdkdir> "
- 或者将JDK安装到名称不带空格的路径中
通过命令行编译和打包APK
总结而言,ant可以提供两种方式的编译,即debug和release。无论何种方式生成的应用程序,都需要经过签名和zipalign的优化,只不过debug的方式默认就会帮开发者完成这些工作。关于签名过程的一些描述,请参阅下一小节。
Debug模式
编译debug版本的项目,一般步骤如下:
- 命令行模式下,进入你的工程目录
- 使用ant debug命令进行编译
这样就会在项目的bin目录下生成一个后缀为-debug.apk的文件,而且它已经用debug key签过名,也经过了zipalign的优化。
Release模式
虽然上面的debug模式非常方便,但并不适用于将要发布出去的应用程序。因为它采用的是系统默认的签名文件,没有起到很好的安全保护作用。
在release模式下,签名和zipalign默认情况下都需要开发者手动完成。一般步骤如下:
- 命令行模式下,进入你的工程目录
- 使用ant release命令进行编译
- 在bin目录下会生成以-unsigned.apk为后缀的apk文件
- 利用Jarsigner或者其它类似工具为apk签名(用私钥签名)
- 利用zipalign优化应用程序
可能有读者会认为这个过程比较繁琐,有一个简化的方法可以在release模式下自动为apk签名和优化。
- 找到项目根目录下的ant.properties文件
- 加入如下两条信息:
这样ant release命令在生成apk的过程中会询问密码,编译完成后的应用程序就已经用你提供的my.keystore签名了。
编译后生成的Apk还需要安装到模拟器或设备上以供用户使用。在Eclipse上,我们只要点击"Run->Run/Debug"就可以将程序安装到目标上(目标可以是模拟器或设备,具体选择哪个设备一方面取决于当前连接的实际情况,另一方面还与Run/Debug Configurations里Target页中的设置有关系),实际上这一过程是借助于adb的install功能,因而命令行模式下,我们还是可以使用adb install来达到安装要求。关于这一过程,可以参见本书的工具篇对adb的详细描述,这里不再赘述。
APK编译过程
上面我们对Ant的两种编译模式进行了概述,从使用的角度出发向读者介绍了命令行模式下的编译过程。接下来,我们进一步分析编译的实际过程,即Android是如何将项目源码一步步编译打包成最终的.apk文件。
以apk为后缀的文件是Android应用的标准格式,它其实是一个zip压缩包,所以可以用WinRar等工具将其解压出来。可以看到,一个典型的apk应用包含以下几部分内容:
- AndroidManifest.xml
这个文件相信大家都不会陌生,如果应用程序是一本书,那么这个文件就是它的"封面"和"目录",记载了应用程序的名称,权限声明,所包含的组件等等一系列信息。不过从普通apk解压出来的AndroidManfiest是无法直接打开的,因为它发布时已经经过了保护处理
- classes.dex
Apk应用程序的核心。它是由项目源码生成的.class文件,经进一步转化而成的Android系统可识别的Dalvik Byte Code
- resources.arsc
编译过后的资源文件
- res目录
未编译的资源文件
- META-INF目录
保存应用程序的签名和校验信息,以保证程序的完整性。当生成apk包时,需要对内容做一次校验,并将结果保存在这里。而设备在安装这一应用时,会对内容再做一次校验,并和先前的值进行比较,以验证程序包是否已经被恶意篡改
下图详细描述了整个编译过程:
图 131 Apk的编译全过程图解
可以清楚地看到,整个编译过程涉及了多种工具,我们下面对其中的几个重要步骤进行讲解。
- 首先.aidl(Android Interface Description Language)文件需要通过aidl工具转换成编译器能处理的Java接口文件
- 同时资源文件将被aapt (Asset Packaging Tool)处理为最终的resources.arsc,并生成R.java文件以使源码可以方便地访问到这些资源
- Java的编译器将R.java, Java源码以及上述生成的接口文件统一编译成.class文件
- 不过.class并不是Android系统所能识别的格式,因而还要利用dex工具转化为Dalvik字节码。这其中还会加入所有需要的第三方库等文件
- 接下来系统将上面生成的dex,资源包,以及其它资源通过apkbuilder生成初始的apk文件包。这时还没有签名和优化
- 签名可以用Jarsigner,也可以用其它类似的工具。如果是在Debug模式下,所签名所用的keystore就是系统默认自带的,否则开发者需要提供自己的私钥以完成签名过程
- 最后一步,将上述签名后的apk通过zipalign进行优化,以提高加载和运行速度。大概原理是通过对其中包含的相关数据进行边界对齐,来加快读取和处理。这也同时解释了其名称"zip"+"align"的由来
上面编译过程所涉及到的一部分重要工具的使用方法,可以参看本书工具篇中的讲解。至此,我们已经熟悉了整个Apk编译的流程,接下来的一个小节,将重要分析下应用程序的签名。
信息安全基础概述
在讲解Android应用程序的签名前,我们有必要补充下信息安全与密码学 (Information security and cryptography) 的一些基础知识,这样大家对后面的学习就能轻车熟路了。
相信读者在生活中多多少少都已经接触过密码学的知识,比如我们在浏览网页,特别是一些银行官方网站时,经常会看到"http"协议已经悄然转成了"https"。还有就是个人电子签名,它的权威性已经得到了广泛的认可,并慢慢取代了传统的签名方式具有法律效应了。这些技术的迅速发展,都得益于密码和安全学的不断突破和创新。
安全是一个抽象的概念,换句话说,什么样的情况下才叫"安全"?我们先来看下对Cryptography的一个经典解释,引用自《Handbook of Applied Cryptography》一书。
Cryptography is the study of mathematical techniques related to aspects of information security such as confidentiality, data integrity, entity authentication, and data origin authentication.
从中可以看出密码与安全学的几个基础目标是:
- Confidentiality
- Data Integrity
- Authentication
- Non-repudiation
这样子说读者可能会觉得枯燥而又抽象难懂,下面就举个例子来帮助大家理解。假设有三个人,分别是小白(WHITE),小红(RED),和小黑(BLACK)。从名称不难看出我们给它们赋予的角色职责,小白和小红是"善良"的通讯双方,而小黑则是"坏人"(Bad Guy, Adversary),如下图所示:
图 132信息安全中的典型场景
我们的目的就是保证小白和小红的正常对话顺利而安全地进行。按照场景的开展顺序,大家可以来推测下将会发生哪些安全隐患。
Authentication
假设是小白发起的通话请求,那么,首先的一个问题就是,小白怎么知道和它建立连接的是小红,或者反过来说,小红又如何确定对方是小白呢?
这是安全学中一个非常重要的研究课题,即Authentication。如果场景中的小红不是人类,而是银行服务器,那么可以想象一下如果小黑冒充是Bank Server而和小白建立连接,后果将是非常严重的。小黑可以模仿银行的登陆界面来轻松骗取小白的账户密码,然后实施各种侵害小白权益的行为。
因而在通信双方建立连接时,必须做相应的身份认证,保证两端的会话者都是合法的(日常生活中客户登陆银行的网上银行,大多数情况下只是银行服务器方提供了身份认证)。
Confidentiality
现在小白和小红已经建立连接并且可以正常通信了。那么这样就高枕无忧了吗?显然不是。小黑能做的破坏还很多,比如它可以在小白家的网络线上剪开,安装上监听器以截取双方来往的信息。这时小白和小红的通信实际上就是下图所示的情况:
图 133监听通信双方的往来信息
那么,如果小白和小红在交流中泄露了一定的机密信息,比如银行卡号,密码等等,那么小黑同样可以达到非法目的。解决的办法就是将通信双方的内容进行加密处理,即Confidentiality所要解决的问题。
Data Integrity
到此,小白和小红的安全性又得到了进一步的保障,它们现在已经建立连接,并且通信的数据也已经得到加密,保证了小黑无法破解其中的内容了。不过这并不代表小黑已经无技可施了。虽然小黑没有办法破解监听到的内容,但它仍然可以篡改这些信息。如下图所示:
图 134篡改通信双方的信息
注意,这个图只是示意BLACK可以对通信内容进行篡改。并不是它真的可以获悉通信内容中有"APPLE"或者"OKAY"这些字眼。
那么如何保证内容不被篡改呢?可以肯定的说,做不到,或者在很多场合下做不到。比如小白是在家里通过有线宽带上网,如果你没有办法阻止小黑在线路上安装监听器,当然也就无法保证通信数据不被恶意更改。
我们所能做的,就是当数据被篡改时,双方可以察觉到这种变化,即保证通信的发送端和接收端的数据的"完整性",这就是Integrity要解决的问题。
Non-repudiation
通过上面的努力,小白和小红终于可以将小黑的破坏抛诸脑后了。不过"安外"以后,"攘内"的问题就出现了。比如有这样一个场景,小白和小红在通信时约定了某项工程的金额,但是没过几天,小红就不认账了,并否认曾经做出的承诺。传统的解决方法里,双方在某项协商达成一致时是必须"白纸黑字"签订合同的。那么在信息通信中,是否也有类似的实现手段?
这就是"Non-repudiation"所要达到的目的。它将保证任何一方的承诺,无可抵赖和篡改,以保证对方的权益。
上面我们以实例的形式分析了信息安全中所面临的四类基础问题(实际上还有第五类问题,即"Availability",用以衡量某项服务的可用性和可访问性。比如网站在大流量访问下是否可以正常运转。顺便提一下,在密码学领域发表论文时,一个惯例就是要明确指明你的文章解决了这其中的哪几类问题),接下来就需要从数学的角度来考虑下如何具体地解决这些问题。
信息安全学的基础是数学,而其中的关键点总结起来有三个,即
- Encryption (加密)
- Decryption (解密)
加解密算法发展到今天种类已经相当繁多,大的方向可以分为对称和非对称两种
- Hash (哈希散列)
简单来讲,Hash就是将不定长的输入变成定长输出的一个过程
我们先来看下加密和解密,哈希的一些基础知识。
对称算法(Symmetric Algorithm)
如果加密和解密所用的密钥是一样(单钥密码体系)的,就是对称算法。这是一种传统的加密方式,从密码学早期就已经存在了(当然,随着科技的发展,其具体算法仍在不断演进中)。我们在日常生活中也随处可见对称加密方式,比如大家家里的锁就是单钥系统,外出锁门时和回家开门时所用的是完全一样的同一把钥匙。这种方式的加密算法速度很快,通常应用于大数据量加密的场合。当前密码学中常见的对称算法包括DES,AES等等。
传统对称算法的一个缺点是不利于传输。如下图所示:
图 135对称算法的密钥传输问题
我们来设想这样一个场景,小白想邮寄一封密信给小红,为了防止被小黑窃取信件的内容,它首先将信放入盒子中,然后加上了一把锁后再邮寄出去。这样确实能保证小黑无法浏览到信的内容,不过却有一个致命的问题,小红也同样没有办法浏览信件的内容,因为它和小黑一样没有锁的钥匙。
那么将钥匙和盒子一起寄过去?显然这样的方式是很愚蠢的,并没有起到任何保护密信的目的。直接寄送密钥是行不通的,于是科学家们开始思考,是否能两边协商出一个共同的密钥?这确实是一个好主意,不过小黑对于这个"协商"过程,也肯定是知晓的(在没有加密前,所有信息都是明文传送,小黑可以轻易获取两方正在进行的任何沟通),因而这个方案成功的前提是,如何绕过小黑完成协商过程?
网络传输过程中的信息小黑是能获知的,这句话的另一种说法,就是通信双方本地(比如小白和小红使用的计算机里内存的数据)的数据,它是没有办法得到的。整个协商过程的突破口就在这里了,我们下面以著名的DH(Diffie-Hellman)算法为例来解释这个实现过程。
- 小白和小红首先需要有两个公共的值g和p。因为是公开的,小黑也可以得到这两个数值
- 小白在本地产生一个私密值a,小红也同样产生一个私密值b
- 小白通过公式Y1=g^a mod p计算出自己的Y值,小红也根据同样的公式算出它的Y2=g^b mod p
- 然后小白和小红互换它们的Y值
- 小白计算出通信所需要采用的密钥Key1=(Y2)^a=( g^b mod p)^a=g^(ab)mod p,而小红计算出密钥Key2=(Y1)^b=(g^a mod p)^b= g^(ab)mod p=Key1
这样一来,它们就协商出共同的Key值了。那么这一过程中,小黑都获得了哪些数据?很明显,在网络中传输的值是g,p,Y1,和Y2,这其中并没有Key1或者Key2,而计算密钥Key所需的关键数值a或者b,也没有被直接传送。因为Y值计算公式的不可逆性,小黑更不可能从中推导出a或者b值。因此我们可以得出一个结论,整个密钥协商过程是安全可靠的。
公钥算法/不对称算法(Public-key Algorithm)
公钥算法的核心是加密和解密所用的密钥不是同一个,即有两个密钥,我们分别称之为公钥和私钥。一般情况下,数据用私钥/公钥进行加密,然后再通过匹配的公钥/私钥解密(其中的数学推导过程我们不做深入分析,有兴趣的读者可以自行查阅相关资料)。公钥是所有人都可以获知的,私钥则由个人自己保存。如下图所示:
图 136公钥算法应用1
通过上面的方法,小白成功的将数据安全传送给小红。因为小黑并没有小红的私钥,它无论如何也无法破解数据内容。而另一方面,因为公钥是所有人都可见的,就避免了对称算法中密钥传输难题。
上面我们使用的是接收方的公钥来加密数据,如果反其道而行,用发送方的私钥进行加密,又会是什么样的情况?如下图所示。
图 137公钥算法应用2
读者可能会觉得有点奇怪,既然公钥是大家都能获取到的,而且可以解密,那么数据还有什么安全性可言?请耐心接着往下阅读,答案很快就揭晓了。
常用的公钥算法包括RSA和DSA等等。
哈希算法 (Hash Algorithm)
学习过数据结构的读者一定对哈希不陌生,因为哈希表进行查找也是常用的算法之一。Hash的作用就是将任意长度的二进制值映射为固定长度的最终值。从概率学的角度而言,两个不同的输入值经过Hash算法后是有可能发生碰撞的。因而算法的好坏很大一方面取决于它能否最大限度的降低这种冲突。另一方面,要求整个转换过程具有随机性,就算两个输入值仅有非常小的差异,其输出值也应该是毫无关联的。这样的做法在信息安全中有重要意义,可以有效防止非法人员通过不断推测来获知明文信息。
Hash算法除了用于查找外,还有很多其它方面的应用,比如消息摘要,数字签名等等。常见的算法有MD5,SHA,SHA-1,SHA-256,SHA384,SHA-512等等。
加解密和哈希算法是解决信息安全领域众多问题的基础。下面我们再回头来看下之前碰到的四个安全隐患。
- Authentication
上面的公钥算法中,我们知道私钥是由个人自己保存的话,其它所有人都是无法获知的。这就给我们这里的身份认证提供了理论依据。比如例子中,小白确认对方是不是小红的依据,就在于对方有没有拥有小红的私钥。那么如何确认呢?目前通常的作法如下:
- 用小红的公钥去加密一段数据,然后传给对方
- 对方用私钥解出明文数据,并返还给小白
- 小白比较对方提供的明文是否和自己之前的数据匹配
因为公钥加密过的数据只能由私钥解,所以只要对方能正确提供原始数据,就可以认定它是小红。
不过实际的过程还要再复杂一点。想象一下,如果小黑是等到小白和小红做完了认证后,再介入呢,此时小白已经完全相信对方是小红,很有可能造成安全问题。所以认证的同时,也要综合考虑双方的数据加密,这样才不会让非法人员有机可乘。
- Confidentiality
加密协商通常是和上述的认证过程综合进行的。如果是大数据量的传送,一般情况下需要使用DH算法协商出对称密钥,而对于一些小量的数据,可以使用双方的公钥进行加密。
- Data Integrity
单纯的加解密算法无法解决完整性认证,它还应该引入Hash算法。
图 138数据完整性的验证过程
上图的主要步骤如下:
- 发送方首先对数据进行哈希处理,得到一个Hash值
- 发送方将数据和哈希值进行加密,并传送到接收方
- 接收方解密后,先对数据进行同样的哈希处理,得到另一个Hash值
- 接收方将收到的哈希值和上一步中得到的Hash值进行比较,判断数据传递过程中是否被篡改
- Non-repudiation
Non-repudiation的直译是"无法否认"。在认证过程中,我们采用的原理是"只有拥有私钥的人才能解密用公钥加密的数据"。与之类似,"只有用私钥加密的数据才能用公钥解开"。这就是数字签名所依据的理论原理。
假设小白认可了一份合同,并使用自己的私钥对其进行了加密,那么如果发生纠纷,就可以使用小白的公钥对这份文件进行解密。由于私钥的唯一性,小白就没有办法否认经过它签名过的内容。
不过在实际的应用中,情况通常不会这么简单。比如谁能保证小白的公钥是哪一个?为了解决这个问题,就需要有一个公共的服务中心来保管和提供权威的公钥查询,这就是CA (Certificate Authority)的职责所在。
目前有比较多的CA机构提供数字证书的颁发和查询,其中一部分是免费的。当用户需要验证某份公钥是否属于它所要建立连接的机构或个人时,就可以向CA发起请求。在浏览网页的过程中,这一过程通常由浏览器自动帮你完成了。比如你在访问Https开头的网站时,通常服务器会发送一份经过CA签名过的证书来证明自己就是你所要找的目标,这时浏览器就需要自动去认证这一证书的真伪。假如浏览器已经有该CA的证书,表示它信任这个组织,那么它就可以使用CA的公钥去解密服务器的证书并做完整性测试,如果一切顺利的话,浏览器就可以相信服务器里所提供的公钥和身份信息。而后使用这一公钥与服务器进行对话了。
这一小节中,我们首先从一个典型的信息对话场景入手,引出可能发生的所有安全隐患,然后结合密码学的基础理论(加解密,哈希算法),详细讲解了如何应对这些安全问题。其中提到了各种解决方案都是应用密码学中的典型应用。下一小节,我们将具体分析Android系统又是如何保证应用程序的安全的。
应用程序签名
首先要明白以下几点:
- 所有的Android应用程序都需要被签名,不论它是debug还是release版本。可以参看本章的第一小节
- 可以采用自签名的形式,也就是说,可以不需要上一小节提到的CA认证
- 系统只在安装过程中检查证书的有效性,如果应用程序安装后证书过期,并不会影响它的使用
- 可以使用Keytool和Jarsigner来完成签名过程,然后还需要使用zipalign对apk文件进行优化。可以参阅本章的第一小节
- 建议开发者对所有自己研发的应用程序采用统一的证书
- 当应用程序升级时,系统会比较新旧版本的证书是否一致。如果证书一致,升级可以顺利进行,否则将失败。当然,你也可以更换新版本的包名,这时系统会把它当成另外一个应用程序进行安装
- 采用相同签名的应用程序允许被安排在同一个进程中运行
- 采用相同签名的应用程序间可以根据特殊权限进行代码和数据的共享
接下来我们分别介绍debug和release模式下的签名过程。其中,debug模式比较简单,因此只是做粗略介绍。
Debug Mode
这个模式下的签名过程是由系统自动完成的。因为采用的是默认的keystore,用户不需要特别输入密码等信息。签名所需用到的工具Keytool和Jarsinger是由JDK提供的,因此需要保证JAVA_HOME环境变量的正确性。
默认的签名信息如下所示:
- Keystore name: "debug.keystore"
- Keystore password: "android"
- Key alias: "androiddebugkey"
- Key password: "android"
- CN: "CN=Android Debug,O=Android,C=US"
需要注意的是,Debug下所使用的证书也是会过期的,它从生成之日起只有365天的有效期。这时系统会有类似下面的提示:
Debug Certificate expired on 8/4/08 3:43 PM
解决的方法就是将debug.keystore文件删除,那么下一次编译时就会再自动生成新的keystore。存放debug.keystore文件的路径依据不同的操作系统会有差异:
- Linux和OS X
~/.android/
- Windows XP
C:\Documents and Settings\<user>\.android\
- Windows 7
C:\Users\<user>\.android\
Release Mode
Release模式的签名过程相对麻烦一些。
- 获取一个私人密钥
可以选择使用keytool工具生成一个新的密钥。需要特别注意,如果发布的应用程序是针对Google Play的话,那么证书的过期时间必须在2033年10月22号以后。Keytool的使用方法我们这里不做详细介绍,读者可以自己参阅其它资料,或者浏览官方文档http://docs.oracle.com/javase/6/docs/technotes/tools/windows/keytool.html
- 编译release版本的应用程序
我们在第一小节已经做过详细介绍,这里不再赘述
- 利用相关工具对应用程序进行签名
JDK已经提供了Jarsigner来完成签名过程。当然,你也可以选择其它合适的工具来替代Jarsigner。关于这个工具的详细使用方法,可以参见官方文档
http://docs.oracle.com/javase/6/docs/technotes/tools/windows/jarsigner.html
- 最后对签名后的应用程序进行对齐
前面的小节我们对zipalign进行过简单的介绍,它保证所有数据能按照特定标准相对文件开头进行字节对齐。这将在一定程度上提高应用程序的运行速度,比如系统可以使用mmap()来读取文件,而不是拷贝包中的所有数据。Zipalign的语法很简单,如下所示:
zipalign -v 4 App_name-unaligned.apk App_name.apk
其中,-v开启verbose输出。数值4代表要对齐的字节数(当前只允许填写4)。
后两个apk对于zipalign分别是输入和输出。如果需要覆盖原有的apk,还需要
加上-f标志
如果你是在Eclipse下开发,觉得使用命令行模式效率太低,那么还可以使用ADT提供的Export Wizard来逐步导出有效的Apk应用程序(如下图的左边部分),这种方式可以让你使用已有的keystore进行签名,也同时允许新建一个keystore(下图右边部分)。
图 139使用ADT的Export功能导出合法的Apk
这时编译系统会对Apk应用程序做更加详细的检查,包括安全(Security),效率(Performance),可用性(Usability)等多个方面。如果发现有错误,默认情况下会停止继续导出Apk。你也可以在Preferences->Lint Error Checking中关闭这个检查功能。如下图所示:
图 1310 Lint Error Checking
由此可见Android系统提供了多种开发的方式。具体选择哪一种,取决于开发者的习惯,以及项目的实际情况。而无论是命令行或是图形界面操作,应用程序的编译,打包,签名,对齐这些操作的流程是不变的。
应用程序签名源码简析
这一小节我们来简要分析下应用程序签名的关键源码。
首先来比较下签名前和签名后Apk的区别。下面两个图显示了分别用Eclipse的"Export Unsigned Application Package"和"Export Singed Application Package"导出来的同一个Apk的目录结构。
图 1311未签名的Apk目录结构
图 1312签名后的Apk目录结构
如果直接安装未签名的Apk,adb将会报错,如下所示:
可以看到,两者间的唯一差别就是META-INF文件夹,其它的数据从大小和内容上都是一样的。META-INF我们前几个小节做过简单的介绍,它是专门用来保存应用程序签名和校验等安全信息的目录,通常情况下包括了MANIFEST.MF,CERT.SF和CERT.RSA三个文件。签名和校验过程实际上就围绕这三个文件展开,可以用如下简图概况它们之间的关系:
图 1313签名与校验简图
接下来我们将通过分析verifier源码来了解META-INF下各文件的用途,以及整个签名校验的大致流程。
当一个应用程序需要安装时,首先需要Package Manager对其进行初始的处理,这其中就包含了对签名和文件哈希值的检查,函数流程图如下图所示:
图 1314安装应用程序时的安全检查
这其中的逻辑关系比较复杂,主要涉及以下几个类:
- PackageParser
负责解析应用程序包,并完成安全校验。而且整个校验过程是对Apk包中的所有文件逐个进行的,这也同时解释了为什么MANIFEST.MF中针对每个文件都提供了hash值。一个典型的MANIFEST.MF文件格式如下所示:
/*MANIFEST.MF*/
Manifest-Version: 1.0
Created-By: 1.0 (Android)
Name: res/drawable-ldpi/pty.png
SHA1-Digest: JfxEcu/NKzCCaCsg1rwnOxUBK7U=
Name: res/drawable-ldpi/fm3_down.png
SHA1-Digest: LvoLkSkySbbH79GbuCc+qg311do=
Name: res/drawable-ldpi/signal2.png
SHA1-Digest: yVjMqmIUQ5cKNi/dgyq35o2d3gQ=
Name: res/drawable-ldpi/fm2.png
SHA1-Digest: 9M3S7wzBvE2bJn/ffa1IF+546sk=
Name: res/drawable/key4_select.xml
SHA1-Digest: jj3NmAjUMeqfAQvnl0ijUNHQN9Q=
Name: res/drawable-ldpi/ta_indicate.png
SHA1-Digest: kcqTpfODE7dh1QTsY0miCCZP6lI=
只要安装过程中的任何文件的Hash匹配无法通过,整个安装就会终止,并有类似如下的提示:
Package *** has no certificates at entry ** ; ignoring!
其中的entry即是指程序包中的某个文件。
我们这里再补充一些密码学的基础知识,这样大家在学习源码时就更容易掌握了。取上面MANIFEST.MF中的第一个文件为例,即:
Name: res/drawable-ldpi/pty.png
SHA1-Digest: JfxEcu/NKzCCaCsg1rwnOxUBK7U=
计算这个pty.png文件SHA-1摘要值的步骤如下:
- 根据SHA-1算法得到这个文件的摘要。标准SHA-1的输出位数为160bit
- 有读者可能会觉得奇怪,既然SHA-1的输出为160位,即20个字节,那么为什么下面的字符串有28个呢?这是因为上述得出的20字节的数据还需要经过BASE64编码。
BASE64的基本规则是将原数据的3个字符变为4个字符,每6位前加上2位0,所以最终得到的每字节最大值都不会超过64。因为0~63的ASCII码是有不可见字符的,为了方便起见,算法还会将这64个数分别对应固定的可见ASCII。
比如经过SHA-1运算后,我们得到如下值:
25FC4472EFCD2B3082682B20D6BC273B15012BB5,一共20个字节。
前3个字节的二进制码为00100101(25) 11111100(FC) 01000100(44)
我们在每6位前都加上两位0,这样就变成:
000010010001111100110001 00000100
| | | |
十进制 9 31 49 4
根据BASE64表,数值9,31,49,4分别对应可见ASCII字条中的J,f,x和E,这和我们上面看到的MANIFEST.MF中存储的HASH值是一致的,说明这个pty.png文件没有被篡改过。读者可以依照上面的算法自行验证计算剩余的几个字符。
- JarFile
继承自ZipFile,每一个Apk包只对应唯一的JarFile,这也进一步验证了应用程序包实际上是一个Zip压缩包。它代表了检验过程中的一个整体,真正的匹配工作则由JarVerifier完成,可以参见下面的类图关系。
- JarVerifier
JarVerifier是各种校验数据的储存仓库,同时它包含了VerifierEntry嵌套类,后者会对每一个文件做具体的检查匹配工作。
- VerifierEntry
真正的匹配是在这里完成的。JarVerifier在生成一个VerifierEntry时,会进行一定的初始化,然后JarFileInputStream还会进一步完善其中的数据,然后进行匹配校验。成功后它的Entry将提交JarVerifier进行存储,以备后面的查询。
- JarFileInputStream
继承自InputStream,同时也是JarFile中的嵌套类。在
我们再提供一个类图来帮助大家理解:
图 1315安全校验相关类的关系图
接下来我们分析部分重点代码。
PackageParser.java
public boolean collectCertificates(Package pkg, int flags) {
…
JarFile jarFile = new JarFile(mArchiveSourcePath);
/*创建一个JarFile实例,以Apk包的路径作为参数*/
if ((flags&PARSE_IS_SYSTEM) != 0) {
/*系统包的情况,只检查AndroidManifest.xml文件,不用逐个校验包中所有文件*/
…
}else{
Enumeration<JarEntry> entries = jarFile.entries(); //程序包中所有文件
final Manifest manifest = jarFile.getManifest(); /*MANIFEST.MF*/
while (entries.hasMoreElements()) {
//逐个对包中的所有文件进行校验
final JarEntry je = entries.nextElement();
if (je.isDirectory()) continue; //忽略目录
final String name = je.getName(); //文件名
if (name.startsWith("META-INF/"))
continue;
if (ANDROID_MANIFEST_FILENAME.equals(name)) {
/*如果文件是AndroidManifest.xml*/
final Attributes attributes = manifest.getAttributes(name);
pkg.manifestDigest = ManifestDigest.fromAttributes(attributes);
}
final Certificate[] localCerts = loadCertificates(jarFile, je, readBuffer);
/*这是整个安全检查的关键,下面我们会详细分析这个函数*/
if (DEBUG_JAR) {
Slog.i(TAG, "File " + mArchiveSourcePath + " entry " + je.getName()
+ ": certs=" + certs + " ("
+ (certs != null ? certs.length : 0) + ")");
}
if (localCerts == null) {
/*当上述函数无论什么原因导致失败时,都会返回null值,这时系统将有
如下的提示信息。要注意失败的具体原因还应根据系统抛出的异常来进一步判断*/
Slog.e(TAG, "Package " + pkg.packageName
+ " has no certificates at entry "
+ je.getName() + "; ignoring!");
jarFile.close();
mParseError =
PackageManager.INSTALL_PARSE_FAILED_NO_CERTIFICATES;
return false;
}
…
PackageParser通过collectCertificates()检验程序包中的所有文件是否符合要求,然后才返回PackageManager继续执行安装过程。
/*PackageParser.java*/
private Certificate[] loadCertificates(JarFile jarFile, JarEntry je, byte[] readBuffer) {
try {
InputStream is = new BufferedInputStream(jarFile.getInputStream(je));
/*getInputStream()返回一个JarFileInputStream实例,后者又包含了一个已
经过初始化的VerifierEntry实例*/
while (is.read(readBuffer, 0, readBuffer.length) != -1) {
/*BufferedInputStream中包含了JarFileInputStream,所以最终是调用
它的read()函数*/
}
is.close();
return je != null ? je.getCertificates() : null;
} catch (IOException e) {
Slog.w(TAG, "Exception reading " + je.getName() + " in "
+ jarFile.getName(), e);
} catch (RuntimeException e) {
Slog.w(TAG, "Exception reading " + je.getName() + " in "
+ jarFile.getName(), e);
}
return null;
}
整个检验过程主要涉及以下几点:
- CERT.RSA
这是应用程序开发者提供的证书,包含了该开发者的公钥和一系列身份信息。因为是自签名的,就不需要CA的认证.
- CERT.SF
后缀名.SF应该是Signature File的缩写,所以这就是我们所说的签名文件。根据前面密码学基础的学习,它是对某个文件的Hash值进行私钥加密产生的。那么针对这里的情况,这个文件会是什么?最合理的可能就是MANIFEST.MF文件,因为它包含了应用包中所有文件的Hash值。理论上可以对每个文件的摘要分别进行签名,但Android选择了一个聪明点的办法,它对整个MANIFEST.MF进行了加密。这样就可以保证此文件是否完整可靠,也能认证程序提供的私钥和公钥是否匹配。
- MANIFEST.MF
只要确认了MANIFEST.MF文件的可靠性,就可以通过读取其中的信息来为APK包中的所有文件做一一校验了。接下来的代码中我们侧重于这一校验过程的分析,其它上面两个文件相关的安全检查,读者可以自己参阅代码。
/*JarFile.java*/
public int read() throws IOException {
if (done) {
return -1;
}
if (count > 0) {
/*如果count大于0,说明还有数据需要写入VerifierEntry(之前initEntry()时
已经做过一定初始化)*/
int r = super.read();
if (r != -1) {
entry.write(r);
count--;
} else {
count = 0;
}
if (count == 0) {
done = true;
entry.verify();
/*所有数据都已经保存完毕,进入校验阶段*/
}
return r;
} else {
done = true;
entry.verify();
return -1;
}
}
最后我们再来分别看下VerifierEntry里的verify()实现。
/*JarVerifier.java*/
class JarVerifier {
…
class VerifierEntry extends OutputStream { //实际上是一个OutputStream
/**
这个verify()函数的作用是将CERF.SF解密后的数据与MANIFEST.MF进行比较,
以此来证明证书的有效性。因而它并不是用来验证应用程序包中所有文件的完整性
*/
void verify() {
byte[] d = digest.digest();
if (!MessageDigest.isEqual(d, Base64.decode(hash))) {
/*正如我们上面所举的例子,存储在MANIFEST.MF中的SHA-1值经过了
BASE64编码,因此这里还需要先进行解码*/
throw invalidDigest(JarFile.MANIFEST_NAME, name, jarName);
/*如果不匹配,抛出异常*/
}
verifiedEntries.put(name, certificates);
}
…}
…}
Android签名机制保证了应用程序在安装前没有被恶意篡改,保护了开发者的权益,也同时为用户选择合法来源的应用程序提供了有利保障。