新一代開源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文件格式)會被分為以下四個區塊:

  1. Contents of ZIP entries(from offset 0 until the start of APK Signing Block)
  2. APK Signing Block
  3. ZIP Central Directory
  4. 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來組成,那我們是不是可以在這裡做一些文章呢?

為了驗證我們的想法,先來看看新的應用簽名方案是怎麼驗證簽名信息的,見下圖:

通過上圖可以看出新的應用簽名方案的驗證過程:

  1. 尋找APK Signing Block,如果能夠找到,則進行驗證,驗證成功則繼續進行安裝,如果失敗了則終止安裝
  2. 如果未找到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

新一代渠道包生成工具

到這裡為止一個新的渠道包生成方案逐步清晰了起來,下面是新一代渠道包生成工具的描述:

  1. 對新的應用簽名方案生成的APK包中的ID-value進行擴展,提供自定義ID-value(渠道信息),並保存在APK中
  2. 而APK在安裝過程中進行的簽名校驗,是忽略我們添加的這個ID-value的,這樣就能正常安裝了
  3. 在App運行階段,可以通過ZIP的EOCD(End of central directory)、Central directory等結構中的信息(會涉及ZIP格式的相關知識,這裡不做展開描述)找到我們自己添加的ID-value,從而實現獲取渠道信息的功能

新一代渠道包生成工具完全是基於ZIP文件格式和APK Signing Block存儲格式而構建,基於文件的二進位流進行處理,有著良好的處理速度和兼容性,能夠滿足不同的語言編寫的要求,目前筆者採用的是Java+Groovy開發, 該工具主要有四部分組成:

  1. 用於寫入ID-value信息的Java類庫
  2. Gradle構建插件用來和Android的打包流程進行結合
  3. 用於讀取ID-value信息的Java類庫
  4. 用於供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來構建我們新一代渠道包生成工具。

新一代渠道包生成工具能夠滿足新應用簽名方案對安全性的要求,同時也能滿足對渠道包打包時間的要求,至此大家生成渠道包的方式需要升級了!

文章中引用的圖片來源於:source.android.com/secu

參考文獻

  1. APK Signature Scheme v2
  2. ApkSigner的源代碼
  3. apksig的源代碼
  4. ZIP Format

不想錯過技術博客更新?想給文章評論、和作者互動?第一時間獲取技術沙龍信息?

請關注我們的官方微信公眾號「美團點評技術團隊」。


推薦閱讀:

Center of Open Science與Open Science Framework
Jordan Hubbard 在蘋果公司 12 年做了什麼,為何加入 iXsystems?
如何僅憑 README 就獲得上千 star?
紅帽如何用自己證明了什麼是開放?

TAG:开源 | 前端开发 | Android开发 |