多渠道打包工具Walle源码分析
一、背景
首先了解多渠道打包工具Walle之前,我们需要先明确一个概念,什么是渠道包。
我们要知道在国内有无数大大小小的APP Store,每一个APP Store就是一个渠道。当我们把APP上传到APP Store上的时候,我们如何知道用户在那个渠道下载我们的APP呢?如果单凭渠道供应商自己给的话,那无疑会带来不可知的损失,当然除了这个原因,我们还有别的等等。
所以通俗的来说,我们需要一种方法来对我们的APK在不改变功能的情况下进行标记,来达到区分的目的。
二、如何给APK打标记
google官方为我们提供了注入meta-data、flavor等方法进行区分,但无疑我们每次去获取不同渠道的APK都面临一个重新打apk的问题。当渠道多的时候,这样大量重复无用的工作无疑是耗时且繁琐的。所以我们需要一种方法,让我们只打一个包,并在这个包的基础上进行区分,来达到获取不同渠道包的功能。
我们都知道编译获取APK后,会进行签名的操作,一旦我们在签名后进行修改apk包内容的修改,那么无疑会破坏签名,导致apk无法安装。所以我们需要一个折中的办法。
三、渠道打包原理分析
通过上面的分析,我们知道打渠道包,需要做到如下的要素。避免重新打包、避免重新签名。第一条是必须去避免的,因为太过耗时。第二条签名过程在渠道包操作较多的时候也是一笔耗时操作,但不属于必须优化项。
既然,我们的渠道包打包流程是在我们出包之后,那么我们则必须去了解Android的签名机制,也就是我们平时签名所勾选的v1、v2和新出的v3签名。
传统的v1签名是这样的:
我们的APK在签名后,通过解压,我们能够发现在APK中出现了一个META-INF的文件夹,它包含了三个文件MANIFEST.MF、CERT.RSA、CERT.SF三个文件,这三个文件就包含我们v1签名的签名信息。
我们本节的重点不是在签名上,所以我就简单的来说一下,这三个文件的作用是什么。
MANIFEST.MF:查看文件内容,我们可以看到这个文件记录的是对每一个文件内容做一次SHA1算法,就是计算出文件的摘要信息,然后用Base64进行编码。
CERT.SF:对MANIFEST.MF的每一个条目进行一次相同的操作
CERT.RSA:这个文件是个二进制文件,也就是用我们的签名文件对 CERT.SF进行签名。
所以我们可以发现,上述三个文件保存了我们所有的签名信息。那么我们可以发现,他却没有验证META-INF文件夹中的信息,所以我们完全可以通过在META-INF文件夹中添加不同的文件,然后在APP中读取,来进行区分。这样避免了重复签名。当然,在v2签名出来之后,v2签名对整个apk,进行了签名。因为我们一般会
同时v1、v2签名,所以自然META-INF也需要验证。再用相同的方法,必然会报错。除非删除签名信息后,重新签名。
v2签名:
先放一张v2签名经典的原理图。
我们可以知道v2签名在原APK的基础上添加了APK SIgning Block区域用来保护其他三跨块区域,所以我们可以很明显的知道,如果我们在这块区域中进行修改,是不会进行相关的签名校验的。Walle正是利用这种方式来进行的相关修改
所以在解析 APK 时,首先要通过以下方法找到“ZIP 中央目录”的起始位置:在文件末尾找到“ZIP 中央目录结尾”记录,然后从该记录中读取“中央目录”的起始偏移量。通过 magic
值,可以快速确定“中央目录”前方可能是“APK 签名分块”。然后,通过 size of block
值,可以高效地找到该分块在文件中的起始位置。
图1
v3 签名
Android 9 支持 APK 密钥轮转,这使应用能够在 APK 更新过程中更改其签名密钥。为了实现轮转,APK 必须指示新旧签名密钥之间的信任级别。为了支持密钥轮转,我们将 APK 签名方案从 v2 更新为 v3,以允许使用新旧密钥。v3 在 APK 签名分块中添加了有关受支持的 SDK 版本和 proof-of-rotation 结构的信息。
v3签名格式与v2类似。APK 的 v3 签名会存储为一个“ID-值”对,其中 ID 为 0xf05368c0。
从walle的commit记录来看,我们了解到walle目前已经支持v3签名写渠道。可以看到代码中添加了generateApkSigningBlock(https://android.googlesource.com/platform/tools/apksig/+/master/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtils.java)这一部分的代码 为什么这么做能够兼容v3,还需要再去研究
四、源码分析
下面的代码是walle中读取渠道信息所用的比较重要的地方。
final class AplUtil {
private ApkUtil(){
super(); } /** * APK Signing Block Magic Code: magic “APK Sig Block 42” (16 bytes) * "APK Sig Block 42" : 41 50 4B 20 53 69 67 20 42 6C 6F 63 6B 20 34 32 */ public static final long APK_SIG_BLOCK_MAGIC_HI = 0x3234206b636f6c42L; // LITTLE_ENDIAN, High public static final long APK_SIG_BLOCK_MAGIC_LO = 0x20676953204b5041L; // LITTLE_ENDIAN, Low private static final int APK_SIG_BLOCK_MIN_SIZE = 32; /* The v2 signature of the APK is stored as an ID-value pair with ID 0x7109871a (https://source.android.com/security/apksigning/v2.html#apk-signing-block) */ public static final int APK_SIGNATURE_SCHEME_V2_BLOCK_ID = 0x7109871a; /** * The padding in APK SIG BLOCK (V3 scheme introduced) * See https://android.googlesource.com/platform/tools/apksig/+/master/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtils.java */ public static final int VERITY_PADDING_BLOCK_ID = 0x42726577; public static final int ANDROID_COMMON_PAGE_ALIGNMENT_BYTES = 4096; // Our Channel Block ID 签名校验区的值是通过ID-value的键值对写进去的,这里walle的渠道key就是下面的值 public static final int APK_CHANNEL_BLOCK_ID = 0x71777777; public static final String DEFAULT_CHARSET = "UTF-8"; private static final int ZIP_EOCD_REC_MIN_SIZE = 22; private static final int ZIP_EOCD_REC_SIG = 0x06054b50; private static final int UINT16_MAX_VALUE = 0xffff; private static final int ZIP_EOCD_COMMENT_LENGTH_FIELD_OFFSET = 20;
//EOCD获取长度,下面的函数就是获取我们所需要的EOCD区域,这里面包含了Central Dir的偏移量,所以很好计算 第四步
//第 4 部分(ZIP 中央目录结尾)包含“ZIP 中央目录”的偏移量 public static long getCommentLength(final FileChannel fileChannel) throws IOException {
//这里的注释将EOCD结构描述的很详细 // End of central directory record (EOCD) // Offset Bytes Description[23] // 0 4 End of central directory signature = 0x06054b50 // 4 2 Number of this disk // 6 2 Disk where central directory starts // 8 2 Number of central directory records on this disk // 10 2 Total number of central directory records // 12 4 Size of central directory (bytes) // 16 4 Offset of start of central directory, relative to start of archive //这里包含了Central Dir的偏移量 // 20 2 Comment length (n) // 22 n Comment // For a zip with no archive comment, the // end-of-central-directory record will be 22 bytes long, so // we expect to find the EOCD marker 22 bytes from the end. final long archiveSize = fileChannel.size(); if (archiveSize < ZIP_EOCD_REC_MIN_SIZE) { throw new IOException("APK too small for ZIP End of Central Directory (EOCD) record"); }
//EOCD位于apk的最后方,他的起始为一个magic魔数,所以我们只要找到这个魔数,就可以确定位置了 // ZIP End of Central Directory (EOCD) record is located at the very end of the ZIP archive. // The record can be identified by its 4-byte signature/magic which is located at the very // beginning of the record. A complication is that the record is variable-length because of // the comment field.
//这里解释了下面计算EOCD区域的大小 // The algorithm for locating the ZIP EOCD record is as follows. We search backwards from // end of the buffer for the EOCD record signature. Whenever we find a signature, we check // the candidate record's comment length is such that the remainder of the record takes up // exactly the remaining bytes in the buffer. The search is bounded because the maximum // size of the comment field is 65535 bytes because the field is an unsigned 16-bit number.
//最大不超过16bit 这个没太懂是从哪里得到的 final long maxCommentLength = Math.min(archiveSize - ZIP_EOCD_REC_MIN_SIZE, UINT16_MAX_VALUE); final long eocdWithEmptyCommentStartPosition = archiveSize - ZIP_EOCD_REC_MIN_SIZE; for (int expectedCommentLength = 0; expectedCommentLength <= maxCommentLength; expectedCommentLength++) { final long eocdStartPos = eocdWithEmptyCommentStartPosition - expectedCommentLength; final ByteBuffer byteBuffer = ByteBuffer.allocate(4); fileChannel.position(eocdStartPos); fileChannel.read(byteBuffer); byteBuffer.order(ByteOrder.LITTLE_ENDIAN); //这个循环就很简单了,0x06054b50不断去找EOCD的魔术,找到了他的位置就是EOCD的起始位置 if (byteBuffer.getInt(0) == ZIP_EOCD_REC_SIG) { final ByteBuffer commentLengthByteBuffer = ByteBuffer.allocate(2); fileChannel.position(eocdStartPos + ZIP_EOCD_COMMENT_LENGTH_FIELD_OFFSET); fileChannel.read(commentLengthByteBuffer); commentLengthByteBuffer.order(ByteOrder.LITTLE_ENDIAN); //这里找到起始位置后,就可以知道我们 EOCD的实际大小了 根据上面那个记录 final int actualCommentLength = commentLengthByteBuffer.getShort(0); if (actualCommentLength == expectedCommentLength) { return actualCommentLength; } } } throw new IOException("ZIP End of Central Directory (EOCD) record not found"); } //找到CentralDir 的起始位置,第二步 public static long findCentralDirStartOffset(final FileChannel fileChannel) throws IOException {
//这里需要获取ECOD区域的大小,通过apk大小减去这一部分的大小 return findCentralDirStartOffset(fileChannel, getCommentLength(fileChannel)); }
//这里是通过获取到的EOCD区域的大小去计算获取CentralDir的偏移量,第五步 public static long findCentralDirStartOffset(final FileChannel fileChannel, final long commentLength) throws IOException { // End of central directory record (EOCD) // Offset Bytes Description[23] // 0 4 End of central directory signature = 0x06054b50 // 4 2 Number of this disk // 6 2 Disk where central directory starts // 8 2 Number of central directory records on this disk // 10 2 Total number of central directory records // 12 4 Size of central directory (bytes) // 16 4 Offset of start of central directory, relative to start of archive // 20 2 Comment length (n) // 22 n Comment // For a zip with no archive comment, the // end-of-central-directory record will be 22 bytes long, so // we expect to find the EOCD marker 22 bytes from the end. final ByteBuffer zipCentralDirectoryStart = ByteBuffer.allocate(4); zipCentralDirectoryStart.order(ByteOrder.LITTLE_ENDIAN);
//这块就很清楚了 apk大小减去comment大小,commenlength大小,CDIR偏移量大小,就是偏移量的起始位置,读一下就可以了。 fileChannel.position(fileChannel.size() - commentLength - 6); // 6 = 2 (Comment length) + 4 (Offset of start of central directory, relative to start of archive) fileChannel.read(zipCentralDirectoryStart); final long centralDirStartOffset = zipCentralDirectoryStart.getInt(0); return centralDirStartOffset; } //我们要找到我们的签名块 这是第一步 public static Pair<ByteBuffer, Long> findApkSigningBlock( final FileChannel fileChannel) throws IOException, SignatureNotFoundException { final long centralDirOffset = findCentralDirStartOffset(fileChannel); return findApkSigningBlock(fileChannel, centralDirOffset); } //第六步,通过获取到的Central Dir偏移地址去找签名块 public static Pair<ByteBuffer, Long> findApkSigningBlock( final FileChannel fileChannel, final long centralDirOffset) throws IOException, SignatureNotFoundException { //CDIR的结构 // Find the APK Signing Block. The block immediately precedes the Central Directory. // FORMAT: // OFFSET DATA TYPE DESCRIPTION // * @+0 bytes uint64: size in bytes (excluding this field) // * @+8 bytes payload // * @-24 bytes uint64: size in bytes (same as the one above) // * @-16 bytes uint128: magic if (centralDirOffset < APK_SIG_BLOCK_MIN_SIZE) { throw new SignatureNotFoundException( "APK too small for APK Signing Block. ZIP Central Directory offset: " + centralDirOffset); } // Read the magic and offset in file from the footer section of the block: // * uint64: size of block // * 16 bytes: magic
//看图一
fileChannel.position(centralDirOffset - 24); final ByteBuffer footer = ByteBuffer.allocate(24); fileChannel.read(footer); footer.order(ByteOrder.LITTLE_ENDIAN); if ((footer.getLong(8) != APK_SIG_BLOCK_MAGIC_LO) || (footer.getLong(16) != APK_SIG_BLOCK_MAGIC_HI)) { throw new SignatureNotFoundException( "No APK Signing Block before ZIP Central Directory"); } // Read and compare size fields final long apkSigBlockSizeInFooter = footer.getLong(0); if ((apkSigBlockSizeInFooter < footer.capacity()) || (apkSigBlockSizeInFooter > Integer.MAX_VALUE - 8)) { throw new SignatureNotFoundException( "APK Signing Block size out of range: " + apkSigBlockSizeInFooter); } final int totalSize = (int) (apkSigBlockSizeInFooter + 8);
//这是计算 签名块 前两处的 末尾偏移量 final long apkSigBlockOffset = centralDirOffset - totalSize; if (apkSigBlockOffset < 0) { throw new SignatureNotFoundException( "APK Signing Block offset out of range: " + apkSigBlockOffset); } fileChannel.position(apkSigBlockOffset);
//这块不是很懂 为什么能通过后两部分的大小计算出 签名块 前两个区域的内容 ,猜测是通过大小段 ,上面的注释应该是提示 final ByteBuffer apkSigBlock = ByteBuffer.allocate(totalSize); fileChannel.read(apkSigBlock); apkSigBlock.order(ByteOrder.LITTLE_ENDIAN); final long apkSigBlockSizeInHeader = apkSigBlock.getLong(0); if (apkSigBlockSizeInHeader != apkSigBlockSizeInFooter) { throw new SignatureNotFoundException( "APK Signing Block sizes in header and footer do not match: " + apkSigBlockSizeInHeader + " vs " + apkSigBlockSizeInFooter); }
//这块就返回了存储有签名信息,渠道信息的块给前面去读 return Pair.of(apkSigBlock, apkSigBlockOffset); }
//这里就是获取签名块中的key和对应的value,基本上获取了这些,读取渠道信息基本没有任何问题了 public static Map<Integer, ByteBuffer> findIdValues(final 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
//这里是过滤apk签名块的中存储签名信息的key,value,起点在8----大小-24
final ByteBuffer pairs = sliceFromTo(apkSigningBlock, 8, apkSigningBlock.capacity() - 24); final Map<Integer, ByteBuffer> idValues = new LinkedHashMap<Integer, ByteBuffer>(); // keep order 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); }
//循环读 每次8个字节 final long lenLong = pairs.getLong(); if ((lenLong < 4) || (lenLong > Integer.MAX_VALUE)) { throw new SignatureNotFoundException( "APK Signing Block entry #" + entryCount + " size out of range: " + lenLong); } final int len = (int) lenLong; final 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()); } final int id = pairs.getInt();
//4个字节的id和变长的value getByteBuffer需要根据调整大小 idValues.put(id, getByteBuffer(pairs, len - 4)); pairs.position(nextEntryPos); } //返回所有的id和value return idValues; } /** * Returns new byte buffer whose content is a shared subsequence of this buffer's content * between the specified start (inclusive) and end (exclusive) positions. As opposed to * {@link ByteBuffer#slice()}, the returned buffer's byte order is the same as the source * buffer's byte order. */ private static ByteBuffer sliceFromTo(final ByteBuffer source, final int start, final int end) { if (start < 0) { throw new IllegalArgumentException("start: " + start); } if (end < start) { throw new IllegalArgumentException("end < start: " + end + " < " + start); } final int capacity = source.capacity(); if (end > source.capacity()) { throw new IllegalArgumentException("end > capacity: " + end + " > " + capacity); } final int originalLimit = source.limit(); final int originalPosition = source.position(); try { source.position(0); source.limit(end); source.position(start); final ByteBuffer result = source.slice(); result.order(source.order()); return result; } finally { source.position(0); source.limit(originalLimit); source.position(originalPosition); } } /** * Relative <em>get</em> method for reading {@code size} number of bytes from the current * position of this buffer. * <p> * <p>This method reads the next {@code size} bytes at this buffer's current position, * returning them as a {@code ByteBuffer} with start set to 0, limit and capacity set to * {@code size}, byte order set to this buffer's byte order; and then increments the position by * {@code size}. */ private static ByteBuffer getByteBuffer(final ByteBuffer source, final int size) throws BufferUnderflowException { if (size < 0) { throw new IllegalArgumentException("size: " + size); } final int originalLimit = source.limit(); final int position = source.position(); final int limit = position + size; if ((limit < position) || (limit > originalLimit)) { throw new BufferUnderflowException(); } source.limit(limit); try { final ByteBuffer result = source.slice(); result.order(source.order()); source.position(limit); return result; } finally { source.limit(originalLimit); } } private static void checkByteOrderLittleEndian(final ByteBuffer buffer) { if (buffer.order() != ByteOrder.LITTLE_ENDIAN) { throw new IllegalArgumentException("ByteBuffer byte order must be little endian"); } } }
/** * https://source.android.com/security/apksigning/v2.html * https://en.wikipedia.org/wiki/Zip_(file_format) */
//写渠道与读类似
class ApkSigningBlock { // The format of the APK Signing Block is as follows (all numeric fields are little-endian): // .size of block in bytes (excluding this field) (uint64) // .Sequence of uint64-length-prefixed ID-value pairs: // *ID (uint32) // *value (variable-length: length of the pair - 4 bytes) // .size of block in bytes—same as the very first field (uint64) // .magic “APK Sig Block 42” (16 bytes) // FORMAT: // OFFSET DATA TYPE DESCRIPTION // * @+0 bytes uint64: size in bytes (excluding this field) // * @+8 bytes payload // * @-24 bytes uint64: size in bytes (same as the one above) // * @-16 bytes uint128: magic // payload 有 8字节的大小,4字节的ID,还有payload的内容组成 private final List<ApkSigningPayload> payloads; ApkSigningBlock() { super(); payloads = new ArrayList<ApkSigningPayload>(); } public final List<ApkSigningPayload> getPayloads() { return payloads; } public void addPayload(final ApkSigningPayload payload) { payloads.add(payload); } //写渠道信息,这里的DataOutput是apk,这个输入流已经 定位到了 签名块的偏移位置 这块还是不明白为什么传的是apksign区域的末尾偏移地址 public long writeApkSigningBlock(final DataOutput dataOutput) throws IOException { long length = 24; // 24 = 8(size of block in bytes—same as the very first field (uint64)) + 16 (magic “APK Sig Block 42” (16 bytes))
//这里计算你要写入的信息的大小 for (int index = 0; index < payloads.size(); ++index) { final ApkSigningPayload payload = payloads.get(index); final byte[] bytes = payload.getByteBuffer(); length += 12 + bytes.length; // 12 = 8(uint64-length-prefixed) + 4 (ID (uint32)) } ByteBuffer byteBuffer = ByteBuffer.allocate(8); // Long.BYTES byteBuffer.order(ByteOrder.LITTLE_ENDIAN); byteBuffer.putLong(length); byteBuffer.flip(); dataOutput.write(byteBuffer.array()); //这个就是不断写入渠道所需要的信息了 for (int index = 0; index < payloads.size(); ++index) { final ApkSigningPayload payload = payloads.get(index);
//写value final byte[] bytes = payload.getByteBuffer(); byteBuffer = ByteBuffer.allocate(8); // Long.BYTES byteBuffer.order(ByteOrder.LITTLE_ENDIAN); byteBuffer.putLong(bytes.length + (8 - 4)); // Long.BYTES - Integer.BYTES byteBuffer.flip(); dataOutput.write(byteBuffer.array()); //写key byteBuffer = ByteBuffer.allocate(4); // Integer.BYTES byteBuffer.order(ByteOrder.LITTLE_ENDIAN); byteBuffer.putInt(payload.getId()); byteBuffer.flip(); dataOutput.write(byteBuffer.array()); dataOutput.write(bytes); } //这块是所有的信息写完后,你需要写大小和魔数 byteBuffer = ByteBuffer.allocate(8); // Long.BYTES byteBuffer.order(ByteOrder.LITTLE_ENDIAN); byteBuffer.putLong(length); byteBuffer.flip(); dataOutput.write(byteBuffer.array()); //写签名魔数 16个字节 byteBuffer = ByteBuffer.allocate(8); // Long.BYTES byteBuffer.order(ByteOrder.LITTLE_ENDIAN); byteBuffer.putLong(ApkUtil.APK_SIG_BLOCK_MAGIC_LO); byteBuffer.flip(); dataOutput.write(byteBuffer.array()); byteBuffer = ByteBuffer.allocate(8); // Long.BYTES byteBuffer.order(ByteOrder.LITTLE_ENDIAN); byteBuffer.putLong(ApkUtil.APK_SIG_BLOCK_MAGIC_HI); byteBuffer.flip(); dataOutput.write(byteBuffer.array()); return length; } }
以上是我对walle的个人分析,还有一些不懂的需要接下来再去深入了解
参考资料:
- https://tech.meituan.com/2017/01/13/android-apk-v2-signature-scheme.html
- https://github.com/Meituan-Dianping/walle/commit/be6421b5ceb72fb427495118b431b1e54b1e8bbe
- https://www.jianshu.com/p/d80e7d9b4c58
- https://juejin.im/post/5be91e776fb9a049c43d2ff3
- http://yifeiyuan.me/blog/d27e6b00.html
- https://source.android.com/security/apksigning/v2