转载请注明:LXS, blog.csdn.net/uiop78uiop78/article/details/8504128
文章是在word上写好后,复制到csdn的,csdn不支持live writer,每次编辑都很伤脑筋,最终的效果也很差。有知道方法的朋友告知一下,感谢。
同步发布在http://www.cnblogs.com/lxs-android/archive/2013/01/15/2861822.html
Android中应用程序的编译可以如下几种方式:
我们在本书的基础篇中对Android系统的编译框架进行过分析。它利用Android.mk文件将众多小项目组织起来,并且提供了非常方便的函数来编译出各种可执行文件,库,和应用程序等等。虽然我们完全可以借助于系统编译来完成应用程序的编译,不过这种方式并不多见。一方面,这需要开发工程师对整个系统的编译框架有一定的认识,另一方面,还需要下载整个源码项目,每次编译的时间也会非常长。
所以一般的纯应用程序(非系统级应用)开发,都会借助于IDE工具,比如Eclipse就是使用最广泛的一种。Android为Eclipse提供了ADT组件,从而让用户可以像操作Visual Studio一样开发Android应用程序。
工程师在ADT的帮助下,几乎不用做多余的操作就可以完成编译。不过,这样造成的一个副作用是很多人对于应用程序的编译、打包、签名等等基础过程都完全不清楚。因而这章的内容我们将讲解隐藏在"ADT"背后的这些细节。
完成一个软件项目编译需要哪些工具?编译器是毋庸置疑的,比如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无法正常运行。解决的办法有两个:
总结而言,ant可以提供两种方式的编译,即debug和release。无论何种方式生成的应用程序,都需要经过签名和zipalign的优化,只不过debug的方式默认就会帮开发者完成这些工作。关于签名过程的一些描述,请参阅下一小节。
Debug模式
编译debug版本的项目,一般步骤如下:
这样就会在项目的bin目录下生成一个后缀为-debug.apk的文件,而且它已经用debug key签过名,也经过了zipalign的优化。
Release模式
虽然上面的debug模式非常方便,但并不适用于将要发布出去的应用程序。因为它采用的是系统默认的签名文件,没有起到很好的安全保护作用。
在release模式下,签名和zipalign默认情况下都需要开发者手动完成。一般步骤如下:
可能有读者会认为这个过程比较繁琐,有一个简化的方法可以在release模式下自动为apk签名和优化。
这样ant release命令在生成apk的过程中会询问密码,编译完成后的应用程序就已经用你提供的my.keystore签名了。
编译后生成的Apk还需要安装到模拟器或设备上以供用户使用。在Eclipse上,我们只要点击"Run->Run/Debug"就可以将程序安装到目标上(目标可以是模拟器或设备,具体选择哪个设备一方面取决于当前连接的实际情况,另一方面还与Run/Debug Configurations里Target页中的设置有关系),实际上这一过程是借助于adb的install功能,因而命令行模式下,我们还是可以使用adb install来达到安装要求。关于这一过程,可以参见本书的工具篇对adb的详细描述,这里不再赘述。
上面我们对Ant的两种编译模式进行了概述,从使用的角度出发向读者介绍了命令行模式下的编译过程。接下来,我们进一步分析编译的实际过程,即Android是如何将项目源码一步步编译打包成最终的.apk文件。
以apk为后缀的文件是Android应用的标准格式,它其实是一个zip压缩包,所以可以用WinRar等工具将其解压出来。可以看到,一个典型的apk应用包含以下几部分内容:
这个文件相信大家都不会陌生,如果应用程序是一本书,那么这个文件就是它的"封面"和"目录",记载了应用程序的名称,权限声明,所包含的组件等等一系列信息。不过从普通apk解压出来的AndroidManfiest是无法直接打开的,因为它发布时已经经过了保护处理
Apk应用程序的核心。它是由项目源码生成的.class文件,经进一步转化而成的Android系统可识别的Dalvik Byte Code
编译过后的资源文件
未编译的资源文件
保存应用程序的签名和校验信息,以保证程序的完整性。当生成apk包时,需要对内容做一次校验,并将结果保存在这里。而设备在安装这一应用时,会对内容再做一次校验,并和先前的值进行比较,以验证程序包是否已经被恶意篡改
下图详细描述了整个编译过程:
图 131 Apk的编译全过程图解
可以清楚地看到,整个编译过程涉及了多种工具,我们下面对其中的几个重要步骤进行讲解。
上面编译过程所涉及到的一部分重要工具的使用方法,可以参看本书工具篇中的讲解。至此,我们已经熟悉了整个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.
从中可以看出密码与安全学的几个基础目标是:
这样子说读者可能会觉得枯燥而又抽象难懂,下面就举个例子来帮助大家理解。假设有三个人,分别是小白(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",用以衡量某项服务的可用性和可访问性。比如网站在大流量访问下是否可以正常运转。顺便提一下,在密码学领域发表论文时,一个惯例就是要明确指明你的文章解决了这其中的哪几类问题),接下来就需要从数学的角度来考虑下如何具体地解决这些问题。
信息安全学的基础是数学,而其中的关键点总结起来有三个,即
加解密算法发展到今天种类已经相当繁多,大的方向可以分为对称和非对称两种
简单来讲,Hash就是将不定长的输入变成定长输出的一个过程
我们先来看下加密和解密,哈希的一些基础知识。
对称算法(Symmetric Algorithm)
如果加密和解密所用的密钥是一样(单钥密码体系)的,就是对称算法。这是一种传统的加密方式,从密码学早期就已经存在了(当然,随着科技的发展,其具体算法仍在不断演进中)。我们在日常生活中也随处可见对称加密方式,比如大家家里的锁就是单钥系统,外出锁门时和回家开门时所用的是完全一样的同一把钥匙。这种方式的加密算法速度很快,通常应用于大数据量加密的场合。当前密码学中常见的对称算法包括DES,AES等等。
传统对称算法的一个缺点是不利于传输。如下图所示:
图 135对称算法的密钥传输问题
我们来设想这样一个场景,小白想邮寄一封密信给小红,为了防止被小黑窃取信件的内容,它首先将信放入盒子中,然后加上了一把锁后再邮寄出去。这样确实能保证小黑无法浏览到信的内容,不过却有一个致命的问题,小红也同样没有办法浏览信件的内容,因为它和小黑一样没有锁的钥匙。
那么将钥匙和盒子一起寄过去?显然这样的方式是很愚蠢的,并没有起到任何保护密信的目的。直接寄送密钥是行不通的,于是科学家们开始思考,是否能两边协商出一个共同的密钥?这确实是一个好主意,不过小黑对于这个"协商"过程,也肯定是知晓的(在没有加密前,所有信息都是明文传送,小黑可以轻易获取两方正在进行的任何沟通),因而这个方案成功的前提是,如何绕过小黑完成协商过程?
网络传输过程中的信息小黑是能获知的,这句话的另一种说法,就是通信双方本地(比如小白和小红使用的计算机里内存的数据)的数据,它是没有办法得到的。整个协商过程的突破口就在这里了,我们下面以著名的DH(Diffie-Hellman)算法为例来解释这个实现过程。
这样一来,它们就协商出共同的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等等。
加解密和哈希算法是解决信息安全领域众多问题的基础。下面我们再回头来看下之前碰到的四个安全隐患。
上面的公钥算法中,我们知道私钥是由个人自己保存的话,其它所有人都是无法获知的。这就给我们这里的身份认证提供了理论依据。比如例子中,小白确认对方是不是小红的依据,就在于对方有没有拥有小红的私钥。那么如何确认呢?目前通常的作法如下:
因为公钥加密过的数据只能由私钥解,所以只要对方能正确提供原始数据,就可以认定它是小红。
不过实际的过程还要再复杂一点。想象一下,如果小黑是等到小白和小红做完了认证后,再介入呢,此时小白已经完全相信对方是小红,很有可能造成安全问题。所以认证的同时,也要综合考虑双方的数据加密,这样才不会让非法人员有机可乘。
加密协商通常是和上述的认证过程综合进行的。如果是大数据量的传送,一般情况下需要使用DH算法协商出对称密钥,而对于一些小量的数据,可以使用双方的公钥进行加密。
单纯的加解密算法无法解决完整性认证,它还应该引入Hash算法。
图 138数据完整性的验证过程
上图的主要步骤如下:
Non-repudiation的直译是"无法否认"。在认证过程中,我们采用的原理是"只有拥有私钥的人才能解密用公钥加密的数据"。与之类似,"只有用私钥加密的数据才能用公钥解开"。这就是数字签名所依据的理论原理。
假设小白认可了一份合同,并使用自己的私钥对其进行了加密,那么如果发生纠纷,就可以使用小白的公钥对这份文件进行解密。由于私钥的唯一性,小白就没有办法否认经过它签名过的内容。
不过在实际的应用中,情况通常不会这么简单。比如谁能保证小白的公钥是哪一个?为了解决这个问题,就需要有一个公共的服务中心来保管和提供权威的公钥查询,这就是CA (Certificate Authority)的职责所在。
目前有比较多的CA机构提供数字证书的颁发和查询,其中一部分是免费的。当用户需要验证某份公钥是否属于它所要建立连接的机构或个人时,就可以向CA发起请求。在浏览网页的过程中,这一过程通常由浏览器自动帮你完成了。比如你在访问Https开头的网站时,通常服务器会发送一份经过CA签名过的证书来证明自己就是你所要找的目标,这时浏览器就需要自动去认证这一证书的真伪。假如浏览器已经有该CA的证书,表示它信任这个组织,那么它就可以使用CA的公钥去解密服务器的证书并做完整性测试,如果一切顺利的话,浏览器就可以相信服务器里所提供的公钥和身份信息。而后使用这一公钥与服务器进行对话了。
这一小节中,我们首先从一个典型的信息对话场景入手,引出可能发生的所有安全隐患,然后结合密码学的基础理论(加解密,哈希算法),详细讲解了如何应对这些安全问题。其中提到了各种解决方案都是应用密码学中的典型应用。下一小节,我们将具体分析Android系统又是如何保证应用程序的安全的。
首先要明白以下几点:
接下来我们分别介绍debug和release模式下的签名过程。其中,debug模式比较简单,因此只是做粗略介绍。
Debug Mode
这个模式下的签名过程是由系统自动完成的。因为采用的是默认的keystore,用户不需要特别输入密码等信息。签名所需用到的工具Keytool和Jarsinger是由JDK提供的,因此需要保证JAVA_HOME环境变量的正确性。
默认的签名信息如下所示:
需要注意的是,Debug下所使用的证书也是会过期的,它从生成之日起只有365天的有效期。这时系统会有类似下面的提示:
Debug Certificate expired on 8/4/08 3:43 PM
解决的方法就是将debug.keystore文件删除,那么下一次编译时就会再自动生成新的keystore。存放debug.keystore文件的路径依据不同的操作系统会有差异:
~/.android/
C:\Documents and Settings\<user>\.android\
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
我们在第一小节已经做过详细介绍,这里不再赘述
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安装应用程序时的安全检查
这其中的逻辑关系比较复杂,主要涉及以下几个类:
负责解析应用程序包,并完成安全校验。而且整个校验过程是对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摘要值的步骤如下:
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,这样就变成:
00001001000111110011000100000100
| | | |
十进制 9 31 49 4
根据BASE64表,数值9,31,49,4分别对应可见ASCII字条中的J,f,x和E,这和我们上面看到的MANIFEST.MF中存储的HASH值是一致的,说明这个pty.png文件没有被篡改过。读者可以依照上面的算法自行验证计算剩余的几个字符。
继承自ZipFile,每一个Apk包只对应唯一的JarFile,这也进一步验证了应用程序包实际上是一个Zip压缩包。它代表了检验过程中的一个整体,真正的匹配工作则由JarVerifier完成,可以参见下面的类图关系。
JarVerifier是各种校验数据的储存仓库,同时它包含了VerifierEntry嵌套类,后者会对每一个文件做具体的检查匹配工作。
真正的匹配是在这里完成的。JarVerifier在生成一个VerifierEntry时,会进行一定的初始化,然后JarFileInputStream还会进一步完善其中的数据,然后进行匹配校验。成功后它的Entry将提交JarVerifier进行存储,以备后面的查询。
继承自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;
}
整个检验过程主要涉及以下几点:
这是应用程序开发者提供的证书,包含了该开发者的公钥和一系列身份信息。因为是自签名的,就不需要CA的认证.
后缀名.SF应该是Signature File的缩写,所以这就是我们所说的签名文件。根据前面密码学基础的学习,它是对某个文件的Hash值进行私钥加密产生的。那么针对这里的情况,这个文件会是什么?最合理的可能就是MANIFEST.MF文件,因为它包含了应用包中所有文件的Hash值。理论上可以对每个文件的摘要分别进行签名,但Android选择了一个聪明点的办法,它对整个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签名机制保证了应用程序在安装前没有被恶意篡改,保护了开发者的权益,也同时为用户选择合法来源的应用程序提供了有利保障。