该部分内容除了用来进行打渠道包以外,还可以用来给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;
}