Android的APK Signature Scheme v2簽名及一款基于Java環境的校驗工具介紹

背景

APK Signature Scheme v2官方介紹

Android 7.0 引入一項新的應用簽名方案 APK Signature Scheme v2,它能提供更快的應用安裝時間和更多針對未授權 APK 文件更改的保護。在默認情況下,Android Studio 2.2 和 Android Plugin for Gradle 2.2 會使用 APK Signature Scheme v2 和傳統簽名方案來簽署您的應用。

如果您使用 APK Signature Scheme v2 簽署您的應用,并對應用進行了進一步更改,則應用的簽名將無效。出于這個原因,請在使用 APK Signature Scheme v2 簽署您的應用之前、而非之后使用 zipalign 等工具。

渠道方或者開發者關于渠道包的解決方案

目前主流的渠道號方案主要有以下幾種:

  1. 修改后重新打包活簽名的,例如在AndroidMainfest里面添加mata-data等
  2. 修改后不需要重新簽名,主要有兩種:

    • 直接把apk包看成一個zip包,然后在zip包的注釋段添加對應的渠道信息
    • 直接把apk包看成一個zip包,然后利用相關命令在META-INF內注入${channel}.txt 文件

其中下面兩種不需要重新簽名的方法,被各主要渠道廣泛使用。

渠道包與V2簽名的沖突

然而,為了提高Android系統的安全性,Google從Android N增強了簽名模式,該模式在原有的簽名模式上,增加校驗APK的SHA256哈希值;這種新引入的簽名機制,會對整個文件的每個字節都會做校驗,包括 comment 區域。 如果簽名后對APK作了任何修改,例如上面提到的修改注釋段或者注入文件,在Android 7.0以上的機型安裝時都會校驗失敗,提示沒有簽名無法安裝。

解決方案

為了解決使用V2引起的問題,官方提供了不啟用V2簽名的方法,但是隨著Android越來越完善,這種方案最終肯定是要全面使用的。目前已經有團隊找到了基于V2簽名的新的渠道號方案,由于種種原因,暫時不詳細論述怎么實現。這里僅提供其余的解決方案:

怎么禁用V2簽名

對于怎么禁用V2簽名,官方提供了對應的方法,下文內容為禁用方法

雖然我們建議您對您的應用采用 APK Signature Scheme v2,但這項新方案并非強制性的。如果您的應用在使用 APK Signature Scheme v2 時不能正確開發,您可以停用這項新方案。禁用過程會導致 Android Studio 2.2 和 Android Plugin for Gradle 2.2 僅使用傳統簽名方案來簽署您的應用。要僅用傳統方案簽署,打開模塊級 build.gradle 文件,然后將行 v2SigningEnabled false 添加到您的版本簽名配置中:

android {
    ...
    defaultConfig { ... }
    signingConfigs {
      release {
        storeFile file("myreleasekey.keystore")
        storePassword "password"
        keyAlias "MyReleaseKey"
        keyPassword "password"
        v2SigningEnabled false
      }
    }
  }

怎么檢查一個apk是否使用了V2簽名以及V2校驗是否通過

對于渠道SDK的開發者或者渠道來說,如果開發者上傳的應用V2簽名校驗不能通過的話,那將是非常嚴重的問題,這樣的安裝包在Android 7.0以上版本的機器上是完全無法安裝的。目前官方并沒有提供校驗一個apk是否使用V2簽名,以及V2簽名校驗是否通過。

雖然官方沒有提供對應獨立的工具,但是源碼中肯定是存在對應的實現方案的。通過閱讀源碼發現在 ApkSignatureSchemeV2Verifier.java 里面有對應的邏輯實現,目前個人已經把對應代碼遷移出來制作了獨立的工具提供使用。

工具下載

  • 下載地址:

    </li>
  • MD5:

    MD5 (CheckAndroidV2Signature.jar) = 3f234f2f19913859332f5c9d00bab7d1
  • </ul>

    使用事例

    • 查看幫助

      ?  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> 
      

    • 查看版本

      ?  java -jar ./CheckAndroidV2Signature.jar --version
        com.tencent.ysdk.CheckAndroidV2Signature version 1.0.0 (CheckAndroidV2Signature - 1)
    • 查看應用信息

      ?  java -jar ./CheckAndroidV2Signature.jar ./YSDK_Android_1.3.1_629-debug-ysdktest-inner.apk
        {"ret":0,"msg":"ok","isV2":false,"isV2OK":false}
    • </ul>

      源碼地址:

      代碼調整

      總體上是對Android的源碼的移植,沒有太多調整。主要調整的部分就是在 feedIntoMessageDigests 函數中計算md5的時候,為了提升效率,源碼使用內存映射的方式,源碼中是直接內存映射,代碼遷移的時候調整為調用Java系統函數來完成內存映射。對應代碼如下:

      • AOSP:

        @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>

      • 修改后

        @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);
              }
          }
      • </ul>

         

        來自:http://blog.bihe0832.com/android-v2-signature.html

         

 本文由用戶 uxymwgcmzy 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!