0%

Android签名笔记

应用沙盒

概述

Android 平台利用基于用户的 Linux 保护机制来识别和隔离应用资源,可将不同的应用分离开,并保护应用和系统免受恶意应用的攻击。为此,Android 会为每个 Android 应用分配一个独一无二的用户 ID (UID),并在自己的进程中运行。

Android 会使用此 UID 设置一个内核级应用沙盒。内核会在进程级别利用标准的 Linux 机制(例如,分配给应用的用户 ID 和组 ID)实现应用和系统之间的安全防护。 默认情况下,应用不能彼此交互,而且对操作系统的访问权限会受到限制。例如,如果应用 A(一个单独的应用)尝试执行恶意操作,例如在没有权限的情况下读取应用 B 的数据或拨打电话,操作系统会阻止此类行为,因为应用 A 没有适当的用户权限。这一沙盒机制非常简单,可审核,并且基于已有数十年历史的 UNIX 风格的进程用户隔离和文件权限机制。

由于应用沙盒位于内核层面,因此该安全模型的保护范围扩展到了原生代码和操作系统应用。位于更高层面的所有软件(例如,操作系统库、应用框架、应用运行时环境和所有应用)都会在应用沙盒中运行。在某些平台上,为了执行安全防护机制,会限制开发者只能使用特定的开发框架、API 或语言。在 Android 上,并没有为此而限制开发者必须如何编写应用;在这方面,原生代码与解释型代码一样进行沙盒化。

保护机制

通常,要在经过适当配置的设备上攻破应用沙盒这道防线,必须要先攻破 Linux 内核的安全功能。但是,与其他安全功能类似,强制执行应用沙盒的各种保护机制并非无懈可击,因此深度防御对于防止通过单个漏洞入侵操作系统或其他应用非常重要。

Android 依靠许多保护机制来强制执行应用沙盒。 这些强制措施是随着时间的推移不断引入的,并且显著增强了基于 UID 的原始自主访问控制 (DAC) 沙盒的安全性。 以前的 Android 版本包括以下保护机制:

  • 在 Android 5.0 中,SELinux 提供了强制访问控制 (MAC) 来将系统和应用分离开。但是,所有第三方应用都在相同的 SELinux 环境中运行,因此应用间的隔离主要由 UID DAC 强制执行。
  • 在 Android 6.0 中,SELinux 沙盒经过扩展,可以跨各个物理用户边界隔离应用。此外,Android 还为应用数据设置了更安全的默认设置:对于 targetSdkVersion >= 24 的应用,应用主目录上的默认 DAC 权限从 751 更改为 700。这为私有应用数据提供了更安全的默认设置(但应用可能会替换这些默认设置)。
  • 在 Android 8.0 中,所有应用都设为使用 seccomp-bpf 过滤器运行,该过滤器可限制允许应用使用的系统调用,从而增强应用/内核边界的安全性。
  • 在 Android 9 中,targetSdkVersion >= 28 的所有非特权应用都必须在不同的 SELinux 沙盒中运行,并针对各个应用提供 MAC。这种保护机制可以提升应用隔离效果,防止替换安全默认设置,并且(最重要的是)防止应用的数据可让所有人访问。

共享文件指南

将应用数据设为可供所有人访问从安全方面来讲是一种不好的做法,因为这会为所有人授予访问权限,并且无法限定只让目标受众访问这些数据。这种做法会导致信息披露泄露,让代理漏洞变得混乱,并会成为针对包含敏感数据的应用(例如电子邮件客户端)的恶意软件的首选目标。在 Android 9 及更高版本中,targetSdkVersion>=28 的应用明确禁止以这种方式共享文件。

在共享文件时,请遵循以下指南,而不是让应用数据可供所有人访问:

如果您的应用需要与其他应用共享文件,请使用内容提供程序外部存储设备上的共享位置。内容提供程序会以适当的粒度共享数据,并且不会出现使用所有人都可访问的 UNIX 权限会带来的诸多问题(如需了解详情,请参阅内容提供程序基础知识)。
如果您的应用包含确实应让所有人访问的文件(例如照片),请使用外部存储设备。如需帮助,请参阅将文件保存至公共目录

应用签名

在 Android 上,应用签名是将应用放入其应用沙盒的第一步。已签名的应用证书定义了哪个用户 ID 与哪个应用相关联;不同的应用要以不同的用户 ID 运行。应用签名可确保一个应用无法访问任何其他应用的数据,通过明确定义的 IPC 进行访问时除外。

当应用(APK 文件)安装到 Android 设备上时,软件包管理器会验证 APK 是否已经过适当签名(已使用 APK 中包含的证书签名)。如果该证书(或更准确地说,证书中的公钥)与设备上的任何其他 APK 使用的签名密钥一致,那么这个新 APK 就可以选择在清单中指定它将与其他以类似方式签名的 APK 共用一个 UID。

应用可以由第三方(OEM、运营商、其他应用市场)签名,也可以自行签名。Android 提供了使用自签名证书进行代码签名的功能,而开发者无需外部协助或许可即可生成自签名证书。应用并非必须由核心机构签名。Android 目前不对应用证书进行 CA 认证。

应用还可以在“签名”保护级别声明安全权限,以便仅限使用同一个密钥签名的应用访问它们,同时维持单独的 UID 和应用沙盒。通过共用 UID 功能,可以与共用的应用沙盒建立更紧密的联系,这是因为借助该功能,使用同一个开发者密钥签名的两个或更多应用可以在其清单中声明共用的 UID。

Android 支持以下三种应用签名方案:

  • v1 方案:基于 JAR 签名。
  • v2 方案:APK 签名方案 v2(在 Android 7.0 中引入)。
  • v3 方案:APK 签名方案 v3(在 Android 9 中引入)。

为了最大限度地提高兼容性,请按照 v1、v2、v3 的先后顺序采用所有方案对应用进行签名。与只通过 v1 方案签名的应用相比,还通过 v2+ 方案签名的应用能够更快速地安装到 Android 7.0 及更高版本的设备上。更低版本的 Android 平台会忽略 v2+ 签名,这就需要应用包含 v1 签名。

v1方案(jarsinger)

签名机制

v1 签名不保护 APK 的某些部分,例如 ZIP 元数据。APK 验证程序需要处理大量不可信(尚未经过验证)的数据结构,然后会舍弃不受签名保护的数据。这会导致相当大的受攻击面。此外,APK 验证程序必须解压所有已压缩的条目,而这需要花费更多时间和内存。为了解决这些问题,Android 7.0 中引入了 APK 签名方案 v2。

APK文件本质上是一个ZIP压缩包,而ZIP格式是固定的,主要由三部分构成:

  • 第一部分是内容块,所有的压缩文件都在这部分。每个压缩文件都有一个local file header,主要记录了文件名、压缩算法、压缩前后的文件大小、修改时间、CRC32值等。
  • 第二部分称为中央目录,包含了多个central directory file header(和第一部分的local file header一一对应),每个中央目录文件头主要记录了压缩算法、注释信息、对应local file header的偏移量等,方便快速定位数据。
  • 最后一部分是EOCD,主要记录了中央目录大小、偏移量和ZIP注释信息等

V1签名只会检验第一部分的所有压缩文件,而不理会后两部分内容。

解压APK后,在META-INF目录下,可以看到三个文件:MANIFEST.MF、CERT.SF、CERT.RSA。它们都是V1签名的产物。

其中,MANIFEST.MF文件内容如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Manifest-Version: 1.0   // 用来定义manifest文件的版本
Built-By: Generated-by-ADT // 声明该文件的构建者
Created-By: Android Gradle 3.4.2 // 声明该文件的生成者

Name: AndroidManifest.xml
SHA-256-Digest: 2ukv+j6Xu2UEEelZaKeZ+63IpYQ5FqCbIfA6QKk7fgM=

Name: META-INF/androidx.appcompat_appcompat.version
SHA-256-Digest: n9KGQtOsoZHlx/wjg8/W+rsqrIdD8Cnau4mJrFhOMbw=

Name: META-INF/androidx.arch.core_core-runtime.version
SHA-256-Digest: wo/MpTY3vIjhJK8XJd8Ty5jGne3v1i+zzb4c22t2BiQ=

// ...

Name: res/drawable-nodpi-v4/filter_48.jpg
SHA-256-Digest: FfpvAStgPFXWDjVvs/N2H+XULiIwwwfqGtp1fPg0YYo=

// ...

Name: res/xml/file_paths.xml
SHA-256-Digest: 9s07yeSRLkE3+iYROBVOBmGlmApgGVDUeGsZQ9HYPfo=

Name: resources.arsc
SHA-256-Digest: uGEAbvopf67Kasj8mPZE7R1NpI9jzuaL26usC1mXVsE=

它记录了APK中所有原始文件的数据摘要的Base64编码,而数据摘要算法就是SHA256(或SHA1)。

CERT.SF文件内容如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Signature-Version: 1.0
Created-By: 1.0 (Android)
SHA-256-Digest-Manifest: qXt5WBCgJuQFdkZK1ZpHd5KnGllF0FNY/yx++rgsdDc=
X-Android-APK-Signed: 2

Name: AndroidManifest.xml
SHA-256-Digest: 9SF8sMBwuyZm25vpoYk4VZImaK37o4rXpIGKPWWDj9M=

Name: META-INF/androidx.appcompat_appcompat.version
SHA-256-Digest: ABbgKP0s08CVeuJ5ZMlIZx/AvJtb1QhNA0ffeXfCaHk=

Name: META-INF/androidx.arch.core_core-runtime.version
SHA-256-Digest: PjygIQMN5T6nIKT/hi5PFaxVcEB+W20fr4f0g2n7jrg=

//...

Name: res/xml/file_paths.xml
SHA-256-Digest: aDSRY140t8cYqDFleCi9Gc4NTFo0EfbEY/Mr3vlfx1o=

Name: resources.arsc
SHA-256-Digest: KNGXNZ4K5DU+sdJIblO/zwEug24++hvcQxp7fbaf6Gk=

SHA-256-Digest-Manifest记录了整个MANIFEST.MF文件的数据摘要的Base64编码。其余的普通属性则和MANIFEST.MF中的属性一一对应,分别记录了对应数据块的数据摘要的Base64编码。这里要注意的是:最后一行的换行符是必不可少,需要参与计算的。

CERT.RSA文件包含了对CERT.SF文件的数字签名和开发者的数字证书。RSA就是计算数字签名使用的非对称加密算法。

整个签名机制的最终产物就是MANIFEST.MF、CERT.SF、CERT.RSA三个文件。

除了CERT.RSA文件,其余两个签名文件其实跟keystore没什么关系,主要是文件自身的摘要及二次摘要,用不同的keystore进行签名,生成的MANIFEST.MF与CERT.SF都是一样的,不同的只有CERT.RSA签名文件。也就是说前两者主要保证各个文件的完整性,CERT.RSA从整体上保证APK的来源及完整性,不过META_INF中的文件不在校验范围中,这也是V1的一个缺点。

签名流程可查看源码:SignApk.java

校验流程

V1签名是怎么保证APK文件不被篡改的?

  • 首先,如果破坏者修改了APK中的任何文件,那么被篡改文件的数据摘要的Base64编码就和MANIFEST.MF文件的记录值不一致,导致校验失败。
  • 其次,如果破坏者同时修改了对应文件在MANIFEST.MF文件中的Base64值,那么MANIFEST.MF中对应数据块的Base64值就和CERT.SF文件中的记录值不一致,导致校验失败。
  • 最后,如果破坏者更进一步,同时修改了对应文件在CERT.SF文件中的Base64值,那么CERT.SF的数字签名就和CERT.RSA记录的签名不一致,也会校验失败。
  • 理论上不可能继续伪造CERT.SF的数字签名,因为破坏者没有开发者的私钥,但是可以重新签名。

v2方案(apksigner)

签名机制

APK 签名方案 v2 是一种全文件签名方案,该方案能够发现对 APK 的受保护部分进行的所有更改,从而有助于加快验证速度并增强完整性保证。为了保持与 v1 APK 格式向后兼容,v2 及更高版本的 APK 签名会存储在“APK 签名分块”内,该分块是为了支持 APK 签名方案 v2 而引入的一个新容器。在 APK 文件中,“APK 签名分块”位于“ZIP 中央目录”(位于文件末尾)之前并紧邻该部分。

APK 签名方案 v2 是在 Android 7.0 (Nougat) 中引入的。为了使 APK 可在 Android 6.0 (Marshmallow) 及更低版本的设备上安装,应先使用 JAR 签名功能对 APK 进行签名,然后再使用 v2 方案对其进行签名。

为了保护 APK 内容,APK 包含以下 4 个部分:

  1. ZIP 条目的内容(从偏移量 0 处开始一直到“APK 签名分块”的起始位置)
  2. APK 签名分块
  3. ZIP 中央目录
  4. ZIP 中央目录结尾

V2签名同时修改了EOCD中的中央目录的偏移量,使签名后的APK还符合ZIP结构。

V2签名块的生成可参考ApkSignerV2

  1. 首先,根据多个签名算法,计算出整个APK的数据摘要,组成APK数据摘要集;
  2. 接着,把数据摘要、数字证书和额外属性组装起来,形成类似于V1签名的“MF”文件;
  3. 其次,再用相同的私钥,不同的签名算法,计算出“MF”文件的数字签名,形成类似于V1签名的“SF”文件;
  4. 然后,把第二列的类似MF文件、类似SF文件和开发者公钥一起组装成通过单个keystore签名后的v2签名块;
  5. 最后,把多个keystore签名后的签名块组装起来,就是完整的V2签名块了(Android中允许使用多个keystore对apk进行签名)。

验证流程

APK 签名方案 v2 负责保护第 1、3、4 部分的完整性,以及第 2 部分包含的“APK 签名方案 v2 分块”中的 signed data 分块的完整性。第 1、3 和 4 部分的完整性通过其内容的一个或多个摘要来保护,这些摘要存储在 signed data 分块中,而这些分块则通过一个或多个签名来保护。

V1签名是怎么保证APK文件不被篡改的?

  • 首先,如果破坏者修改了APK文件的任何部分(签名块本身除外),那么APK的数据摘要就和“MF”数据块中记录的数据摘要不一致,导致校验失败。
  • 其次,如果破坏者同时修改了“MF”数据块中的数据摘要,那么“MF”数据块的数字签名就和“SF”数据块中记录的数字签名不一致,导致校验失败。
  • 然后,如果破坏者使用自己的私钥去加密生成“SF”数据块,那么使用开发者的公钥去解密“SF”数据块中的数字签名就会失败。
  • 最后,更进一步,若破坏者甚至替换了开发者公钥,那么使用数字证书中的公钥校验签名块中的公钥就会失败,这也正是数字证书的作用。

v3方案(apksigner)

Android 9 新增了对 APK Signature Scheme v3 的支持。该架构提供的选择可以在其签名块中为每个签名证书加入一条轮转证据记录。 利用此功能,应用可以通过将 APK 文件过去的签名证书链接到现在签署应用时使用的证书,从而使用新签名证书来签署应用。

Android 9 支持 APK 密钥轮转,这使应用能够在 APK 更新过程中更改其签名密钥。为了实现轮转,APK 必须指示新旧签名密钥之间的信任级别。为了支持密钥轮转,我们将 APK 签名方案从 v2 更新为 v3,以允许使用新旧密钥。v3 在 APK 签名分块中添加了有关受支持的 SDK 版本和 proof-of-rotation 结构的信息。

为了保持与 v1 APK 格式向后兼容,v2 和 v3 APK 签名存储在“APK 签名分块”内紧邻 ZIP Central Directory 前面。v3 APK 签名分块的格式与 v2 相同。

配置调试的Debug包apk可安装

Android Studio 3.0会在debug apk的配置文件application标签里自动添加 android:testOnly=”true”属性,导致IDE中run跑出的apk无法安装,只能用于as测试安装。

解决办法:在gradle.properties(项目根目录或者gradle全局配置目录 ~/.gradle/)文件中添加android.injected.testOnly=false 之后就可以安装了。

证书和密钥库

Keystore 称为密钥库,一个keystore里面可以放多组秘钥,每组密钥都有有效期、地址、公司等信息,可以通过别名来进行区分拿取。开发者将录入自己信息的秘钥(而非秘钥库Keystore)存入APP中,以认证此APP为自己开发。

Eclipse或Android Studio在Debug时,对App签名都会使用一个默认的密钥库:~/.android/debug.keystore

  • 密钥库名: debug.keystore
  • 密钥别名: androiddebugkey
  • 密钥库密码: android

由于调试证书是由构建工具创建并且设计上不安全,因此大多数应用商店(包括 Google Play 商店)都不接受使用调试证书签署发布的 APK 或应用软件包。

数据摘要、数字签名和数字证书

数据摘要

数据摘要算法是一种能产生特定输出格式的算法,其原理是根据一定的运算规则对原始数据进行某种形式的信息提取,被提取出的信息就是原始数据的消息摘要,也称为数据指纹。 一般情况下,数据摘要算法具有以下特点:

  1. 无论输入数据有多大(长),计算出来的数据摘要的长度总是固定的。例如:MD5算法计算出的数据摘要有128Bit。
  2. 一般情况下(不考虑碰撞的情况下),只要原始数据不同,那么其对应的数据摘要就不会相同。同时,只要原始数据有任何改动,那么其数据摘要也会完全不同。即:相同的原始数据必有相同的数据摘要,不同的原始数据,其数据摘要也必然不同。
  3. 不可逆性,即只能正向提取原始数据的数据摘要,而无法从数据摘要中恢复出原始数据。

著名的摘要算法有RSA公司的MD5算法和SHA系列算法。

数字签名和数字证书

数字签名和数字证书是成对出现的,两者不可分离(数字签名主要用来校验数据的完整性,数字证书主要用来确保公钥的安全发放)。

要明白数字签名的概念,必须要了解数据的加密、传输和校验流程。一般情况下,要实现数据的可靠通信,需要解决以下两个问题:

  1. 确定数据的来源是其真正的发送者。
  2. 确保数据在传输过程中,没有被篡改,或者若被篡改了,可以及时发现。

而数字签名,就是为了解决这两个问题而诞生的。 首先,数据的发送者需要先申请一对公私钥对,并将公钥交给数据接收者。 然后,若数据发送者需要发送数据给接收者,则首先要根据原始数据,生成一份数字签名,然后把原始数据和数字签名一起发送给接收者。 数字签名由以下两步计算得来:

  1. 计算发送数据的数据摘要
  2. 用私钥对提取的数据摘要进行加密

这样,数据接收者拿到的消息就包含了两块内容:

  1. 原始数据内容
  2. 附加的数字签名

接下来,接收者就会通过以下几步,校验数据的真实性:

  1. 用相同的摘要算法计算出原始数据的数据摘要。
  2. 用预先得到的公钥解密数字签名。
  3. 对比签名得到的数据是否一致,如果一致,则说明数据没有被篡改,否则数据就是脏数据了。

因为私钥只有发送者才有,所以其他人无法伪造数字签名。这样通过数字签名就确保了数据的可靠传输。 综上所述,数字签名就是只有发送者才能产生的别人无法伪造的一段数字串,这段数字串同时也是对发送者发送数据真实性的一个有效证明。

想法虽好,但是上面的整个流程,有一个前提,就是数据接收者能够正确拿到发送者的公钥。如果接收者拿到的公钥被篡改了,那么坏人就会被当成好人,而真正的数据发送者发送的数据则会被视作脏数据。那怎么才能保证公钥的安全性那?这就要靠数字证书来解决了。

数字证书是由有公信力的证书中心(CA)颁发给申请者的证书,主要包含了:证书的发布机构、证书的有效期、申请者的公钥、申请者信息、数字签名使用的算法,以及证书内容的数字签名。

可见,数字证书也用到了数字签名技术。只不过签名的内容是数据发送方的公钥,以及一些其它证书信息。这样数据发送者发送的消息就包含了三部分内容:

  1. 原始数据内容
  2. 附加的数字签名
  3. 申请的数字证书。

接收者拿到数据后,首先会根据CA的公钥,解码出发送者的公钥。然后就与上面的校验流程完全相同了。

所以,数字证书主要解决了公钥的安全发放问题。

密钥管理

如果准备自行创建密钥和密钥库,请确保先为密钥库选择一个强密码,然后为密钥库中存储的每个私钥选择一个单独的强密码,且必须为密钥库存放在一个可靠的地方。

签名步骤

1. 生成密钥对

  1. 生成密钥对

    1
    2
    3
    4
    5
    6
    7
    8
    9
    keytool -genkeypair -keystore 密钥库名 -alias 密钥别名 -validity 天数 -keyalg RSA

    参数:
    -genkeypair 生成一条密钥对(由私钥和公钥组成)
    -keystore 密钥库名字以及存储位置(默认当前目录)
    -alias 密钥对的别名(密钥库可以存在多个密钥对,用于区分不同密钥对)
    -validity 密钥对的有效期(单位: 天)
    -keyalg 生成密钥对的算法(常用RSA/DSA,DSA只用于签名,默认采用DSA)
    -delete 删除一条密钥

    提示: 可重复使用此条命令,在同一密钥库中创建多条密钥对,例如,在debug.keystore中新增一对密钥,别名是release:

    1
    keytool -genkeypair -keystore debug.keystore -alias release -validity 30000
  2. 查看密钥库

    1
    2
    3
    4
    5
    keytool -list -v -keystore 密钥库名

    参数:
    -list 查看密钥列表
    -v 查看密钥详情

2. 签名

zipalign

位于Android SDK/build-tools/SDK版本/下,zipalign是对zip包对齐的工具,使APK包内未压缩的数据有序排列对齐,从而减少APP运行时内存消耗。

1
2
zipalign -v 4 in.apk out.apk   # 4字节对齐优化
zipalign -c -v 4 in.apk # 检查APK是否对齐

zipalign可以在V1签名后执行,但zipalign不能在V2签名后执行,只能在V2签名之前执行。

jarsigner

1
jarsigner -keystore 密钥库名 xxx.apk 密钥别名

从JDK7开始, jarsigner默认算法是SHA256, 但Android 4.2以下不支持该算法,所以需要修改算法, 添加参数-digestalg SHA1 -sigalg SHA1withRSA

1
2
3
4
5
jarsigner -keystore 密钥库名 -digestalg SHA1 -sigalg SHA1withRSA xxx.apk 密钥别名

参数:
-digestalg 摘要算法
-sigalg 签名算法

apksigner

1
2
3
4
5
6
7
8
9
10
11
12
apksigner sign --ks 密钥库名 --ks-key-alias 密钥别名 xxx.apk

# 若密钥库中有多个密钥对,则必须指定密钥别名
apksigner sign --ks 密钥库名 --ks-key-alias 密钥别名 xxx.apk

# 禁用V2签名
apksigner sign --v2-signing-enabled false --ks 密钥库名 xxx.apk

参数:
--ks-key-alias 密钥别名,若密钥库有一个密钥对,则可省略,反之必选
--v1-signing-enabled 是否开启V1签名,默认开启
--v2-signing-enabled 是否开启V2签名,默认开启

3. 签名验证

keytool,只支持V1签名校验

1
2
3
4
5
keytool -printcert -jarfile MyApp.apk (显示签名证书信息)

参数:
-printcert 打印证书内容
-jarfile <filename> 已签名的jar文件 或apk文件

apksigner,支持V1和V2签名校验

1
2
3
4
5
apksigner verify -v --print-certs xxx.apk

参数:
-v, --verbose 显示详情(显示是否使用V1和V2签名)
--print-certs 显示签名证书信息

Gradle签名发布

可以使用Android Studio工具配置KeyStore相关的配置,会自动在build.gradle文件中生成配置。

  1. 通过keytool生成签名文件

  2. 配置build.gradle

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    android {
    signingConfigs {
    release {
    storeFile file("path/release.keystore")
    storePassword "123456"
    keyAlias "release.keystore"
    keyPassword "123456"
    }
    debug {
    storeFile file("path/test.keystore")
    storePassword "123456"
    keyAlias "test.keystore"
    keyPassword "123456"
    }
    }
    buildTypes {
    release {
    minifyEnabled enableProguardInReleaseBuilds
    proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
    signingConfig signingConfigs.release

    android.applicationVariants.all { variant ->
    variant.outputs.all {
    if (variant.buildType.name.equals('release')) {
    outputFileName = "SecureMail${defaultConfig.versionName}-${releaseTime()}.apk"
    variant.getPackageApplication().outputDirectory = new File(
    project.rootDir.absolutePath + "/app/release")
    }
    }
    }
    }
    }

    flavorDimensions "version"
    productFlavors {
    hearing {
    dimension "version"
    signingConfig signingConfigs.release
    }
    full {
    dimension "version"
    }
    }
    }

    static def releaseTime() {
    return new Date().format("yyyy-MM-dd", TimeZone.getTimeZone("UTC"))
    }
  3. 执行打包命令

    1
    ./gradlew assemblerelease

注意:可以在根目录的gradle.properties文件中配置签名相关的变量,在build.gradle中直接引入即可。

多渠道打包方案

Android Gradle Plugin

Gradle Plugin本身提供了多渠道的打包策略: 首先,在AndroidManifest.xml中添加渠道信息占位符:

1
<meta-data android:name="InstallChannel" android:value="${InstallChannel}" />

然后,通过Gradle Plugin提供的productFlavors标签,添加渠道信息:

1
2
3
4
5
6
7
8
productFlavors{
"YingYongBao"{
manifestPlaceholders = [InstallChannel : "YingYongBao"]
}
"360"{
manifestPlaceholders = [InstallChannel : "360"]
}
}

这样,Gradle编译生成多渠道包时,会用不同的渠道信息替换AndroidManifest.xml中的占位符。我们在代码中,也就可以直接读取AndroidManifest.xml中的渠道信息了。

但是,这种方式存在一些缺点:

  1. 每生成一个渠道包,都要重新执行一遍构建流程,效率太低,只适用于渠道较少的场景。
  2. Gradle会为每个渠道包生成一个不同的BuildConfig.java类,记录渠道信息,导致每个渠道包的DEX的CRC值都不同。一般情况下,这是没有影响的。但是如果你使用了微信的Tinker热补丁方案,那么就需要为不同的渠道包打不同的补丁,这完全是不可以接受的。(因为Tinker是通过对比基础包APK和新包APK生成差分补丁,然后再把补丁和基础包APK一起合成新包APK。这就要求用于生成差分补丁的基础包DEX和用于合成新包的基础包DEX是完全一致的,即:每一个基础渠道包的DEX文件是完全一致的,不然就会合成失败)

ApkTool

ApkTool是一个逆向分析工具,可以把APK解开,添加代码后,重新打包成APK。因此,基于ApkTool的多渠道打包方案分为以下几步:

  1. 复制一份新的APK
  2. 通过ApkTool工具,解压APK(apktool d origin.apk)
  3. 删除已有签名信息
  4. 添加渠道信息(可以在APK的任何文件添加渠道信息)
  5. 通过ApkTool工具,重新打包生成新APK(apktool b newApkDir)
  6. 重新签名

优点: 不需要重新构建新渠道包,仅需要复制修改就可以了。并且因为是重新签名,所以同时支持V1和V2签名。

缺点:

  • ApkTool工具不稳定,曾经遇到过升级Gradle Plugin版本后,低版本ApkTool解压APK失败的情况。
  • 生成新渠道包时,需要重新解包、打包和签名,而这几步操作又是相对比较耗时的。经过测试:生成企鹅电竞10个渠道包需要16分钟左右,虽然比Gradle Plugin方案减少很多耗时。但是若需要同时生成上百个渠道包,则需要几个小时,显然不适合渠道非常多的业务场景。

VasDolly

VasDolly实现原理

概述

众所周知,因为国内Android应用分发市场的现状,我们在发布APP时,一般需要生成多个渠道包,上传到不同的应用市场。这些渠道包需要包含不同的渠道信息,在APP和后台交互或者数据上报时,会带上各自的渠道信息。这样,我们就能统计到每个分发市场的下载数、用户数等关键数据。

项目地址:VasDolly

VasDolly是一种快速多渠道打包工具,同时支持基于V1签名和V2签名进行多渠道打包。插件本身会自动检测Apk使用的签名类别,并选择合适的多渠道打包方式,对使用者来说完全透明。

基于V1签名的多渠道打包方案

根据之前的V1签名和校验机制可知,V1签名只会检验第一部分的所有压缩文件,而不理会后两部分内容。因此,只要把渠道信息写入到后两块内容就可以通过V1校验,而EOCD的注释字段无疑是最好的选择。

在APK文件的注释字段,添加渠道信息。 整个方案包括以下几步:

  1. 复制APK
  2. 找到EOCD数据块
  3. 修改注释长度
  4. 添加渠道信息
  5. 添加渠道信息长度
  6. 添加魔数:方便从后向前读取数据,定位渠道信息。

该方案的最大优点就是:不需要解压缩APK,不需要重新签名,只需要复制APK,在注释字段添加渠道信息。每个渠道包仅需几秒的耗时,非常适合渠道较多的APK。

基于V2签名的多渠道打包方案

V2签名块中的数据摘要是针对APK的文件内容块、中央目录和EOCD三块内容计算的。但是在写入签名块后,修改了EOCD中的中央目录偏移量,那么在进行V2签名校验时,理论上在“数据摘要校验”这步应该会校验失败。

Android系统在校验APK的数据摘要时,首先会把EOCD的中央目录偏移量替换成签名块的偏移量,然后再计算数据摘要。而签名块的偏移量就是v2签名之前的中央目录偏移量,因此,这样计算出的数据摘要就和“MF”数据块中的数据摘要完全一致了。

Android系统只会关注ID为0x7109871a的V2签名块,并且忽略其他的ID-Value,同时V2签名只会保护APK本身,不包含签名块。因此,基于V2签名的多渠道打包方案就应运而生:在APK签名块中添加一个ID-Value,存储渠道信息。整个方案包括以下几步:

  1. 找到APK的EOCD块
  2. 找到APK签名块
  3. 获取已有的ID-Value Pair
  4. 添加包含渠道信息的ID-Value
  5. 基于所有的ID-Value生成新的签名块
  6. 修改EOCD的中央目录的偏移量(上面已介绍过:修改EOCD的中央目录偏移量,不会导致数据摘要校验失败)
  7. 用新的签名块替代旧的签名块,生成带有渠道信息的APK

多渠道包的强校验

那么如何保证通过这些方案生成的渠道包,能够在所有Android平台上正确安装那?

Google提供了一个同时支持V1和V2签名和校验的工具:apksig。它包括一个apksigner命令行和一个apksig类库。其中前者就是Android SDK build-tools下面的命令行工具。正是借助后面的apksig来进行渠道包强校验,它可以保证渠道包在apk Minsdk ~ 最高版本之间都校验通过。