作者 :李濤
ApkChannelPackage是一種高速多渠道打包工具。同一時候支持基於V1簽名和V2簽名進行多渠道打包。插件本身會自己主動檢測Apk使用的簽名方法,並選擇合適的多渠道打包方式。對使用者來說全然透明。
概述
眾所周知,由於國內Android應用分發市場的現狀。我們在公布APP時,一般須要生成多個渠道包。上傳到不同的應用市場。
這些渠道包須要包括不同的渠道信息,在APP和后台交互或者數據上報時,會帶上各自的渠道信息。這樣,我們就能統計到每一個分發市場的下載數、用戶數等重要數據。
普通的多渠道打包方案
既然我們須要進行多渠道打包。那我們就看下最常見的多渠道打包方案。
Android Gradle Plugin
Gradle Plugin本身提供了多渠道的打包策略:
首先,在AndroidManifest.xml中加入渠道信息占位符:
<meta-data
android:name="InstallChannel" android:value="${InstallChannel}" />
然后。通過Gradle Plugin提供的productFlavors標簽。加入渠道信息:
productFlavors{
"YingYongBao"{
manifestPlaceholders = [InstallChannel : "YingYongBao"]
}
"360"{
manifestPlaceholders = [InstallChannel : "360"]
}
}
這樣。Gradle編譯生成多渠道包時,會用不同的渠道信息替換AndroidManifest.xml中的占位符。我們在代碼中。也就能夠直接讀取AndroidManifest.xml中的渠道信息了。
可是。這樣的方式存在一些缺點:
1)每生成一個渠道包。都要又一次執行一遍構建流程,效率太低,僅僅適用於渠道較少的場景。
2)Gradle會為每一個渠道包生成一個不同的BuildConfig.java類,記錄渠道信息,導致每一個渠道包的DEX的CRC值都不同。普通情況下,這是沒有影響的。可是假設你使用了微信的Tinker熱補丁方案,那么就須要為不同的渠道包打不同的補丁,這全然是不能夠接受的。(由於Tinker是通過對照基礎包APK和新包APK生成差分補丁,然后再把補丁和基礎包APK一起合成新包APK。這就要求用於生成差分補丁的基礎包DEX和用於合成新包的基礎包DEX是全然一致的。即:每一個基礎渠道包的DEX文件是全然一致的,不然就會合成失敗)
ApkTool
ApkTool是一個逆向分析工具。能夠把APK解開,加入代碼后。又一次打包成APK。因此,基於ApkTool的多渠道打包方案分為下面幾步:
復制一份新的APK
通過ApkTool工具,解壓APK(apktool d origin.apk)
刪除已有簽名信息
加入渠道信息(能夠在APK的不論什么文件加入渠道信息)
通過ApkTool工具,又一次打包生成新APK(apktool b newApkDir)
又一次簽名
經過測試,這樣的方案全然是可行的。
長處:
不須要又一次構建新渠道包,僅須要復制改動就能夠了。而且由於是又一次簽名。所以同一時候支持V1和V2簽名。
缺點:
ApkTool工具不穩定,以前遇到過升級Gradle Plugin版本號后,低版本號ApkTool解壓APK失敗的情況。
生成新渠道包時。須要又一次解包、打包和簽名,而這幾步操作又是相對照較耗時的。
經過測試:生成企鵝電競10個渠道包須要16分鍾左右,盡管比Gradle Plugin方案降低非常多耗時。
可是若須要同一時候生成上百個渠道包,則須要幾個小時,顯然不適合渠道非常多的業務場景。
那有沒有一種方案:能夠在加入渠道信息后。不須要又一次簽名那?首先我們要了解一下APK的簽名和校驗機制。
數據摘要、數字簽名和數字證書
在進一步學習V1和V2簽名之前,我們有必要學習一下簽名相關的基礎知識。
數據摘要
數據摘要算法是一種能產生特定輸出格式的算法,其原理是依據一定的運算規則對原始數據進行某種形式的信息提取,被提取出的信息就是原始數據的消息摘要,也稱為數據指紋。
普通情況下,數據摘要算法具有下面特點:
不管輸入數據有多大(長),計算出來的數據摘要的長度總是固定的。比如:MD5算法計算出的數據摘要有128Bit。
普通情況下(不考慮碰撞的情況下),僅僅要原始數據不同,那么其相應的數據摘要就不會同樣。同一時候,僅僅要原始數據有不論什么改動,那么其數據摘要也會全然不同。
即:同樣的原始數據必有同樣的數據摘要。不同的原始數據,其數據摘要也必定不同。
不可逆性。即僅僅能正向提取原始數據的數據摘要。而無法從數據摘要中恢復出原始數據。
著名的摘要算法有RSA公司的MD5算法和SHA系列算法。
數字簽名和數字證書
數字簽名和數字證書是成對出現的,兩者不可分離(數字簽名主要用來校驗數據的完整性。數字證書主要用來確保公鑰的安全發放)。
要明白數字簽名的概念,必須要了解數據的加密、傳輸和校驗流程。普通情況下,要實現數據的可靠通信,須要解決下面兩個問題:
1.確定數據的來源是其真正的發送者。
2.確保數據在傳輸過程中。沒有被篡改,或者若被篡改了。能夠及時發現。
而數字簽名。就是為了解決這兩個問題而誕生的。
首先。數據的發送者須要先申請一對公私鑰對,並將公鑰交給數據接收者。
然后,若數據發送者須要發送數據給接收者。則首先要依據原始數據,生成一份數字簽名,然后把原始數據和數字簽名一起發送給接收者。
數字簽名由下面兩步計算得來:
1.計算發送數據的數據摘要
2.用私鑰對提取的數據摘要進行加密
這樣,數據接收者拿到的消息就包括了兩塊內容:
1.原始數據內容
2.附加的數字簽名
接下來。接收者就會通過下面幾步,校驗數據的真實性:
- 用同樣的摘要算法計算出原始數據的數據摘要。
- 用預先得到的公鑰解密數字簽名。
- 對照簽名得到的數據是否一致,假設一致,則說明數據沒有被篡改,否則數據就是臟數據了。
由於私鑰僅僅有發送者才有,所以其它人無法偽造數字簽名。這樣通過數字簽名就確保了數據的可靠傳輸。
綜上所述。數字簽名就是僅僅有發送者才干產生的別人無法偽造的一段數字串,這段數字串同一時候也是對發送者發送數據真實性的一個有效證明。
想法雖好。可是上面的整個流程,有一個前提。就是數據接收者能夠正確拿到發送者的公鑰。
假設接收者拿到的公鑰被篡改了,那么壞人就會被當成好人,而真正的數據發送者發送的數據則會被視作臟數據。
那怎么才干保證公鑰的安全性那?這就要靠數字證書來攻克了。
數字證書是由有公信力的證書中心(CA)頒發給申請者的證書,主要包括了:證書的公布機構、證書的有效期、申請者的公鑰、申請者信息、數字簽名使用的算法,以及證書內容的數字簽名。
可見,數字證書也用到了數字簽名技術。
僅僅只是簽名的內容是數據發送方的公鑰。以及一些其它證書信息。
這樣數據發送者發送的消息就包括了三部分內容:
- 原始數據內容
- 附加的數字簽名
- 申請的數字證書。
接收者拿到數據后,首先會依據CA的公鑰,解碼出發送者的公鑰。然后就與上面的校驗流程全然同樣了。
所以,數字證書主要攻克了公鑰的安全發放問題。
因此,包括數字證書的整個簽名和校驗流程例如以下圖所看到的:

V1簽名和多渠道打包方案
V1簽名機制
默認情況下,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三個文件。
V1校驗流程
在安裝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也是不能同一時候安裝的。
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簽名的多渠道打包方案
既然找到了突破口,那么基於V1簽名的多渠道打包方案就應運而生:在APK文件的凝視字段,加入渠道信息。
整個方案包括下面幾步:
- 復制APK
- 找到EOCD數據塊
- 改動凝視長度
- 加入渠道信息
- 加入渠道信息長度
- 加入魔數
加入渠道信息后的EOCD數據塊例如以下所看到的:

這里加入魔數的長處是方便從后向前讀取數據,定位渠道信息。
因此,讀取渠道信息包括下面幾步:
- 定位到魔數
- 向前讀兩個字節。確定渠道信息的長度LEN
- 繼續向前讀LEN字節,就是渠道信息了。
通過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簽名。
V2簽名和多渠道打包方案
為什么須要V2簽名
從前面的V1簽名介紹,能夠知道V1存在兩個弊端:
1)MANIFEST.MF中的數據摘要是基於原始未壓縮文件計算的。因此在校驗時,須要先解壓出原始文件,才干進行校驗。而解壓操作無疑是耗時的。
2) 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的安裝速度還是提升不少的。
V2簽名機制
不同於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也優化了計算數據摘要的算法,使得能夠並行計算。例如以下圖所看到的:

數據摘要的計算包括下面幾步:
首先,將上述APK中文件內容塊、中央文件夾、EOCD依照1MB大小切割成一些小塊。
然后,計算每一個小塊的數據摘要。基礎數據是0xa5 + 塊字節長度 + 塊內容。
最后,計算總體的數據摘要。基礎數據是0x5a + 數據塊的數量 + 每一個數據塊的摘要內容。
這樣,每一個數據塊的數據摘要就能夠並行計算。加快了V2簽名和校驗的速度。
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的中央文件夾偏移量。
事實上也非常easy,原來Android系統在校驗APK的數據摘要時。首先會把EOCD的中央文件夾偏移量替換成簽名塊的偏移量。然后再計算數據摘要。而簽名塊的偏移量不就是v2簽名之前的中央文件夾偏移量嘛!。。,因此,這樣計算出的數據摘要就和“MF”數據塊中的數據摘要全然一致了。詳細代碼邏輯,可參考ApkSignatureSchemeV2Verifier.java的416 ~ 420行。
基於V2簽名的多渠道打包方案
在上節V2簽名的校驗流程中。有一個非常重要的細節:Android系統僅僅會關注ID為0x7109871a的V2簽名塊。而且忽略其它的ID-Value。同一時候V2簽名僅僅會保護APK本身,不包括簽名塊。
因此,基於V2簽名的多渠道打包方案就應運而生:在APK簽名塊中加入一個ID-Value,存儲渠道信息。
整個方案包括下面幾步:
- 找到APK的EOCD塊
- 找到APK簽名塊
- 獲取已有的ID-Value Pair
- 加入包括渠道信息的ID-Value
- 基於全部的ID-Value生成新的簽名塊
- 改動EOCD的中央文件夾的偏移量(上面已介紹過:改動EOCD的中央文件夾偏移量,不會導致數據摘要校驗失敗)
- 用新的簽名塊替代舊的簽名塊,生成帶有渠道信息的APK
實際上,除了渠道信息,我們能夠在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和它們之間的簡單對照。
| 多渠道打包工具對照 | ApkChannelPackage | packer-ng-plugin | Walle |
|---|---|---|---|
| V1簽名方案 | 支持 | 支持 | 不支持 |
| V2簽名方案 | 支持 | 不支持 | 支持 |
| 帶有凝視的APK | 支持 | 不支持 | 不支持 |
| 依據已有APK生成渠道包 | 支持 | 不支持 | 不支持 |
| 命令行工具 | 不支持 | 支持 | 支持 |
| 強校驗 | 支持 | 不支持 | 支持 |
這里我之所以同一時候支持V1和V2簽名方案,主要是操心興許Android平台加強簽名校驗機制,導致V2多渠道打包方案行不通,能夠無痛切換到V1簽名方案。興許我也會盡快支持命令行工具。
ApkChannelPackage插件接入
詳細的接入流程可參考APKChannelPackage插件接入文檔。
相關推薦
Unity編譯Android的原理解析和apk打包分析
【騰訊雲的1001種玩法】安卓加固在騰訊雲上的使用(附反編譯結果)
Android動態庫壓縮殼的實現
此文已由作者授權騰訊雲技術社區公布,轉載請注明文章出處
原文鏈接:https://www.qcloud.com/community/article/146038
獲取很多其它騰訊海量技術實踐干貨。歡迎大家前往騰訊雲技術社區
