新一代開源Android渠道包生成工具Walle
在Android 7.0(Nougat)推出了新的應用簽名方案APK Signature Scheme v2後,之前快速生成渠道包的方式(美團Android自動化之旅—生成渠道包)已經行不通了,在此應用簽名方案下如何快速生成渠道包呢?
本文會對新的應用簽名方案APK Signature Scheme v2以及新一代渠道生成工具進行詳細深入的介紹。
新的應用簽名方案APK Signature Scheme v2
Android 7.0(Nougat)引入一項新的應用簽名方案APK Signature Scheme v2,它是一個對全文件進行簽名的方案,能提供更快的應用安裝時間、對未授權APK文件的更改提供更多保護,在默認情況下,Android Gradle 2.2.0插件會使用APK Signature Scheme v2和傳統簽名方案來簽署你的應用。
下面以 新的應用簽名方案 來指APK Signature Scheme v2。
目前該方案不是強制性的,在 build.gradle 添加 v2SigningEnabled false ,就能使用傳統簽名方案來簽署我們的應用(見下面的代碼片段)。
android {n ...n defaultConfig { ... }n signingConfigs {n release {n storeFile file("myreleasekey.keystore")n storePassword "password"n keyAlias "MyReleaseKey"n keyPassword "password"n v2SigningEnabled falsen }n }n }n
但新的應用簽名方案有著良好的向後兼容性,能完全兼容低於Android 7.0(Nougat)的版本。對比舊簽名方案,它有更快的驗證速度和更安全的保護,因此新的應用簽名方案可能會被採納成一個強制配置,筆者認為現在有必要對現有的渠道包生成方式進行檢查、升級或改造來支持新的應用簽名方案。
新的簽名方案對已有的渠道生成方案有什麼影響呢?下圖是新的應用簽名方案和舊的簽名方案的一個對比:
新的簽名方案會在ZIP文件格式的 Central Directory 區塊所在文件位置的前面添加一個APK Signing Block區塊,下面按照ZIP文件的格式來分析新應用簽名方案簽名後的APK包。
整個APK(ZIP文件格式)會被分為以下四個區塊:
- Contents of ZIP entries(from offset 0 until the start of APK Signing Block)
- APK Signing Block
- ZIP Central Directory
- ZIP End of Central Directory
新應用簽名方案的簽名信息會被保存在區塊2(APK Signing Block)中, 而區塊1(Contents of ZIP entries)、區塊3(ZIP Central Directory)、區塊4(ZIP End of Central Directory)是受保護的,在簽名後任何對區塊1、3、4的修改都逃不過新的應用簽名方案的檢查。
之前的渠道包生成方案是通過在META-INF目錄下添加空文件,用空文件的名稱來作為渠道的唯一標識,之前在META-INF下添加文件是不需要重新簽名應用的,這樣會節省不少打包的時間,從而提高打渠道包的速度。但在新的應用簽名方案下META-INF已經被列入了保護區了,向META-INF添加空文件的方案會對區塊1、3、4都會有影響,新應用簽名方案簽署的應用經過我們舊的生成渠道包方案處理後,在安裝時會報以下錯誤:
Failure [INSTALL_PARSE_FAILED_NO_CERTIFICATES: nFailed to collect certificates from base.apk: META-INF/CERT.SF indicates base.apk is signed using APK Signature Scheme v2, nbut no such signature was found. Signature stripped?]n
目前另外一種比較流行的渠道包快速生成方案(往APK中添加ZIP Comment)也因為上述原因,無法在新的應用簽名方案下進行正常工作。
如果新的應用簽名方案後續改成強制要求,那我們現有的生成渠道包的方式就會無法工作,那我們難道要退回到解放前,通過傳統的方式(例如:使用APKTool逆向工具、採用Flavor + BuildType等比較耗時的方案來進行渠道包打包)來生成支持新應用簽名方案的渠道包嗎?
如果只有少量渠道包的場景下,這種耗時時長還能夠勉強接受。但是目前我們有將近900個渠道,如果採用傳統方式打完所有的渠道包需要近3個小時,這是不能接受的。
那我們有沒有其他更好的渠道包生成方式,既能支持新的應用簽名方案,又能體驗毫秒級的打包耗時呢?我們來分析一下新方案中的區塊2——Block。
可擴展的APK Signature Scheme v2 Block
通過上面的描述,可以看出因為APK包的區塊1、3、4都是受保護的,任何修改在簽名後對它們的修改,都會在安裝過程中被簽名校驗檢測失敗,而區塊2(APK Signing Block)是不受簽名校驗規則保護的,那是否可以在這個不受簽名保護的區塊2(APK Signing Block)上做文章呢?我們先來看看對區塊2格式的描述:
偏移位元組數描述@+08這個Block的長度(本欄位的長度不計算在內)@+8n一組ID-value@-248這個Block的長度(和第一個欄位一樣值)@-1616魔數 「APK Sig Block 42」區塊2中APK Signing Block是由這幾部分組成:2個用來標示這個區塊長度的8位元組 + 這個區塊的魔數(APK Sig Block 42)+ 這個區塊所承載的數據(ID-value)。
我們重點來看一下這個ID-value,它由一個8位元組的長度標示+4位元組的ID+它的負載組成。V2的簽名信息是以ID(0x7109871a)的ID-value來保存在這個區塊中,不知大家有沒有注意這是一組ID-value,也就是說它是可以有若干個這樣的ID-value來組成,那我們是不是可以在這裡做一些文章呢?
為了驗證我們的想法,先來看看新的應用簽名方案是怎麼驗證簽名信息的,見下圖:
通過上圖可以看出新的應用簽名方案的驗證過程:
- 尋找APK Signing Block,如果能夠找到,則進行驗證,驗證成功則繼續進行安裝,如果失敗了則終止安裝
- 如果未找到APK Signing Block,則執行原來的簽名驗證機制,也是驗證成功則繼續進行安裝,如果失敗了則終止安裝
那Android應用在安裝時新的應用簽名方案是怎麼進行校驗的呢?筆者通過翻閱Android相關部分的源碼,發現下面代碼段是用來處理上面所說的ID-value的:
public static ByteBuffer findApkSignatureSchemeV2Block(n ByteBuffer apkSigningBlock,n Result result) throws SignatureNotFoundException {n checkByteOrderLittleEndian(apkSigningBlock);n // FORMAT:n // OFFSET DATA TYPE DESCRIPTIONn // * @+0 bytes uint64: size in bytes (excluding this field)n // * @+8 bytes pairsn // * @-24 bytes uint64: size in bytes (same as the one above)n // * @-16 bytes uint128: magicn ByteBuffer pairs = sliceFromTo(apkSigningBlock, 8, apkSigningBlock.capacity() - 24);nn int entryCount = 0;n while (pairs.hasRemaining()) {n entryCount++;n if (pairs.remaining() < 8) {n throw new SignatureNotFoundException(n "Insufficient data to read size of APK Signing Block entry #" + entryCount);n }n long lenLong = pairs.getLong();n if ((lenLong < 4) || (lenLong > Integer.MAX_VALUE)) {n throw new SignatureNotFoundException(n "APK Signing Block entry #" + entryCountn + " size out of range: " + lenLong);n }n int len = (int) lenLong;n int nextEntryPos = pairs.position() + len;n if (len > pairs.remaining()) {n throw new SignatureNotFoundException(n "APK Signing Block entry #" + entryCount + " size out of range: " + lenn + ", available: " + pairs.remaining());n }n int id = pairs.getInt();n if (id == APK_SIGNATURE_SCHEME_V2_BLOCK_ID) {n return getByteBuffer(pairs, len - 4);n }n result.addWarning(Issue.APK_SIG_BLOCK_UNKNOWN_ENTRY_ID, id);n pairs.position(nextEntryPos);n }nn throw new SignatureNotFoundException(n "No APK Signature Scheme v2 block in APK Signing Block");n }n
上述代碼中關鍵的一個位置是 if (id == APK_SIGNATURE_SCHEME_V2_BLOCK_ID) {return getByteBuffer(pairs, len - 4);},通過源代碼可以看出Android是通過查找ID為 APK_SIGNATURE_SCHEME_V2_BLOCK_ID = 0x7109871a 的ID-value,來獲取APK Signature Scheme v2 Block,對這個區塊中其他的ID-value選擇了忽略。
在APK Signature Scheme v2中沒有看到對無法識別的ID,有相關處理的介紹。
當看到這裡時,我們可不可以設想一下,提供一個自定義的ID-value並寫入該區域,從而為快速生成渠道包服務呢?
怎麼向ID-value中添加信息呢?通過閱讀ZIP的文件格式和APK Signing Block格式的描述,筆者通過編寫下面的代碼片段進行驗證,發現通過在已經被新的應用簽名方案簽名後的APK中添加自定義的ID-value,是不需要再次經過簽名就能安裝的,下面是部分代碼片段。
public void writeApkSigningBlock(DataOutput dataOutput) {n long length = 24;n for (int index = 0; index < payloads.size(); ++index) {n ApkSigningPayload payload = payloads.get(index);n byte[] bytes = payload.getByteBuffer();n length += 12 + bytes.length;n }nn ByteBuffer byteBuffer = ByteBuffer.allocate(Long.BYTES);n byteBuffer.order(ByteOrder.LITTLE_ENDIAN);n byteBuffer.putLong(length);n dataOutput.write(byteBuffer.array());nn for (int index = 0; index < payloads.size(); ++index) {n ApkSigningPayload payload = payloads.get(index);n byte[] bytes = payload.getByteBuffer();nn byteBuffer = ByteBuffer.allocate(Integer.BYTES);n byteBuffer.order(ByteOrder.LITTLE_ENDIAN);n byteBuffer.putInt(payload.getId());n dataOutput.write(byteBuffer.array());nn dataOutput.write(bytes);n }n ...n }n
新一代渠道包生成工具
到這裡為止一個新的渠道包生成方案逐步清晰了起來,下面是新一代渠道包生成工具的描述:
- 對新的應用簽名方案生成的APK包中的ID-value進行擴展,提供自定義ID-value(渠道信息),並保存在APK中
- 而APK在安裝過程中進行的簽名校驗,是忽略我們添加的這個ID-value的,這樣就能正常安裝了
- 在App運行階段,可以通過ZIP的EOCD(End of central directory)、Central directory等結構中的信息(會涉及ZIP格式的相關知識,這裡不做展開描述)找到我們自己添加的ID-value,從而實現獲取渠道信息的功能
新一代渠道包生成工具完全是基於ZIP文件格式和APK Signing Block存儲格式而構建,基於文件的二進位流進行處理,有著良好的處理速度和兼容性,能夠滿足不同的語言編寫的要求,目前筆者採用的是Java+Groovy開發, 該工具主要有四部分組成:
- 用於寫入ID-value信息的Java類庫
- Gradle構建插件用來和Android的打包流程進行結合
- 用於讀取ID-value信息的Java類庫
- 用於供com.android.application使用的讀取渠道信息的AAR
這樣,每打一個渠道包只需複製一個APK,然後在APK中添加一個ID-value即可,這種打包方式速度非常快,對一個30M大小的APK包只需要100多毫秒(包含文件複製時間)就能生成一個渠道包,而在運行時獲取渠道信息只需要大約幾毫秒的時間。
這個項目我們取名為Walle(瓦力),已經開源,項目的Github地址是: Meituan-Dianping/walle (求Issue、PR、Star)。希望業內有類似需求的團隊能夠在APK Signature Scheme V2簽名下愉快地生成渠道包,同時也期待大家一起對該項目進行完善和優化。
總結
以上就是我們對新的應用簽名方案進行的分析,並根據它所帶來的文件存儲格式上的變化,找到了可以利用的ID-value,然後基於這個ID-value來構建我們新一代渠道包生成工具。
新一代渠道包生成工具能夠滿足新應用簽名方案對安全性的要求,同時也能滿足對渠道包打包時間的要求,至此大家生成渠道包的方式需要升級了!
文章中引用的圖片來源於:https://source.android.com/security/apksigning/v2.html
參考文獻
- APK Signature Scheme v2
- ApkSigner的源代碼
- apksig的源代碼
- ZIP Format
不想錯過技術博客更新?想給文章評論、和作者互動?第一時間獲取技術沙龍信息?
請關注我們的官方微信公眾號「美團點評技術團隊」。
推薦閱讀:
※Center of Open Science與Open Science Framework
※Jordan Hubbard 在蘋果公司 12 年做了什麼,為何加入 iXsystems?
※如何僅憑 README 就獲得上千 star?
※紅帽如何用自己證明了什麼是開放?