Android APK Signature Scheme v2 渠道包生成方案

Google在Android7.0(Nougat)推出了新的簽名方案,該方案能夠發現對 APK 的受保護部分進行的所有更改,從而有助于加快驗證速度并增強完整性保證。也就是說目前比較流行的渠道分包方案在APK Signature Scheme v2下將無法使用,雖然目前V2簽名方案Google并不是強制要求使用,但和傳統的簽名方案對比它有更快的簽名速度和更安全的保護,不排除新的簽名方案會被強制要求使用的可能,所以我們需要適配V2簽名。

目前在舊的簽名方式下能夠實現分包的方案有以下幾種:

1、先解壓apk,往assets目錄或其他目錄放置配置文件(也可以不需要解壓)。這種方式是最簡單,也是最安全的方式,別人不能篡改配置。缺點就是速度慢,渠道包一多需要等待很長一段時間。

2、在apk的meta-info文件夾下面放置一個配置文件,這種方式分包速度挺快,但讀取配置文件效率不高,需要初始化zip才能讀取。

3、第三種方式是 在apk的zip file comment 區域寫入數據,這種方式是目前比較流行的,也是效率最高的一種。

APK Signature Scheme v2方式將會對以上幾種方式產生什么影響了?我們先了解一下V2簽名方式。
使用 APK 簽名方案 v2 進行簽名時,會在 APK 文件中插入一個 APK Signing Block,該分塊位于“ZIP 中央目錄”部分之前并緊鄰該部分。在“APK 簽名分塊”內,v2 簽名和簽名者身份信息會存儲在APK簽名方案中。

簽名前和簽名后的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

簽名后的各個 APK 部分

APK 簽名方案 v2 負責保護第 1、3、4 部分的完整性,以及第 2 部分包含的“APK 簽名方案 v2 分塊”中的 signed data 分塊的完整性。之前所列出來的分包方案都將會影響1、3、4的完整性。

通過以上分析我們知道區塊1、3、4都是受完整性保護的,而區塊2是部分區域是不受保護的,我們是否可以從區塊2入手解決問題呢?那我們先看一下Google對區塊2格式的描述:


偏移  字節數   描述


@+0    8    這個Block的長度(本字段的長度不計算在內)


@+8   n    一組ID-value


@-24   8    這個Block的長度(和第一個字段一樣值)


@-16   16    魔數 “APK Sig Block 42”


區塊2中APK Signing Block是由以上幾部分組成,其中兩個部分記錄的是區塊的長度,一個部分是魔數,這些都是用做驗證,我們重點注意一下ID-value這部分,一組ID-value是由8字節長度標示+4字節ID+內容組成,Apk v2的簽名信息的ID為0x7109871a。也就是說可以有若干組ID-value,那我們是不是可以加一組ID-value用于記錄渠道信息呢?

我們先查看一下Android驗證簽名的機制:

APK 簽名驗證過程(新步驟以紅色顯示)

APK 驗證簽名信息步驟:

1、安裝APK時先判斷是否有v2簽名塊,如果有則驗證,驗證成功安裝,驗證失敗拒絕安裝。

2、未找到v2簽名塊,則走原有的v1驗證機制。

那么Android系統是如何驗證v2簽名模塊的呢?我們只能從源碼入手,查看源碼android.util.apk.ApkSignatureSchemeV2Verifier。從方法hasSignature開始查看

/**
     * Returns {@code true} if the provided APK contains an APK Signature Scheme V2 signature.
     *
     * <p><b>NOTE: This method does not verify the signature.</b>
     */
    public static boolean hasSignature(String apkFile) throws IOException {
        try (RandomAccessFile apk = new RandomAccessFile(apkFile, "r")) {
            findSignature(apk);
            return true;
        } catch (SignatureNotFoundException e) {
            return false;
        }
    }

這個方法只是提供了一個apk文件,再繼續查看findSignature方法

/**
     * Returns the APK Signature Scheme v2 block contained in the provided APK file and the
     * additional information relevant for verifying the block against the file.
     *
     * @throws SignatureNotFoundException if the APK is not signed using APK Signature Scheme v2.
     * @throws IOException if an I/O error occurs while reading the APK file.
     */
    private static SignatureInfo findSignature(RandomAccessFile apk)
            throws IOException, SignatureNotFoundException {
        // Find the ZIP End of Central Directory (EoCD) record.
        Pair<ByteBuffer, Long> eocdAndOffsetInFile = getEocd(apk);
        ByteBuffer eocd = eocdAndOffsetInFile.first;
        long eocdOffset = eocdAndOffsetInFile.second;
        if (ZipUtils.isZip64EndOfCentralDirectoryLocatorPresent(apk, eocdOffset)) {
            throw new SignatureNotFoundException("ZIP64 APK not supported");
        }

        // Find the APK Signing Block. The block immediately precedes the Central Directory.
        long centralDirOffset = getCentralDirOffset(eocd, eocdOffset);
        Pair<ByteBuffer, Long> apkSigningBlockAndOffsetInFile =
                findApkSigningBlock(apk, centralDirOffset);
        ByteBuffer apkSigningBlock = apkSigningBlockAndOffsetInFile.first;
        long apkSigningBlockOffset = apkSigningBlockAndOffsetInFile.second;

        // Find the APK Signature Scheme v2 Block inside the APK Signing Block.
        ByteBuffer apkSignatureSchemeV2Block = findApkSignatureSchemeV2Block(apkSigningBlock);

        return new SignatureInfo(
                apkSignatureSchemeV2Block,
                apkSigningBlockOffset,
                centralDirOffset,
                eocdOffset,
                eocd);
    }

讀懂這段代碼需要了解zip的格式,getEocd(apk)通過標識ox06054b50查找到Eocd的位移,從zip格式得知位移@+16 4個字節記錄的是中央目錄的起始位移,方法getCentralDirOffset就是通過該邏輯查找到中央目錄的。緊挨著中央目錄起始位移的就是APK Signing Block,再根據APK Signing Block區塊格式就能找到APK Signing Block起始位移。方法findApkSignatureSchemeV2Block是用用來查找v2簽名塊的信息的,我們重點看下這個方法。

private static ByteBuffer findApkSignatureSchemeV2Block(ByteBuffer apkSigningBlock)
            throws SignatureNotFoundException {
        checkByteOrderLittleEndian(apkSigningBlock);
        // FORMAT:
        // OFFSET       DATA TYPE  DESCRIPTION
        // * @+0  bytes uint64:    size in bytes (excluding this field)
        // * @+8  bytes pairs
        // * @-24 bytes uint64:    size in bytes (same as the one above)
        // * @-16 bytes uint128:   magic
        ByteBuffer pairs = sliceFromTo(apkSigningBlock, 8, apkSigningBlock.capacity() - 24);

        int entryCount = 0;
        while (pairs.hasRemaining()) {
            entryCount++;
            if (pairs.remaining() < 8) {
                throw new SignatureNotFoundException(
                        "Insufficient data to read size of APK Signing Block entry #" + entryCount);
            }
            long lenLong = pairs.getLong();
            if ((lenLong < 4) || (lenLong > Integer.MAX_VALUE)) {
                throw new SignatureNotFoundException(
                        "APK Signing Block entry #" + entryCount
                                + " size out of range: " + lenLong);
            }
            int len = (int) lenLong;
            int nextEntryPos = pairs.position() + len;
            if (len > pairs.remaining()) {
                throw new SignatureNotFoundException(
                        "APK Signing Block entry #" + entryCount + " size out of range: " + len
                                + ", available: " + pairs.remaining());
            }
            int id = pairs.getInt();
            if (id == APK_SIGNATURE_SCHEME_V2_BLOCK_ID) {
                return getByteBuffer(pairs, len - 4);
            }
            pairs.position(nextEntryPos);
        }

        throw new SignatureNotFoundException(
                "No APK Signature Scheme v2 block in APK Signing Block");
    }

這個方法是在遍歷APK Signing Block中ID-value,當查找到id == APK_SIGNATURE_SCHEME_V2_BLOCK_ID時候就返回內容,而APK_SIGNATURE_SCHEME_V2_BLOCK_ID的值是0x7109871a,也就是說查找到簽名信息后其余未知的ID-value選擇忽略。谷歌官方文檔APK 簽名方案 v2也有描述“在解譯該分塊時,應忽略 ID 未知的“ID-值”對。”,至此我們可以放心大膽的在該區域增加一組ID-value了。

接下來我們就往apk簽名塊中插入一組ID-value,以下是步驟:

1、根據標識(0x06054b50)找到EOCD位移。

2、EOCD起始位移16字節,找到記錄中央目錄的起始位移。

3、根據插入ID-value的大小修改EOCD中記錄中央目錄的位移。

4、根據中央目錄起始位移-24找到記錄簽名塊大小。

5、修改前后記錄簽名塊大小的值。

以下為插入一組渠道id的代碼:

    private static final long APK_SIG_BLOCK_MAGIC_HI = 0x3234206b636f6c42L;
    private static final long APK_SIG_BLOCK_MAGIC_LO = 0x20676953204b5041L;
    private static final int APK_SIG_BLOCK_MIN_SIZE = 32;
    private static final int ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET = 16;
    
    private final static int CHANNEL_FLAG = 0x12345678;   //渠道id標識
    private static void insertChannelId(RandomAccessFile apk,int adChannelId) {
        try{
            
            byte[] channelIdBuff = intToBytes2(adChannelId);
            int contentSize = channelIdBuff.length;
            
            //根據標識(0x06054b50)找到EOCD
            Pair<ByteBuffer, Long> eocdAndOffsetInFile = getEocd(apk);
            ByteBuffer eocd = eocdAndOffsetInFile.first;
            long eocdOffset = eocdAndOffsetInFile.second;
            
            if (ZipUtils.isZip64EndOfCentralDirectoryLocatorPresent(apk, eocdOffset)) {
                throw new SignatureNotFoundException("ZIP64 APK not supported");
            }
            int size = 8 + 4 + contentSize;
            long neweocdOffset = eocdOffset + size;
            
            //查找中央目錄位移
            long centralDirOffset = getCentralDirOffset(eocd, eocdOffset);
            long newCentralDirOffset = centralDirOffset + size;
            
            //查找簽名塊位移
            Pair<ByteBuffer, Long> apkSigningBlockAndOffsetInFile = findApkSigningBlock(apk, centralDirOffset);
            long newSigningBlockSize = apkSigningBlockAndOffsetInFile.first.capacity()-8 + size;
            
            
            //插入一組渠道 格式為[大小:標識:內容]
            int pos = (int) (apkSigningBlockAndOffsetInFile.second + 8);
            File tmp = File.createTempFile("tmp", null);//創建一個臨時文件存放數據;
            FileInputStream fis = new FileInputStream(tmp);
            FileOutputStream fos = new FileOutputStream(tmp);
            apk.seek(pos);//把指針移動到指定位置
            byte[] buf = new byte[1024];
            int len = -1;
            //把指定位置之后的數據寫入到臨時文件
            while((len = apk.read(buf)) != -1){
                fos.write(buf, 0, len);
            }
            apk.seek(pos);//再把指針移動到指定位置,插入追加的數據
            ByteBuffer buffer = ByteBuffer.allocate(size);
            buffer.order(ByteOrder.LITTLE_ENDIAN);
            buffer.putLong(size-8);  //大小
            buffer.putInt(CHANNEL_FLAG); //標識
            buffer.putInt(adChannelId); //內容
            apk.write(buffer.array());
            //再把臨時文件的數據寫回
            while((len = fis.read(buf)) > 0){
                apk.write(buf, 0, len);
            }
            
            apk.seek(neweocdOffset+ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET);
            buffer = ByteBuffer.allocate(4);
            buffer.order(ByteOrder.LITTLE_ENDIAN);
            buffer.clear();
            buffer.putInt((int) newCentralDirOffset); 
            apk.write(buffer.array());//修改eocd中央目錄位移
            
            apk.seek(apkSigningBlockAndOffsetInFile.second);//移到簽名塊頭
            buffer = ByteBuffer.allocate(8);
            buffer.order(ByteOrder.LITTLE_ENDIAN);
            buffer.clear();
            buffer.putLong(newSigningBlockSize);
            apk.write(buffer.array()); //修改簽名頭大小
            
            apk.seek(newCentralDirOffset-24);
            buffer.clear();
            buffer.putLong(newSigningBlockSize);
            apk.write(buffer.array()); //修改簽名尾大小
            
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

讀取插入的ID-value原理也是一樣,代碼就不貼出來了。

參考:

新一代開源Android渠道包生成工具Walle
APK 簽名方案 v2
Android Apk 動態寫入數據方案,用于添加渠道號,數據倒流等
Zip (file format)

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容