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

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

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

晒干的老咸鱼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值