From c4dcec60b4c4d513a6b4dacc53831de5e8b2b1ee Mon Sep 17 00:00:00 2001 From: Omooo <869759698@qq.com> Date: Mon, 13 May 2019 15:11:33 +0800 Subject: [PATCH] =?UTF-8?q?add=20Android=20=E7=AD=BE=E5=90=8D=E6=A0=A1?= =?UTF-8?q?=E9=AA=8C=E6=9C=BA=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- blogs/Android/Framework/APK 构建流程.md | 9 +- .../Framework/Android 签名校验机制.md | 96 ++++++++++++++++++ .../读书笔记之一.md | 14 +-- blogs/Android/Gradle/Gradle_Plugin_Guide.md | 11 +- blogs/Android/Gradle/TinyPngPlugin.md | 4 +- images/Android/Framework/v2 签名.png | Bin 0 -> 13494 bytes 6 files changed, 116 insertions(+), 18 deletions(-) create mode 100644 blogs/Android/Framework/Android 签名校验机制.md create mode 100644 images/Android/Framework/v2 签名.png diff --git a/blogs/Android/Framework/APK 构建流程.md b/blogs/Android/Framework/APK 构建流程.md index dd4016c..e7ceda6 100644 --- a/blogs/Android/Framework/APK 构建流程.md +++ b/blogs/Android/Framework/APK 构建流程.md @@ -4,9 +4,10 @@ Android APK 构建流程 #### 目录 -2. 前言 -3. 构建流程 -4. Android Gradle Plugin 源码分析 +1. 前言 +2. 构建流程 + +2. Android Gradle Plugin 源码分析 #### 前言 @@ -18,7 +19,7 @@ Android APK 构建流程 #### 构建流程 -先来一张 [Android 配置构建]() 官网上的一张图: +先来一张 [Android 配置构建](https://developer.android.com/studio/build/index.html?hl=zh-cn#build-process) 官网上的一张图: ![](https://i.loli.net/2019/05/07/5cd1888bcfafa.png) diff --git a/blogs/Android/Framework/Android 签名校验机制.md b/blogs/Android/Framework/Android 签名校验机制.md new file mode 100644 index 0000000..4d704a0 --- /dev/null +++ b/blogs/Android/Framework/Android 签名校验机制.md @@ -0,0 +1,96 @@ +--- +Android 签名校验机制 v1、v2、v3 +--- + +#### 目录 + +2. 概述 +3. v1 +4. v2 +5. v3 +6. 参考 + +#### 概述 + +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 方案:JAR 签名 + +v1 利用的是 jarsigner 工具,它是 JDK 自带的。它会对所有的文件(包括 META-INF 中与签名不相关的文件)都会被签名。 + +在 APK 的 META-INF 目录下一般有三个文件:.MF、.SF 和 .RSA 三个文件。 + +**(1).MF 文件:** + +APK 当中的原始文件信息用摘要算法如 SHA1 计算得到的摘要信息并用 base64 编码保存,以及对应采用的摘要算法如 SHA1。 + +**(2).SF 文件:** + +.MF 文件的摘要信息以及 .MF 文件当中每个条目在用摘要算法计算得到的摘要信息并用 base64 编码保存。 + +**(3).RSA 文件:** + +存放证书信息,公钥信息,以及用私钥对 .SF 文件的加密数据即签名信息,这段数据是无法伪造的,除非有私钥,另外,.RSA 文件还记录了所用的签名算法等信息。 + +APK 包在安装的时候,是按照从 (3)到(1)到顺序依次校验的。先用公钥还原签名信息,然后和 .SF 文件中的信息对比,然后用同样的摘要算法对 .MF 文件里面的每一个条目计算对应的摘要信息,然后对比 .MF 文件是否一致。 + +在这个过程中,我没发现有两点: + +1. 在校验的过程中需要解压,因为 .MF 文件的摘要信息是基于原始未压缩文件内存,因此在校验的时候就需要解压出原始数据,而这个解压操作无疑是耗时操作。 +2. APK 包的完整性校验不够强。这里我们可以看到,如果我们在 APK 签名后,对 APK 包中没有涉及到原始文件的数据块做改变,那么这层校验机制就会失效。所以,在美团的打包工具 Walle 的做法就是在 META-INF 中添加空文件来实现多渠道打包,因为在 META-INF 中添加新文件是不需要重新签名的。这也同时说明了,其实存在过度签名的问题。 + +为了解决这些问题,Android 7.0 中引入了 APK 签名方案 v2。 + +#### v2 方案 + +APK 签名方案 v2 是一种全文件签名方案,该方案能够发现对 APK 受保护部分进行的所有更改,从而有助于加快验证速度并**增强完整性保证**。 + +使用 APK 签名方案 v2 进行签名时,会在 APK 文件中插入一个 APK 签名分块,该分块位于 " ZIP 中央目录" 部分之前并紧邻该部分。在 " APK 签名分块" 内,v2 签名和签名者身份信息会存储在其中。 + +![](https://i.loli.net/2019/05/13/5cd8e2f3387d818425.png) + +v2 签名机制不存在解压原始数据,签名校验时间显著减少,因此安装时间也相应减少。同时,它是基于 APK 的二进制内存做的签名信息(APK Signing Block 签名块本身不参与加密校验),因此打包后改变 APK 的其他三部分的任何字节都会导致签名校验不通过。这也说明了,zipalign 是要在 apksigner 之前的。 + +APK Signing Bolck 由这几个部分组成:两个用来标示这个区块长度的八字节 + 这个区块的魔数(APK Sig Bolck 42)+ 这个区块所承载数据(ID-value)。 + +| 偏移 | 字节数 | 描述 | +| ---- | ------ | ------------------------------------------- | +| @0 | 8 | 这个 Block 的长度(本字段的长度不计算在内) | +| @8 | n | 一组 ID-value | +| @-24 | 8 | 这个 Bolck 的长度(和第一个字段一样值) | +| @-16 | 16 | 魔数 "APK Sig Block 42" | + + v2 的签名信息是以 ID(0x7109871a)的 ID-value 来保存在这个区块中,这是一组 ID-value,但是在验证的时候,只是通过 ID 为 0x7109871a 的来获取 APK Signature Scheme v2 Block,对这个区块中其他的 ID-value 选择了忽略。所以 Walle 提供一个自定义的 ID-value 并写入该区域,从而为快速生成渠道包服务。 + +想要了解是如何做的,请参考:[新一代开源Android渠道包生成工具Walle](https://tech.meituan.com/2017/01/13/android-apk-v2-signature-scheme.html) + +在 APK 签名分块内,也就是 ID 为 0x7109871a,它里面有一个叫做 **signed data** 的数据结构。APK 签名方案 v2 负责保护第1、3、4 部分的完整性,以及第二部分包含的 APK 签名方案 v2 分块中的 signed data 分块的完整性。 + +第1、3、4 部分的完整性通过其内容的一个或多个摘要来保护,这些摘要存储在 signed data 分块中,而这些分块则通过一个或多个签名保护。第1、3、4 部分都会被拆分为多个大小为 1MB 的连续块分段计算摘要,摘要以分段方式计算,以便通过并行处理来加快计算速度。 + +APK 签名方案 v2 是在 Android 7.0 中引入的,为了使 APK 可在 Android 6.0 及更低版本的设备上安装,应先使用 JAR 签名功能对 APK 进行签名,然后再使用 v2 方案对其进行签名。 + +#### v3 方案 + +Android 9 支持 APK 密钥轮转,这使得应用能够在 APK 更新过程中更改签名密钥。为了实现轮转,APK 必须指示新旧签名密钥之间的信任级别。为了支持密钥轮转,我们将 APK 签名方案从 v2 更新为 v3,以允许使用新旧密钥。v3 在 APK 签名分块中添加了有关受支持的 SDK 版本和 proof-of-rotation 结构的信息。 + +v3 APK 签名分块的格式与 v2 相同,APK 的 v3 签名会存储为一个 ID 为 0xf05368c0 的 ID-value。看来 Google 给自己留了后路,以后再增加版本,肯定又是增加新的 ID-value,在高版本的 SDK 设备上解析即可。 + +更多请参考:[Android 文档 APK 签名方案 v3](https://source.android.com/security/apksigning/v3) + +#### 参考 + +[Oracle Signed_JAR_File](https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Signed_JAR_File) + +[https://source.android.com/security/apksigning](https://source.android.com/security/apksigning) + +[分析Android V2新签名打包机制](https://mp.weixin.qq.com/s?__biz=MzI1NjEwMTM4OA==&mid=2651232457&idx=1&sn=90b16c3868a341272b8f1aa26d6c0122&chksm=f1d9e5aac6ae6cbcfaecb07bdd280abf81a46f1937c43f61e69d7f78d64350943356f5443d58&scene=27#wechat_redirect) + +[新一代开源Android渠道包生成工具Walle](https://tech.meituan.com/2017/01/13/android-apk-v2-signature-scheme.html) + diff --git a/blogs/Android/Gradle/Android Gradle 权威指南/读书笔记之一.md b/blogs/Android/Gradle/Android Gradle 权威指南/读书笔记之一.md index 056609c..1f7d44b 100644 --- a/blogs/Android/Gradle/Android Gradle 权威指南/读书笔记之一.md +++ b/blogs/Android/Gradle/Android Gradle 权威指南/读书笔记之一.md @@ -26,7 +26,7 @@ 在写完 [Gradle Plugin 入门指南](https://github.com/Omooo/Android-Notes/blob/master/blogs/Android/Gradle/Gradle_Plugin_Guide.md) 和 [Gradle Plugin 实践之 TinyPng Plugin](https://github.com/Omooo/Android-Notes/blob/master/blogs/Android/Gradle/TinyPngPlugin.md) 之后,总会有一些疑问,如果你仔细阅读过这两篇文章,你甚至会发现文章还有一些错误的说法,比如以下命令中 -q 参数的含义: ``` -./gradlew task -q build +./gradlew -q build ``` -q 的含义并不是表示静默输出,而是…..(下面会讲到 @@ -46,13 +46,13 @@ Gradle 命令一般用于执行一些 Task 。 在前两篇文章,我们也接触到了两个 Gradle 命令,一个是用于执行我们自定义 task 的命令: ``` -./gradlew task myCustomTask +./gradlew myCustomTask ``` 一个是用于执行上传 jar 包依赖的命令: ``` -./gradlew task uploadArchives +./gradlew uploadArchives ``` 这个 uploadArchives Task 其实是 java plugin 里面的 Task,也就是说我们依赖了这个插件: @@ -103,13 +103,13 @@ clean - Deletes the build directory. 比如我们可以执行上面 Android tasks 中的 androidDependencies 来查看项目中的所有依赖: ``` -./gradlew task androidDependencies +./gradlew androidDependencies ``` 当然,我们也可以一次执行多个任务,比如: ``` -./gradlew task clean build +./gradlew clean build ``` 但是,需要主要的是两者的依赖关系,这个我们后面自定义 Task 的时候会讲到。 @@ -446,13 +446,13 @@ myCustomTask.onlyIf{ 我们判断的条件是是否有 build_type 属性,所以当我们还是按往常执行如下命令: ```groovy -./gradlew task -q myCustomTask +./gradlew -q myCustomTask ``` 是不会有任何输出的,那我们怎么做才能会输出呢? ```groovy -./gradlew task -q -Pbuild_type=debug myCustomTask +./gradlew -q -Pbuild_type=debug myCustomTask ``` 加一个参数就好啦,就可以控制任务是否执行了。 diff --git a/blogs/Android/Gradle/Gradle_Plugin_Guide.md b/blogs/Android/Gradle/Gradle_Plugin_Guide.md index a449121..f6698ce 100644 --- a/blogs/Android/Gradle/Gradle_Plugin_Guide.md +++ b/blogs/Android/Gradle/Gradle_Plugin_Guide.md @@ -38,10 +38,10 @@ apply plugin: MyPlugin 在 Plugin 中我们定义了一个 hello 的 Task,然后 apply 这个 Plugin,现在我们可以打开 Terminal 执行这个 Task: ``` -./gradlew task -q hello +./gradlew -q hello ``` -就输出 Hello Plugin~ 啦。这里 -q 参数表示静默执行,后面就是我们定义的 Task name,还是很简单的。 +就输出 Hello Plugin~ 啦。这里 -q 参数表示只输出重要信息的一种日志级别,后面就是我们定义的 Task name,还是很简单的。 如果这个你还有问题,就不用往下看了 :) @@ -163,7 +163,7 @@ top.omooo.my_plugin:my_plugin:1.0 这时候我们我们还没有真正的发布,需要执行: ```java -./gradlew task uploadArchives +./gradlew uploadArchives ``` 执行完后,就可以看到我们根目录生成了一个 repo 文件夹,里面就是我们的插件 jar 包了: @@ -221,7 +221,8 @@ apply plugin: 'top.omooo.my_plugin' 之后,我们肯定会改 MyPlugin 里面的代码的,这时候就要重新上传依赖,用的还是那个命令: ```java -./gradlew task uploadArchives +./gradlew uploadArchives ``` -OK,大功告成~ \ No newline at end of file +OK,大功告成~ + diff --git a/blogs/Android/Gradle/TinyPngPlugin.md b/blogs/Android/Gradle/TinyPngPlugin.md index eb3814f..c5b197b 100644 --- a/blogs/Android/Gradle/TinyPngPlugin.md +++ b/blogs/Android/Gradle/TinyPngPlugin.md @@ -155,7 +155,7 @@ class CrazyPlugin implements Plugin { 回顾上一篇文章,我们说到,既然我们修改了 Plugin 的代码,就要重新生成 jar 包上传依赖,即执行: ``` -./gradlew task uploadArchives +./gradlew uploadArchives ``` 然后在 app 模块的 build.gradle 文件里,我们就可以 apply Plugin 并且添加 Extensions 了: @@ -183,7 +183,7 @@ tinyInfo { 然后我们在执行: ``` -./gradlew task tinyTask +./gradlew tinyTask ``` 就可以输出我们的自定义配置项了。也就是说,我们写的自定义配置项可以在 Task 中获取的。 diff --git a/images/Android/Framework/v2 签名.png b/images/Android/Framework/v2 签名.png new file mode 100644 index 0000000000000000000000000000000000000000..e73b3966afd246003bcd984ccb1e92290d4dc838 GIT binary patch literal 13494 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 literal 0 HcmV?d00001