背景:目前項目在打渠道包的時候危队,采用的是AndroidManifest.xml配置渠道號,上線前一個個構(gòu)建出來,全部構(gòu)建完成耗時長達一個小時氧苍,這對于追求高效的工程師來講是無法忍受的。當(dāng)前有很多公司開源了多渠道打包的方案泛范,如美團的walle让虐,騰訊的VasDolly,還有技術(shù)達人個人研究的packer-ng-plugin罢荡,今天就來探索下多渠道打包的奧秘赡突。
在了解多渠道打包之前对扶,需要先了解下android的簽名方式,這樣才能知己知彼百戰(zhàn)不殆惭缰!
APK 簽名方案
Android 支持兩種應(yīng)用簽名方案浪南,一種是基于 JAR 簽名的方案(v1 方案),另一種是 Android Nougat (Android 7.0) 中引入的 APK 簽名方案 v2(v2 方案)漱受。
為了最大限度地提高兼容性络凿,應(yīng)同時采用 v1 和 v2 這兩種方案對應(yīng)用進行簽名。與只通過 v1 方案簽名的應(yīng)用相比昂羡,通過 v2 方案簽名的應(yīng)用能夠更快速地安裝到 Android Nougat 及更高版本的設(shè)備上絮记。更低版本的 Android 平臺會忽略 v2 簽名,這就需要應(yīng)用包含 v1 簽名虐先。
JAR 簽名(v1 方案)
從一開始怨愤,APK 簽名就是 Android 的一個有機部分。該方案基于簽名的JAR蛹批。
v1 簽名不保護 APK 的某些部分撰洗,例如 ZIP 元數(shù)據(jù)。APK 驗證程序需要處理大量不可信(尚未經(jīng)過驗證)的數(shù)據(jù)結(jié)構(gòu)腐芍,然后會舍棄不受簽名保護的數(shù)據(jù)差导。這會導(dǎo)致相當(dāng)大的受攻擊面。此外猪勇,APK 驗證程序必須解壓所有已壓縮的條目设褐,而這需要花費更多時間和內(nèi)存。為了解決這些問題埠对,Android 7.0 中引入了 APK 簽名方案 v2络断。
APK 簽名方案 v2(v2 方案)
Android 7.0 中引入了 APK 簽名方案 v2(v2 方案)。該方案會對 APK 的內(nèi)容進行哈希處理和簽名项玛,然后將生成的“APK 簽名分塊”插入到 APK 中貌笨。
多渠道打包方案
Gradle插件模式
- 在AndroidManifest.xml中添加渠道信息:
<meta-data
android:name="UMENG_CHANNEL"
android:value="${UMENG_CHANNEL_VALUE}"></meta-data>
- 2.通過Gradle Plugin提供的productFlavors標簽,添加渠道信息:
productFlavors.all { flavor ->
flavor.manifestPlaceholders = [UMENG_CHANNEL_VALUE: name]
}
flavorDimensions 'channel'
productFlavors {
"yingyongbao" {
dimension "channel"
}
"_360" {
dimension "channel"
}
Gradle編譯生成多渠道包時襟沮,會用不同的渠道信息替換AndroidManifest.xml中的占位符锥惋。在代碼中,也就可以直接讀取AndroidManifest.xml中的渠道信息了开伏。
ApplicationInfo appInfo = context.getPackageManager().getApplicationInfo(context.getPackageName(),
PackageManager.GET_META_DATA);
int channelId = appInfo.metaData.getInt("UMENG_CHANNEL");
當(dāng)前方案存在的問題膀跌,在開頭也說了,現(xiàn)在總結(jié)一下(引用VasDolly原文)
1.每生成一個渠道包固灵,都要重新執(zhí)行一遍構(gòu)建流程捅伤,效率太低,浪費時間巫玻,不太適合多渠道包的場景丛忆。
2.Gradle會為每個渠道包生成一個不同的BuildConfig.java類祠汇,記錄渠道信息,導(dǎo)致每個渠道包的DEX的CRC值都不同熄诡。一般情況下可很,這是沒有影響的。但是如果你使用了微信的Tinker熱補丁方案凰浮,那么就需要為不同的渠道包打不同的補丁我抠,這完全是不可以接受的。(因為Tinker是通過對比基礎(chǔ)包APK和新包APK生成差分補丁袜茧,然后再把補丁和基礎(chǔ)包APK一起合成新包APK菜拓。這就要求用于生成差分補丁的基礎(chǔ)包DEX和用于合成新包的基礎(chǔ)包DEX是完全一致的,即:每一個基礎(chǔ)渠道包的DEX文件是完全一致的惫周,不然就會合成失敵揪濉)
有沒有可能康栈,在不改變基礎(chǔ)包的情況下递递,快速的打出多渠道包呢?
APK本身也是個zip壓縮包啥么,多渠道打包是根據(jù)zip包的文件格式來切入的登舞,所以可以先來看下zip包的文件格式。
[local file header 1]
[local file header 1]
[file data 1]
[data descriptor 1]
.
.
.
[local file header n]
[file data n]
[data descriptor n]
[archive decryption header] (EFS)
[archive extra data record] (EFS)
[central directory]
[zip64 end of central directory record]
[zip64 end of central directory locator]
[end of central directory record]
zip主要由三部分組成:
壓縮源文件數(shù)據(jù)區(qū) | 中央目錄 | 目錄結(jié)束 |
---|---|---|
local file header + file data + data descriptor | central directory | end of central directory record |
記錄了文件名悬荣、壓縮算法菠秒、壓縮前后的文件大小、修改時間氯迂、CRC32值等 | 包含了多個central directory file header(和第一部分的local file header一一對應(yīng))践叠,每個中央目錄文件頭主要記錄了壓縮算法、注釋信息嚼蚀、對應(yīng)local file header的偏移量等 | 主要記錄了中央目錄大小禁灼、偏移量和ZIP注釋信息等 |
因基于v1方案的多渠道打包,會在目錄結(jié)束部分做文章轿曙,這里把End of central directory record(EOCD)結(jié)構(gòu)詳細結(jié)構(gòu)描述下弄捕。
目錄結(jié)束標識存在于整個歸檔包的結(jié)尾,用于標記壓縮的目錄數(shù)據(jù)的結(jié)束导帝。每個壓縮文件必須有且只有一個EOCD記錄守谓。
Offset | Bytes | Description | 譯 |
---|---|---|---|
0 | 4 | End of central directory signature = 0x06054b50 | 核心目錄結(jié)束標記(0x06054b50) |
4 | 2 | Number of this disk | 當(dāng)前磁盤編號 |
6 | 2 | number of the disk with the start of the central directory | 核心目錄開始位置的磁盤編號 |
8 | 2 | total number of entries in the central directory on this disk | 該磁盤上所記錄的核心目錄數(shù)量 |
10 | 2 | total number of entries in the central directory | 核心目錄結(jié)構(gòu)總數(shù) |
12 | 2 | Size of central directory (bytes) | 核心目錄的大小 |
16 | 4 | offset of start of central directory with respect to the starting disk number | 核心目錄開始位置相對于archive開始的位移 |
20 | 2 | .ZIP file comment length(n) | 注釋長度 |
22 | n | .ZIP Comment | 注釋內(nèi)容 |
有了以上對zip文件格式的認知,來看下android簽名方案以及如何從簽名方案下手來做多渠道打包您单。
基于V1簽名的多渠道打包方案
根據(jù)之前的V1簽名和校驗機制可知斋荞,V1簽名只會檢驗第一部分的所有壓縮文件,而不理會后兩部分內(nèi)容虐秦。因此平酿,只要把渠道信息寫入到后兩塊內(nèi)容就可以通過V1校驗讯檐,而EOCD的注釋字段無疑是最好的選擇湿痢。
上面也說過锡足,apk實際上就是普通的zip,在一個zip文件的最后允許寫入N個字符的注釋携栋,zip末尾兩個部分:2字節(jié)的的注釋長度+N個字節(jié)的注釋柳刮。
那么挖垛,我們只要把簽名內(nèi)容作為注釋寫入,再修改2字節(jié)的注釋長度即可秉颗。
那么我們怎么知道一個apk有沒有寫入這個渠道信息呢痢毒?
我們可以在文件文件末尾寫入一個特殊的字符串,當(dāng)我們讀取文件末尾為這個特殊的字符串蚕甥,即可認為該apk寫入了渠道信息哪替。該特殊字符串稱之為魔數(shù)
最終的渠道信息為:
渠道字符串+渠道字符串長度+魔數(shù)
在APK文件的注釋字段,添加渠道信息菇怀。
整個方案包括以下幾步:
1.復(fù)制APK
2.找到EOCD數(shù)據(jù)塊
3.修改注釋長度
4.添加渠道信息
5.添加渠道信息長度
6.添加魔數(shù)
接下來看如何從apk讀取這個渠道信息呢凭舶?
根據(jù)上面的了解,添加魔數(shù)的好處是方便從后向前讀取數(shù)據(jù)爱沟,定位渠道信息帅霜。 因此,讀取渠道信息包括以下幾步:
1.定位到魔數(shù)
2.向前讀兩個字節(jié)呼伸,確定渠道信息的長度LEN
3.繼續(xù)向前讀LEN字節(jié)身冀,就是渠道信息了。
用二進制編輯器打開打包好的Apk括享,看末尾的幾個字節(jié)搂根,如圖:
對這圖可以分析如下:
- 首先讀取8個字節(jié),對應(yīng)一個特殊字符串“l(fā)tlovezh”
- 往前兩個字節(jié)為02 00铃辖,對應(yīng)渠道信息長度剩愧,實際值為2.
- 再往前讀取2個字節(jié)為63 31,對照ASCII表澳叉,即可知為c1
讀取到渠道信息為:c1隙咸。
到這,渠道讀寫方案基本完了成洗,再此不再深入五督,有時間在結(jié)合代碼分析。
基于V2簽名的多渠道打包方案
使用 APK 簽名方案 v2 進行簽名時瓶殃,會在 APK 文件中插入一個 APK 簽名分塊充包,該分塊位于“ZIP 中央目錄”部分之前并緊鄰該部分。在“APK 簽名分塊”內(nèi),v2 簽名和簽名者身份信息會存儲在 APK 簽名方案 v2 分塊中基矮。
上圖中淆储,簽名前和簽名后的 APK
APK 簽名方案 v2 是在 Android 7.0 (Nougat) 中引入的。為了使 APK 可在 Android 6.0 (Marshmallow) 及更低版本的設(shè)備上安裝家浇,應(yīng)先使用 JAR 簽名功能對 APK 進行簽名本砰,然后再使用 v2 方案對其進行簽名。
APK 簽名分塊
為了保持與當(dāng)前 APK 格式向后兼容钢悲,v2 及更高版本的 APK 簽名會存儲在“APK 簽名分塊”內(nèi)点额,該分塊是為了支持 APK 簽名方案 v2 而引入的一個新容器。在 APK 文件中莺琳,“APK 簽名分塊”位于“ZIP 中央目錄”(位于文件末尾)之前并緊鄰該部分还棱。
該分塊包含多個“ID-值”對,所采用的封裝方式有助于更輕松地在 APK 中找到該分塊惭等。APK 的 v2 簽名會存儲為一個“ID-值”對珍手,其中 ID 為 0x7109871a。
為了保護 APK 內(nèi)容辞做,APK 包含以下 4 個部分:
ZIP 條目的內(nèi)容(從偏移量 0 處開始一直到“APK 簽名分塊”的起始位置)
APK 簽名分塊
ZIP 中央目錄
ZIP 中央目錄結(jié)尾
上圖中琳要,簽名后的各個 APK 部分
APK 簽名方案 v2 負責(zé)保護第 1、3凭豪、4 部分的完整性焙蹭,以及第 2 部分包含的“APK 簽名方案 v2 分塊”中的 signed data
分塊的完整性晒杈。
第 1嫂伞、3 和 4 部分的完整性通過其內(nèi)容的一個或多個摘要來保護,這些摘要存儲在 signed data
分塊中拯钻,而這些分塊則通過一個或多個簽名來保護帖努。
第 1、3 和 4 部分的摘要采用以下計算方式粪般,類似于兩級 Merkle 樹拼余。 每個部分都會被拆分成多個大小為 1 MB(220 個字節(jié))的連續(xù)塊。每個部分的最后一個塊可能會短一些亩歹。每個塊的摘要均通過字節(jié) 0xa5
的連接匙监、塊的長度(采用小端字節(jié)序的 uint32 值,以字節(jié)數(shù)計)和塊的內(nèi)容進行計算小作。頂級摘要通過字節(jié) 0x5a
的連接亭姥、塊數(shù)(采用小端字節(jié)序的 uint32 值)以及塊的摘要的連接(按照塊在 APK 中顯示的順序)進行計算。摘要以分塊方式計算顾稀,以便通過并行處理來加快計算速度达罗。
上圖中 APK 摘要
由于第 4 部分(ZIP 中央目錄結(jié)尾)包含“ZIP 中央目錄”的偏移量,因此該部分的保護比較復(fù)雜静秆。當(dāng)“APK 簽名分塊”的大小發(fā)生變化(例如粮揉,添加了新簽名)時巡李,偏移量也會隨之改變。因此扶认,在通過“ZIP 中央目錄結(jié)尾”計算摘要時侨拦,必須將包含“ZIP 中央目錄”偏移量的字段視為包含“APK 簽名分塊”的偏移量。
V2校驗流程
在 Android 7.0 中辐宾,可以根據(jù) APK 簽名方案 v2(v2 方案)或 JAR 簽名(v1 方案)驗證 APK阳谍。更低版本的平臺會忽略 v2 簽名,僅驗證 v1 簽名螃概。
上圖 中APK 簽名驗證過程(新步驟以紅色顯示)
APK 簽名方案 v2 驗證
- 找到“APK 簽名分塊”并驗證以下內(nèi)容:
- “APK 簽名分塊”的兩個大小字段包含相同的值矫夯。
- “ZIP 中央目錄結(jié)尾”緊跟在“ZIP 中央目錄”記錄后面。
- “ZIP 中央目錄結(jié)尾”之后沒有任何數(shù)據(jù)吊洼。
- 找到“APK 簽名分塊”中的第一個“APK 簽名方案 v2 分塊”训貌。如果 v2 分塊存在,則繼續(xù)執(zhí)行第 3 步冒窍。否則递沪,回退至使用 v1 方案驗證 APK。
- 對“APK 簽名方案 v2 分塊”中的每個
signer
執(zhí)行以下操作:- 從
signatures
中選擇安全系數(shù)最高的受支持signature algorithm ID
综液。安全系數(shù)排序取決于各個實現(xiàn)/平臺版本款慨。 - 使用
public key
并對照signed data
驗證signatures
中對應(yīng)的signature
。(現(xiàn)在可以安全地解析signed data
了谬莹。) - 驗證
digests
和signatures
中的簽名算法 ID 列表(有序列表)是否相同檩奠。(這是為了防止刪除/添加簽名。) - 使用簽名算法所用的同一種摘要算法計算 APK 內(nèi)容的摘要附帽。
- 驗證計算出的摘要是否與
digests
中對應(yīng)的digest
相同埠戳。 - 驗證
certificates
中第一個certificate
的 SubjectPublicKeyInfo 是否與public key
相同。
- 從
- 如果找到了至少一個
signer
蕉扮,并且對于每個找到的signer
整胃,第 3 步都取得了成功,APK 驗證將會成功喳钟。
注意:如果第 3 步或第 4 步失敗了屁使,則不得使用 v1 方案驗證 APK。
基于V2簽名的多渠道打包方案
在看一個問題奔则,V2簽名是怎么保證APK不被篡改的蛮寂?
首先,如果破壞者修改了APK文件的任何部分(簽名塊本身除外)应狱,那么APK的數(shù)據(jù)摘要就和“MF”數(shù)據(jù)塊中記錄的數(shù)據(jù)摘要不一致共郭,導(dǎo)致校驗失敗。 其次,如果破壞者同時修改了“MF”數(shù)據(jù)塊中的數(shù)據(jù)摘要除嘹,那么“MF”數(shù)據(jù)塊的數(shù)字簽名就和“SF”數(shù)據(jù)塊中記錄的數(shù)字簽名不一致写半,導(dǎo)致校驗失敗。 然后尉咕,如果破壞者使用自己的私鑰去加密生成“SF”數(shù)據(jù)塊叠蝇,那么使用開發(fā)者的公鑰去解密“SF”數(shù)據(jù)塊中的數(shù)字簽名就會失敗年缎; 最后悔捶,更進一步,若破壞者甚至替換了開發(fā)者公鑰单芜,那么使用數(shù)字證書中的公鑰校驗簽名塊中的公鑰就會失敗蜕该,這也正是數(shù)字證書的作用。
綜上所述洲鸠,任何對APK的修改堂淡,在安裝時都會失敗,除非對APK重新簽名扒腕。但是相同包名绢淀,不同簽名的APK也是不能同時安裝的。
基于V2簽名的多渠道打包方案
在上節(jié)V2簽名的校驗流程中瘾腰,有一個很重要的細節(jié):Android系統(tǒng)只會關(guān)注ID為0x7109871a的V2簽名塊皆的,并且忽略其他的ID-Value,同時V2簽名只會保護APK本身蹋盆,不包含簽名塊费薄,因此,k添加一個ID-Value怪嫌,存儲渠道信息义锥,即解決了基于V2簽名的多渠道打包問題。
方案包括以下幾步:
找到APK的EOCD塊
找到APK簽名塊
獲取已有的ID-Value Pair
添加包含渠道信息的ID-Value
基于所有的ID-Value生成新的簽名塊
修改EOCD的中央目錄的偏移量(上面已介紹過:修改EOCD的中央目錄偏移量岩灭,不會導(dǎo)致數(shù)據(jù)摘要校驗失敗)
用新的簽名塊替代舊的簽名塊赂鲤,生成帶有渠道信息的APK
實際上噪径,除了渠道信息,我們可以在APK簽名塊中添加任何輔助信息数初。
多渠道包的強校驗
那么如何保證通過這些方案生成的渠道包找爱,能夠在所有Android平臺上正確安裝呢?
Google提供了一個同時支持V1和V2簽名和校驗的工具:apksig泡孩。它包括一個apksigner
命令行和一個apksig
類庫车摄。其中前者就是Android SDK build-tools下面的命令行工具。而我們正是借助后面的apksig來進行渠道包強校驗,它可以保證渠道包在apk Minsdk ~ Maxsdk之間都校驗通過吮播。
至此变屁,多渠道打包方案基本看完了,還有其他方案意狠,暫未分析粟关,有興趣的可以自行研究。
參考文獻:
APK 簽名方案 v2
應(yīng)用簽名
VasDolly
walle
帶你了解騰訊開源的多渠道打包技術(shù) VasDolly源碼解析