Android應用程序的編譯和打包


  1. Android應用程序的編譯和打包

    Android中應用程序的編譯可以如下幾種方式:

  • 借助於系統編譯

我們在本書的基礎篇中對Android系統的編譯框架進行過分析。它利用Android.mk文件將眾多小項目組織起來,並且提供了非常方便的函數來編譯出各種可執行文件,庫,和應用程序等等。雖然我們完全可以借助於系統編譯來完成應用程序的編譯,不過這種方式並不多見。一方面,這需要開發工程師對整個系統的編譯框架有一定的認識,另一方面,還需要下載整個源碼項目,每次編譯的時間也會非常長。

 

  • 借助於IDE工具

所以一般的純應用程序(非系統級應用)開發,都會借助於IDE工具,比如Eclipse就是使用最廣泛的一種。Android為Eclipse提供了ADT組件,從而讓用戶可以像操作Visual Studio一樣開發Android應用程序。

 

  • 命令行編譯

工程師在ADT的幫助下,幾乎不用做多余的操作就可以完成編譯。不過,這樣造成的一個副作用是很多人對於應用程序的編譯、打包、簽名等等基礎過程都完全不清楚。因而這章的內容我們將講解隱藏在"ADT"背后的這些細節。

  1. 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安裝到名稱不帶空格的路徑中
  1. 通過命令行編譯和打包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的詳細描述,這里不再贅述。

  1. 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編譯的流程,接下來的一個小節,將重要分析下應用程序的簽名。

  1. 信息安全基礎概述

    在講解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)算法為例來解釋這個實現過程。

  • 小白和小紅首先需要有兩個公共的值gp。因為是公開的,小黑也可以得到這兩個數值
  • 小白在本地產生一個私密值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系統又是如何保證應用程序的安全的。

  1. 應用程序簽名

    首先要明白以下幾點:

  • 所有的Android應用程序都需要被簽名,不論它是debug還是release版本。可以參看本章的第一小節
  • 可以采用自簽名的形式,也就是說,可以不需要上一小節提到的CA認證
  • 系統只在安裝過程中檢查證書的有效性,如果應用程序安裝后證書過期,並不會影響它的使用
  • 可以使用KeytoolJarsigner來完成簽名過程,然后還需要使用zipalignapk文件進行優化。可以參閱本章的第一小節
  • 建議開發者對所有自己研發的應用程序采用統一的證書
  • 當應用程序升級時,系統會比較新舊版本的證書是否一致。如果證書一致,升級可以順利進行,否則將失敗。當然,你也可以更換新版本的包名,這時系統會把它當成另外一個應用程序進行安裝
  • 采用相同簽名的應用程序允許被安排在同一個進程中運行
  • 采用相同簽名的應用程序間可以根據特殊權限進行代碼和數據的共享

 

接下來我們分別介紹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文件的路徑依據不同的操作系統會有差異:

  • LinuxOS X

    ~/.android/

  • Windows XP

    C:\Documents and Settings\<user>\.android\

  • Windows 7

    C:\Users\<user>\.android\

     

Release Mode

Release模式的簽名過程相對麻煩一些。

  • 獲取一個私人密鑰

    可以選擇使用keytool工具生成一個新的密鑰。需要特別注意,如果發布的應用程序是針對Google Play的話,那么證書的過期時間必須在20331022號以后。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 使用ADTExport功能導出合法的Apk

 

這時編譯系統會對Apk應用程序做更加詳細的檢查,包括安全(Security),效率(Performance),可用性(Usability)等多個方面。如果發現有錯誤,默認情況下會停止繼續導出Apk。你也可以在Preferences->Lint Error Checking中關閉這個檢查功能。如下圖所示:

1310 Lint Error Checking

 

由此可見Android系統提供了多種開發的方式。具體選擇哪一種,取決於開發者的習慣,以及項目的實際情況。而無論是命令行或是圖形界面操作,應用程序的編譯,打包,簽名,對齊這些操作的流程是不變的。

 

  1. 應用程序簽名源碼簡析

    這一小節我們來簡要分析下應用程序簽名的關鍵源碼。

    首先來比較下簽名前和簽名后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位前加上20,所以最終得到的每字節最大值都不會超過64。因為0~63ASCII碼是有不可見字符的,為了方便起見,算法還會將這64個數分別對應固定的可見ASCII

    比如經過SHA-1運算后,我們得到如下值:

    25FC4472EFCD2B3082682B20D6BC273B15012BB5,一共20個字節。

    3個字節的二進制碼為00100101(25) 11111100(FC) 01000100(44)

    我們在每6位前都加上兩位0,這樣就變成:

    00001001 00011111 00110001 00000100

    | | | |

十進制 9 31 49 4

根據BASE64表,數值931494分別對應可見ASCII字條中的J,f,xE,這和我們上面看到的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簽名機制保證了應用程序在安裝前沒有被惡意篡改,保護了開發者的權益,也同時為用戶選擇合法來源的應用程序提供了有利保障。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM