Android的APK Signature Scheme v2签名及一款基于Java环境的校验工具介绍

uxymwgcmzy 8年前
   <h2>背景</h2>    <h3>APK Signature Scheme v2官方介绍</h3>    <p>Android 7.0 引入一项新的应用签名方案 APK Signature Scheme v2,它能提供更快的应用安装时间和更多针对未授权 APK 文件更改的保护。在默认情况下,Android Studio 2.2 和 Android Plugin for Gradle 2.2 会使用 APK Signature Scheme v2 和传统签名方案来签署您的应用。</p>    <p>如果您使用 APK Signature Scheme v2 签署您的应用,并对应用进行了进一步更改,则应用的签名将无效。出于这个原因,请在使用 APK Signature Scheme v2 签署您的应用之前、而非之后使用 zipalign 等工具。</p>    <p>渠道方或者开发者关于渠道包的解决方案</p>    <p>目前主流的渠道号方案主要有以下几种:</p>    <ol>     <li>修改后重新打包活签名的,例如在AndroidMainfest里面添加mata-data等</li>     <li> <p>修改后不需要重新签名,主要有两种:</p>      <ul>       <li>直接把apk包看成一个zip包,然后在zip包的注释段添加对应的渠道信息</li>       <li>直接把apk包看成一个zip包,然后利用相关命令在META-INF内注入${channel}.txt 文件</li>      </ul> </li>    </ol>    <p>其中下面两种不需要重新签名的方法,被各主要渠道广泛使用。</p>    <h3>渠道包与V2签名的冲突</h3>    <p>然而,为了提高Android系统的安全性,Google从Android N增强了签名模式,该模式在原有的签名模式上,增加校验APK的SHA256哈希值;这种新引入的签名机制,会对整个文件的每个字节都会做校验,包括 comment 区域。 如果签名后对APK作了任何修改,例如上面提到的修改注释段或者注入文件,在Android 7.0以上的机型安装时都会校验失败,提示没有签名无法安装。</p>    <h2>解决方案</h2>    <p>为了解决使用V2引起的问题,官方提供了不启用V2签名的方法,但是随着Android越来越完善,这种方案最终肯定是要全面使用的。目前已经有团队找到了基于V2签名的新的渠道号方案,由于种种原因,暂时不详细论述怎么实现。这里仅提供其余的解决方案:</p>    <h3>怎么禁用V2签名</h3>    <p>对于怎么禁用V2签名,官方提供了对应的方法,下文内容为禁用方法</p>    <p>虽然我们建议您对您的应用采用 APK Signature Scheme v2,但这项新方案并非强制性的。如果您的应用在使用 APK Signature Scheme v2 时不能正确开发,您可以停用这项新方案。禁用过程会导致 Android Studio 2.2 和 Android Plugin for Gradle 2.2 仅使用传统签名方案来签署您的应用。要仅用传统方案签署,打开模块级 build.gradle 文件,然后将行 v2SigningEnabled false 添加到您的版本签名配置中:</p>    <pre>  <code class="language-java">android {      ...      defaultConfig { ... }      signingConfigs {        release {          storeFile file("myreleasekey.keystore")          storePassword "password"          keyAlias "MyReleaseKey"          keyPassword "password"          v2SigningEnabled false        }      }    }</code></pre>    <h3>怎么检查一个apk是否使用了V2签名以及V2校验是否通过</h3>    <p>对于渠道SDK的开发者或者渠道来说,如果开发者上传的应用V2签名校验不能通过的话,那将是非常严重的问题,这样的安装包在Android 7.0以上版本的机器上是完全无法安装的。目前官方并没有提供校验一个apk是否使用V2签名,以及V2签名校验是否通过。</p>    <p>虽然官方没有提供对应独立的工具,但是源码中肯定是存在对应的实现方案的。通过阅读源码发现在 ApkSignatureSchemeV2Verifier.java 里面有对应的逻辑实现,目前个人已经把对应代码迁移出来制作了独立的工具提供使用。</p>    <p>工具下载</p>    <ul>     <li> <p>下载地址:</p>      <ul>       <li><a href="/misc/goto?guid=4959731669430310997" rel="nofollow,noindex">https://github.com/bihe0832/AndroidAPKInfo/tree/master/CheckAndroidV2Signature/CheckAndroidV2Signature.jar</a></li>      </ul> </li>     <li> <p>MD5:</p> <pre>  <code class="language-java">MD5 (CheckAndroidV2Signature.jar) = 3f234f2f19913859332f5c9d00bab7d1</code></pre> </li>    </ul>    <p>使用事例</p>    <ul>     <li> <p>查看帮助</p> <pre>  <code class="language-java">➜  java -jar CheckAndroidV2Signature.jar      usage: java -jar ./CheckAndroidV2Signature.jar [--version] [--help] [filePath]       such as:            java -jar ./CheckAndroidV2Signature.jar --version         java -jar ./CheckAndroidV2Signature.jar --help         java -jar ./CheckAndroidV2Signature.jar ./test.apk       after check,the result will be a string json such as:            {"ret":0,"msg":"ok","isV2":true,"isV2OK":true}            ret: result code for check                0 : command exec succ             -1 : file not found             -2 : file not an Android APK file             -3 : check File signature error ,retry again            msg: result msg for check         isV2: whether the file is use Android-V2 signature or not         isV2OK: whether the file's Android-V2 signature is ok or not</code></pre> </li>     <li> <p>查看版本</p> <pre>  <code class="language-java">➜  java -jar ./CheckAndroidV2Signature.jar --version    com.tencent.ysdk.CheckAndroidV2Signature version 1.0.0 (CheckAndroidV2Signature - 1)</code></pre> </li>     <li> <p>查看应用信息</p> <pre>  <code class="language-java">➜  java -jar ./CheckAndroidV2Signature.jar ./YSDK_Android_1.3.1_629-debug-ysdktest-inner.apk    {"ret":0,"msg":"ok","isV2":false,"isV2OK":false}</code></pre> </li>    </ul>    <p>源码地址:</p>    <ul>     <li><a href="/misc/goto?guid=4959731669527094182" rel="nofollow,noindex">https://github.com/bihe0832/AndroidAPKInfo/tree/master/CheckAndroidV2Signature</a></li>    </ul>    <p>代码调整</p>    <p>总体上是对Android的源码的移植,没有太多调整。主要调整的部分就是在 feedIntoMessageDigests 函数中计算md5的时候,为了提升效率,源码使用内存映射的方式,源码中是直接内存映射,代码迁移的时候调整为调用Java系统函数来完成内存映射。对应代码如下:</p>    <ul>     <li> <p>AOSP:</p> <pre>  <code class="language-java">@Override    public void feedIntoMessageDigests(            MessageDigest[] mds, long offset, int size) throws IOException {        // IMPLEMENTATION NOTE: After a lot of experimentation, the implementation of this        // method was settled on a straightforward mmap with prefaulting.        //        // This method is not using FileChannel.map API because that API does not offset a way        // to "prefault" the resulting memory pages. Without prefaulting, performance is about        // 10% slower on small to medium APKs, but is significantly worse for APKs in 500+ MB        // range. FileChannel.load (which currently uses madvise) doesn't help. Finally,        // invoking madvise (MADV_SEQUENTIAL) after mmap with prefaulting wastes quite a bit of        // time, which is not compensated for by faster reads.          // We mmap the smallest region of the file containing the requested data. mmap requires        // that the start offset in the file must be a multiple of memory page size. We thus may        // need to mmap from an offset less than the requested offset.        long filePosition = mFilePosition + offset;        long mmapFilePosition =                (filePosition / MEMORY_PAGE_SIZE_BYTES) * MEMORY_PAGE_SIZE_BYTES;        int dataStartOffsetInMmapRegion = (int) (filePosition - mmapFilePosition);        long mmapRegionSize = size + dataStartOffsetInMmapRegion;        long mmapPtr = 0;        try {            mmapPtr = OS.mmap(                    0, // let the OS choose the start address of the region in memory                    mmapRegionSize,                    OsConstants.PROT_READ,                    OsConstants.MAP_SHARED | OsConstants.MAP_POPULATE, // "prefault" all pages                    mFd,                    mmapFilePosition);            // Feeding a memory region into MessageDigest requires the region to be represented            // as a direct ByteBuffer.            ByteBuffer buf = new DirectByteBuffer(                    size,                    mmapPtr + dataStartOffsetInMmapRegion,                    mFd,  // not really needed, but just in case                    null, // no need to clean up -- it's taken care of by the finally block                    true  // read only buffer                    );            for (MessageDigest md : mds) {                buf.position(0);                md.update(buf);            }        } catch (ErrnoException e) {            throw new IOException("Failed to mmap " + mmapRegionSize + " bytes", e);        } finally {            if (mmapPtr != 0) {                try {                    OS.munmap(mmapPtr, mmapRegionSize);                } catch (ErrnoException ignored) {}            }        }    }</code></pre> </li>     <li> <p>修改后</p> <pre>  <code class="language-java">@Override    public void feedIntoMessageDigests(FileChannel channel,            MessageDigest[] mds, long offset, int size) throws IOException {        long filePosition = mFilePosition + offset;        MappedByteBuffer inputBuffer = channel.map(FileChannel.MapMode.READ_ONLY, filePosition, size);// 读取大文件            for (MessageDigest md : mds) {         inputBuffer.position(0);            md.update(inputBuffer);        }    }</code></pre> </li>    </ul>    <p> </p>    <p>来自:http://blog.bihe0832.com/android-v2-signature.html</p>    <p> </p>