Android apk多渠道打包工具,支持V1和V2签名的apk,不破坏原有签名

  • Post author:
  • Post category:其他


该部分内容除了用来进行打渠道包以外,还可以用来给apk进行二次签名,只是写入的渠道信息变成二次签名信息而已。

该工程已打包成一个可执行jar包,下载地址:

Android多渠道打包工具,支持V1和V2签名的apk-Android文档类资源-CSDN下载

本文只是讲解具体的实现思路,不会进行详细的分析,因为网上关于V1和V2签名的文章太多了,这里不赘述了。

V1签名的apk已经有很多人写过分析了,主要是对META-INF目录进行操作,因为V1签名的apk除了该目录外,别的文件都是被保护的,只要进行了修改,那么就会破坏原有签名,所以对于V1签名的apk主要是在META-INF目录下写入渠道信息,这里简单的创建一个新文件,渠道名称为文件名称。

V2签名的apk只有V2签名块是不被保护的,V2签名块的具体结构为:

8 +   V2签名块总长度 (8 + 4 + 签名块长)

8 +  V2签名总长度(4 + 签名块长)

4 +  V2签名块的key

signData +    V2签名块具体信息

8 +  V2签名块的总长度

16  16位的魔法值,用于定位V2签名块的位置

这里对于V2签名的apk写入渠道信息也是在V2签名块进行,写入渠道信息后的V2签名块结构为:

8 +   V2签名块总长度 (8 + 4 + 签名块长)

8 +  V2签名总长度(4 + 签名块长)

4 +  V2签名块的key

signData +    V2签名块具体信息

8 + 渠道信息总长度(4 + 渠道信息长)

4 + 渠道信息的key

channelData + 渠道信息

8 +  V2签名块的总长度

16  16位的魔法值,用于定位V2签名块的位置

这里新增的内容为8 + 4 + 渠道信息

因为这里新增了8 +4 + 渠道信息的长度,所以整个V2签名块的总长度也需要进行修改,导致V2签名的第三部分开始索引也需要跟着进行修改,否则会导致找不到V2签名的魔法值,从而导致V2签名被破坏。

渠道信息可以是任意文本文件,支持以#开头的注释,每一个渠道换一行,示例channel.txt如下:

#美团 支持#开头的注释

meituan

#91

#baidu

360

#jialian

#huawei

xiaomi

#oppo

具体的部分代码如下:

向V1签名的apk写入渠道信息的方法如下:

/**

* 向V1签名的apk写入渠道信息

* @param sourceFile 原始apk文件

* @param outputFile 写入渠道信息后的apk文件

* @param channel  待写入的渠道信息

*/

public static void writechannelToV1Apk(File sourceFile, File outputFile, String channel) {


try {


ZipArchiveOutputStream zipArchiveOutputStream = new ZipArchiveOutputStream(outputFile);

ZipFile zipFile = new ZipFile(sourceFile);

Enumeration<ZipArchiveEntry> enumeration = zipFile.getEntries();

// 原样拷贝

while (enumeration.hasMoreElements()) {


ZipArchiveEntry zipArchiveEntry = enumeration.nextElement();

zipArchiveOutputStream.putArchiveEntry(zipArchiveEntry);

if (zipArchiveEntry.isDirectory()) {


System.out.println(zipArchiveEntry.getName());

} else {


ZipUtils.copy(zipFile.getInputStream(zipArchiveEntry), zipArchiveOutputStream);

}

zipArchiveOutputStream.closeArchiveEntry();

}

// 添加文件,这里如果担心安全问题,可以在写入的channel命名的文件里写入特定内容,对该内容进行校验

ZipArchiveEntry zipArchiveEntry = new ZipArchiveEntry(“META-INF/” + channel);

zipArchiveOutputStream.putArchiveEntry(zipArchiveEntry);

zipArchiveOutputStream.closeArchiveEntry();

zipFile.close();

zipArchiveOutputStream.close();

} catch (ZipException e) {


e.printStackTrace();

} catch (IOException e) {


e.printStackTrace();

} catch (Exception e) {


e.printStackTrace();

}

}

读取apk是否有V2签名的方法如下:

/**

* 读取原V2签名apk的签名块信息

*

* @param inputApk

* @return

*/

public static byte[] getApkSignBlockData(ByteBuffer inputApk) {


byte[] signBlockData = null;

if (inputApk == null) {


Utils.printoutString(“getApkSignBlockData after buffer is null”);

return null;

}

ByteBuffer originalInputApk = inputApk;

inputApk = originalInputApk.slice();

inputApk.order(ByteOrder.LITTLE_ENDIAN);

try {


int eocdOffset = ZipUtils.findZipEndOfCentralDirectoryRecord(inputApk);

Utils.printoutString(

“getApkSignBlockData fourth area start eocdOffset: ” + eocdOffset);

if (eocdOffset == -1) {


throw new ApkSignerV2.ApkParseException(“Failed to locate ZIP End of Central Directory”);

}

if (ZipUtils.isZip64EndOfCentralDirectoryLocatorPresent(inputApk, eocdOffset)) {


throw new ApkSignerV2.ApkParseException(“ZIP64 format not supported”);

}

inputApk.position(eocdOffset);

long centralDirSizeLong = ZipUtils.getZipEocdCentralDirectorySizeBytes(inputApk);

if (centralDirSizeLong > Integer.MAX_VALUE) {


throw new ApkSignerV2.ApkParseException(

“ZIP Central Directory size out of range: ” + centralDirSizeLong);

}

int centralDirSize = (int) centralDirSizeLong;

long centralDirOffsetLong = ZipUtils.getZipEocdCentralDirectoryOffset(inputApk);

if (centralDirOffsetLong > Integer.MAX_VALUE) {


throw new ApkSignerV2.ApkParseException(

“ZIP Central Directory offset in file out of range: ”

+ centralDirOffsetLong);

}

int centralDirOffset = (int) centralDirOffsetLong;

Utils.printoutString(

“getApkSignBlockData centralDirOffset: ” + centralDirOffset

+ “, centralDirSize: ” + centralDirSize + “, third area start: ”

+ centralDirOffset);

inputApk.position(centralDirOffset);

int expectedEocdOffset = centralDirOffset + centralDirSize;

if (expectedEocdOffset < centralDirOffset) {


throw new ApkSignerV2.ApkParseException(

“ZIP Central Directory extent too large. Offset: ” + centralDirOffset

+ “, size: ” + centralDirSize);

}

if (eocdOffset != expectedEocdOffset) {


throw new ApkSignerV2.ApkParseException(

“ZIP Central Directory not immeiately followed by ZIP End of Central Directory. CD end: ”

+ expectedEocdOffset + “, EoCD start: ” + eocdOffset);

}

inputApk.position(centralDirOffset – 16);

byte[] magic = new byte[16];

inputApk.get(magic);

for (int i = 0; i < 16; i++) {


if (magic[i] != APK_SIGNING_BLOCK_MAGIC[i]) {


Utils.printoutString(

“getApkSignBlockData magic is not equal,please check whether has v2 signature block”);

return null;

}

}

inputApk.position(centralDirOffset – 24);

long v2SignatureBlockSizeExcludeFirst8ByteSizeLong = inputApk.getLong();

Utils.printoutString(

“getApkSignBlockData v2SignatureBlockSizeExcludeFirst8ByteSizeLong: ”

+ v2SignatureBlockSizeExcludeFirst8ByteSizeLong);

if (v2SignatureBlockSizeExcludeFirst8ByteSizeLong > Integer.MAX_VALUE) {


Utils.printoutString(

“getApkSignBlockData v2SignatureBlockSizeExcludeFirst8ByteSizeLong exceed 4G”);

throw new ApkSignerV2.ApkParseException(“sizeexceed”);

}

int v2SignatureBlockSizeExcludeFirst8ByteSize = (int) v2SignatureBlockSizeExcludeFirst8ByteSizeLong;

int v2SignatureBlockOffset = centralDirOffset

– v2SignatureBlockSizeExcludeFirst8ByteSize – 8;

Utils.printoutString(

“getApkSignBlockData first area end v2SignatureBlockOffset: ”

+ v2SignatureBlockOffset);

Utils.printoutString(

“getApkSignBlockData second area start v2SignatureBlockOffset: ”

+ v2SignatureBlockOffset + “, second area end centralDirOffset: ”

+ centralDirOffset);

inputApk.position(v2SignatureBlockOffset);

long v2SignatureFisrt8ByteBlockSizeLong = inputApk.getLong();

Utils.printoutString(

“getApkSignBlockData v2SignatureFisrt8ByteBlockSizeLong: ”

+ v2SignatureFisrt8ByteBlockSizeLong);

if (v2SignatureFisrt8ByteBlockSizeLong != v2SignatureBlockSizeExcludeFirst8ByteSizeLong) {


Utils.printoutString(“getApkSignBlockData verify signature v2Signature Size error”);

throw new ApkSignerV2.ApkParseException(“v2Signature BlockSize is not equal”);

} else {


Utils.printoutString(“getApkSignBlockData v2Signature BlockSize is equal”);

}

long v2SchemeBlockSizeMinus4ByteLong = inputApk.getLong();

long v2SchemeBlockSizeLong = v2SchemeBlockSizeMinus4ByteLong – 4;

int v2BlockSchemeId = inputApk.getInt();

Utils.printoutString(“getApkSignBlockData verify v2SchemeId is 0x7109871a”);

Utils.printHexLong(v2BlockSchemeId);

if (v2BlockSchemeId != APK_SIGNATURE_SCHEME_V2_BLOCK_ID) {


Utils.printoutString(“v2SchemeId is not valid,please check”);

throw new ApkSignerV2.ApkParseException(“v2SchemeId is not valid”);

}

int v2SchemeBlockSize = (int) v2SchemeBlockSizeLong;

inputApk.position(v2SignatureBlockOffset + 8 + 8 + 4 + v2SchemeBlockSize);

inputApk.position(v2SignatureBlockOffset + 8);

int v2SignBlockLength = inputApk.getInt();

v2SignBlockLength = v2SignBlockLength – 4;

Utils.printoutString(“getApkSignBlockData v2SignBlockLength: ” + v2SignBlockLength);

inputApk.position(v2SignatureBlockOffset);

int signBlockLength = v2SchemeBlockSize;

signBlockData = new byte[signBlockLength];

inputApk.get(signBlockData, 0, signBlockLength);

} catch (Exception e) {


Utils.printoutString(“getApkSignBlockData 读取签名块发生错误” + e.getMessage());

e.printStackTrace();

}

return signBlockData;

}

向V2签名的apk写入渠道信息的代码如下

/**

* 在原有V2签名的基础上,增加了渠道信息之后,需要更新相应的索引数据,避免索引混乱而找不到签名信息

* @param v2SchemeBlockBytes

* @param channel

* @return

* @throws ApkSignerV2.ApkParseException

*/

private static byte[] getAfterWriteChannelSignatureBlock(byte[] v2SchemeBlockBytes, String channel)

throws ApkSignerV2.ApkParseException {


if (v2SchemeBlockBytes == null || v2SchemeBlockBytes.length < 1) {


throw new ApkSignerV2.ApkParseException(

“getAfterWriteChannelSignatureBlock error get v2SchemeBlockBytes”);

}

byte[] chanelBytes = channel.getBytes(Charset.forName(“UTF-8”));

int resultSize = 8

+ 8 + 4 + v2SchemeBlockBytes.length

+ 8 + 4 + chanelBytes.length

+ 8

+ 16

;

ByteBuffer result = ByteBuffer.allocate(resultSize);

result.order(ByteOrder.LITTLE_ENDIAN);

long blockSizeFieldValue = resultSize – 8;

result.putLong(blockSizeFieldValue);

long pairSizeFieldValue = 4 + v2SchemeBlockBytes.length;

result.putLong(pairSizeFieldValue);

result.putInt(APK_SIGNATURE_SCHEME_V2_BLOCK_ID);

result.put(v2SchemeBlockBytes);

long channelLength = 4 + chanelBytes.length;

result.putLong(channelLength);

result.putInt(SIGN_ID);

result.put(chanelBytes);

result.putLong(blockSizeFieldValue);

result.put(APK_SIGNING_BLOCK_MAGIC);

return result.array();

}

对于apk启动的时候如何读取写入的渠道信息


ApplicationInfo appinfo = context.getApplicationInfo();


String sourceDir = appinfo.sourceDir;//获取apk的路径

/**

*  读取apk里面写入的渠道信息,没有渠道信息就返回null

* @param channelApk

* @param channels 所有的渠道信息

* @return

*/

public static String readChannelFromV1Apk(File channelApk, List<String> channels) {


String channel = null;

try {


ZipFile zipFile = new ZipFile(channelApk);

Enumeration<ZipArchiveEntry> enumeration = zipFile.getEntries();

while (enumeration.hasMoreElements()) {


ZipArchiveEntry zipArchiveEntry = enumeration.nextElement();

String zipArchiveEntryName = zipArchiveEntry.getName();

//META-INF/下出了MF、SF、RSA文件外,就是渠道文件了

if (zipArchiveEntryName.startsWith(“META-INF/”)

&& !zipArchiveEntryName.equals(“META-INF/CERT.SF”)

&& !zipArchiveEntryName.equals(“META-INF/CERT.RSA”)

&& !zipArchiveEntryName.equals(“META-INF/MANIFEST.MF”)) {


if (channels.contains(zipArchiveEntryName.substring(9))) {


channel = zipArchiveEntryName;

break;

}

}

}

zipFile.close();

} catch (IOException e) {


e.printStackTrace();

} catch (Exception e) {


e.printStackTrace();

}

return channel;

}

/**

* 从渠道文件获取渠道信息

* @param channelFilePath

* @return

*/

public static List<String> getChannels(String channelFilePath) {


List<String> channels = null;

try {


File channelFile = new File(channelFilePath);

if (!channelFile.exists()) {


throw new RuntimeException(“渠道文件未找到!”);

}

BufferedReader br = new BufferedReader(new FileReader(channelFile));

String line = null;

channels = new ArrayList<>();

while ((line = br.readLine()) != null) {


//如果是#开头的注释信息,就跳过

if (line.startsWith(“#”)) {


continue;

}else {


//不是#开头的就是实际的渠道信息

channels.add(line);

}

}

br.close();

} catch (Exception e) {


e.printStackTrace();

}

return channels;

}



版权声明:本文为chali1314原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。