多渠道打包工具Walle源码分析

时间:2023-12-16 11:47:14

一、背景

  首先了解多渠道打包工具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进行编码。

多渠道打包工具Walle源码分析

CERT.SF:对MANIFEST.MF的每一个条目进行一次相同的操作

多渠道打包工具Walle源码分析

CERT.RSA:这个文件是个二进制文件,也就是用我们的签名文件对 CERT.SF进行签名。
所以我们可以发现,上述三个文件保存了我们所有的签名信息。那么我们可以发现,他却没有验证META-INF文件夹中的信息,所以我们完全可以通过在META-INF文件夹中添加不同的文件,然后在APP中读取,来进行区分。这样避免了重复签名。当然,在v2签名出来之后,v2签名对整个apk,进行了签名。因为我们一般会
同时v1、v2签名,所以自然META-INF也需要验证。再用相同的方法,必然会报错。除非删除签名信息后,重新签名。

v2签名:

 先放一张v2签名经典的原理图。

多渠道打包工具Walle源码分析

我们可以知道v2签名在原APK的基础上添加了APK SIgning Block区域用来保护其他三跨块区域,所以我们可以很明显的知道,如果我们在这块区域中进行修改,是不会进行相关的签名校验的。Walle正是利用这种方式来进行的相关修改

所以在解析 APK 时,首先要通过以下方法找到“ZIP *目录”的起始位置:在文件末尾找到“ZIP *目录结尾”记录,然后从该记录中读取“*目录”的起始偏移量。通过 magic 值,可以快速确定“*目录”前方可能是“APK 签名分块”。然后,通过 size of block 值,可以高效地找到该分块在文件中的起始位置。

多渠道打包工具Walle源码分析

图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的个人分析,还有一些不懂的需要接下来再去深入了解

参考资料: