├── README.md ├── Sample ├── .gitignore ├── .idea │ ├── .name │ ├── codeStyles │ │ ├── Project.xml │ │ └── codeStyleConfig.xml │ ├── gradle.xml │ ├── misc.xml │ ├── runConfigurations.xml │ └── vcs.xml ├── app │ ├── .gitignore │ ├── app-release.apk │ ├── build.gradle │ ├── channleConfig.json │ ├── proguard-rules.pro │ ├── sample.jks │ └── src │ │ ├── androidTest │ │ └── java │ │ │ └── cn │ │ │ └── zengcanxiang │ │ │ └── packpluginSample │ │ │ └── ExampleInstrumentedTest.kt │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ │ └── cn │ │ │ │ └── zengcanxiang │ │ │ │ └── packpluginSample │ │ │ │ └── Main.kt │ │ └── res │ │ │ ├── drawable-v24 │ │ │ └── ic_launcher_foreground.xml │ │ │ ├── drawable │ │ │ └── ic_launcher_background.xml │ │ │ ├── layout │ │ │ └── main.xml │ │ │ ├── mipmap-anydpi-v26 │ │ │ ├── ic_launcher.xml │ │ │ └── ic_launcher_round.xml │ │ │ ├── mipmap-hdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-mdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ └── values │ │ │ ├── colors.xml │ │ │ ├── strings.xml │ │ │ └── styles.xml │ │ └── test │ │ └── java │ │ └── cn │ │ └── zengcanxiang │ │ └── packpluginSample │ │ └── ExampleUnitTest.kt ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle ├── android-pack-plugin ├── .gitignore ├── .idea │ ├── .gitignore │ ├── .name │ ├── codeStyles │ │ └── Project.xml │ ├── compiler.xml │ ├── encodings.xml │ ├── gradle.xml │ ├── jarRepositories.xml │ ├── misc.xml │ ├── uiDesigner.xml │ └── vcs.xml ├── build.gradle ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── local.properties ├── packPlugin │ ├── build.gradle │ └── src │ │ └── main │ │ ├── groovy │ │ └── cn │ │ │ └── zengcanxiang │ │ │ └── packplugin │ │ │ ├── PluginEntranceImpl.groovy │ │ │ ├── PluginExtension.groovy │ │ │ └── task │ │ │ ├── CopySourceTask.groovy │ │ │ ├── DownloadTask.groovy │ │ │ ├── FirmTask.groovy │ │ │ └── MultiChannelTask.groovy │ │ ├── java │ │ └── com │ │ │ └── android │ │ │ ├── apksigner │ │ │ └── core │ │ │ │ ├── ApkSignerEngine.java │ │ │ │ ├── ApkVerifier.java │ │ │ │ ├── DefaultApkSignerEngine.java │ │ │ │ ├── apk │ │ │ │ └── ApkUtils.java │ │ │ │ ├── internal │ │ │ │ ├── apk │ │ │ │ │ ├── v1 │ │ │ │ │ │ ├── DigestAlgorithm.java │ │ │ │ │ │ └── V1SchemeSigner.java │ │ │ │ │ └── v2 │ │ │ │ │ │ ├── ContentDigestAlgorithm.java │ │ │ │ │ │ ├── SignatureAlgorithm.java │ │ │ │ │ │ ├── V2SchemeSigner.java │ │ │ │ │ │ └── V2SchemeVerifier.java │ │ │ │ ├── jar │ │ │ │ │ ├── ManifestWriter.java │ │ │ │ │ └── SignatureFileWriter.java │ │ │ │ ├── util │ │ │ │ │ ├── ByteArrayOutputStreamSink.java │ │ │ │ │ ├── ByteBufferDataSource.java │ │ │ │ │ ├── ByteBufferSink.java │ │ │ │ │ ├── DelegatingX509Certificate.java │ │ │ │ │ ├── MessageDigestSink.java │ │ │ │ │ └── Pair.java │ │ │ │ └── zip │ │ │ │ │ └── ZipUtils.java │ │ │ │ ├── util │ │ │ │ ├── DataSink.java │ │ │ │ ├── DataSource.java │ │ │ │ └── DataSources.java │ │ │ │ └── zip │ │ │ │ └── ZipFormatException.java │ │ │ └── signapk │ │ │ └── SignApk.java │ │ └── resources │ │ └── META-INF │ │ └── gradle-plugins │ │ └── cn.zengcanxiang.androidPackPlugin.properties └── settings.gradle └── walle_cli.jar /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Android-pack-plugin 4 | 5 | Android的打包gradle插件。 6 | 7 | 最少只需要配置加固账号即刻享受加固、签名、注入渠道一条龙服务。 8 | 9 | 优点: 10 | 11 | - 免配置繁琐的加固文件路径:通过获取系统环境,下载对应360加固文件。 12 | - 免配置繁琐的签名信息:通过获取项目配置,自动获取版本信息和签名信息。 13 | - 可通过配置,导出原Apk和mapping文件,项目build产物可以在同一路径下可见。 14 | 15 | ## 使用步骤(可参考sample) 16 | 17 | 1. 项目build.gradle下添加 18 | 19 | ```groovy 20 | repositories{ 21 | jcenter() 22 | } 23 | dependencies{ 24 | classpath 'cn.zengcanxiang:android-pack-plugin:1.0.1' 25 | } 26 | ``` 27 | 28 | 29 | 30 | 2. sync同步之后,app项目apply插件。 31 | 32 | ```groovy 33 | apply plugin: 'cn.zengcanxiang.androidPackPlugin' 34 | 35 | androidPackPlugin { 36 | apkFilePath "${project.projectDir}/app-release.apk" 37 | // 360加固账号用户名,建议配置到本地文件中。 38 | firmAccountName "firmAccountName" 39 | // 360加固账号密码,建议配置到本地文件中。 40 | firmAccountPwd "firmAccountPwd" 41 | // waller渠道包的渠道配置文件。具体格式可以参考sample下的config文件 42 | channelConfigFile project.file("${project.projectDir}/channleConfig.json") 43 | // 360加固所需要文件下载地址和加固、渠道文件输出目录,建议配置到本地文件中。 44 | // 比较好兼容团队中mac电脑和Windows电脑不同环境里的尴尬 45 | // 默认为项目的build目录 46 | outPath "outPath" 47 | } 48 | ``` 49 | 50 | 3. 在gradle面板里通过task列表里,android-pack分组里的task来进行操作。 51 | 52 | - downTask 判断当前所在的系统,进行360加固相关文件的下载。如果本地以及存在相关压缩包,则进行解压,减少重复下载。 53 | - firmTask 依赖于downTask。 登录360账号,清除config设置(个人不喜欢添加加固方的功能),然后下载到指定位置下的firmResult目录。 54 | - MultiChannelTask 依赖于firmTask。获取Android项目里build.gradle配置的相关信息,通过这些对加固后的apk进行签名和渠道注入。 55 | 56 | ## TODO 57 | 58 | - [ ] 360config高级设置支持(主要是没有高贵的vip体验,无法验证效果是否有效) 59 | - [ ] 持续跟进waller的最新版本 60 | - [ ] 多渠道打包在高版本api好像存在兼容问题 -------------------------------------------------------------------------------- /Sample/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | -------------------------------------------------------------------------------- /Sample/.idea/.name: -------------------------------------------------------------------------------- 1 | AndroidPackPluginSample -------------------------------------------------------------------------------- /Sample/.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | xmlns:android 17 | 18 | ^$ 19 | 20 | 21 | 22 |
23 |
24 | 25 | 26 | 27 | xmlns:.* 28 | 29 | ^$ 30 | 31 | 32 | BY_NAME 33 | 34 |
35 |
36 | 37 | 38 | 39 | .*:id 40 | 41 | http://schemas.android.com/apk/res/android 42 | 43 | 44 | 45 |
46 |
47 | 48 | 49 | 50 | .*:name 51 | 52 | http://schemas.android.com/apk/res/android 53 | 54 | 55 | 56 |
57 |
58 | 59 | 60 | 61 | name 62 | 63 | ^$ 64 | 65 | 66 | 67 |
68 |
69 | 70 | 71 | 72 | style 73 | 74 | ^$ 75 | 76 | 77 | 78 |
79 |
80 | 81 | 82 | 83 | .* 84 | 85 | ^$ 86 | 87 | 88 | BY_NAME 89 | 90 |
91 |
92 | 93 | 94 | 95 | .* 96 | 97 | http://schemas.android.com/apk/res/android 98 | 99 | 100 | ANDROID_ATTRIBUTE_ORDER 101 | 102 |
103 |
104 | 105 | 106 | 107 | .* 108 | 109 | .* 110 | 111 | 112 | BY_NAME 113 | 114 |
115 |
116 |
117 |
118 | 119 | 121 |
122 |
-------------------------------------------------------------------------------- /Sample/.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /Sample/.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 19 | 20 | -------------------------------------------------------------------------------- /Sample/.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | -------------------------------------------------------------------------------- /Sample/.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /Sample/.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Sample/app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /Sample/app/app-release.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zengcanxiang/Android-pack-plugin/a44c4680932ee2cf5cb41478ec7fcba8cd8af7d9/Sample/app/app-release.apk -------------------------------------------------------------------------------- /Sample/app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-android-extensions' 4 | apply plugin: 'cn.zengcanxiang.androidPackPlugin' 5 | 6 | android { 7 | compileSdkVersion 28 8 | 9 | defaultConfig { 10 | applicationId "cn.zengcanxiang.packpluginSample" 11 | minSdkVersion 21 12 | targetSdkVersion 28 13 | versionCode 1 14 | versionName "1.0" 15 | 16 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 17 | } 18 | 19 | buildTypes { 20 | release { 21 | minifyEnabled false 22 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 23 | } 24 | } 25 | 26 | signingConfigs { 27 | release { 28 | keyAlias "sample" 29 | keyPassword "123456" 30 | storeFile file(project.projectDir.absolutePath+"/sample.jks") 31 | storePassword "123456" 32 | } 33 | } 34 | 35 | } 36 | 37 | androidPackPlugin { 38 | Properties properties = new Properties() 39 | InputStream inputStream = project.rootProject.file('local.properties').newDataInputStream() 40 | properties.load(inputStream) 41 | 42 | apkFilePath "${project.projectDir}/app-release.apk" 43 | firmAccountName properties.getProperty("firmAccountName") 44 | firmAccountPwd properties.getProperty("firmAccountPwd") 45 | channelConfigFile project.file("${project.projectDir}/channleConfig.json") 46 | outPath properties.getProperty("outPath") 47 | } 48 | 49 | dependencies { 50 | implementation fileTree(dir: 'libs', include: ['*.jar']) 51 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 52 | implementation 'com.android.support:appcompat-v7:28.0.0' 53 | } 54 | -------------------------------------------------------------------------------- /Sample/app/channleConfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "channelInfoList": [ 3 | { 4 | "channel": "360", 5 | "alias": "360", 6 | "extraInfo": { 7 | "id": 1 8 | } 9 | }, 10 | { 11 | "channel": "Tencent", 12 | "alias": "应用宝", 13 | "extraInfo": { 14 | "id": 2 15 | } 16 | }, 17 | { 18 | "channel": "Huawei", 19 | "alias": "华为", 20 | "extraInfo": { 21 | "id": 3 22 | } 23 | }, 24 | { 25 | "channel": "Xiaomi", 26 | "alias": "小米", 27 | "extraInfo": { 28 | "id": 4 29 | } 30 | }, 31 | { 32 | "channel": "Oppo", 33 | "alias": "oppo", 34 | "extraInfo": { 35 | "id": 5 36 | } 37 | }, 38 | { 39 | "channel": "vivo", 40 | "alias": "vivo", 41 | "extraInfo": { 42 | "id": 6 43 | } 44 | } 45 | ] 46 | } 47 | -------------------------------------------------------------------------------- /Sample/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /Sample/app/sample.jks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zengcanxiang/Android-pack-plugin/a44c4680932ee2cf5cb41478ec7fcba8cd8af7d9/Sample/app/sample.jks -------------------------------------------------------------------------------- /Sample/app/src/androidTest/java/cn/zengcanxiang/packpluginSample/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package cn.zengcanxiang.packpluginSample 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("cn.zengcanxiang.packpluginSample", appContext.packageName) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sample/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /Sample/app/src/main/java/cn/zengcanxiang/packpluginSample/Main.kt: -------------------------------------------------------------------------------- 1 | package cn.zengcanxiang.packpluginSample 2 | 3 | import android.os.Bundle 4 | import androidx.appcompat.app.AppCompatActivity 5 | 6 | class Main : AppCompatActivity() { 7 | override fun onCreate(savedInstanceState: Bundle?) { 8 | super.onCreate(savedInstanceState) 9 | setContentView(R.layout.main) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Sample/app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | 31 | -------------------------------------------------------------------------------- /Sample/app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /Sample/app/src/main/res/layout/main.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 11 | 12 | -------------------------------------------------------------------------------- /Sample/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Sample/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Sample/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zengcanxiang/Android-pack-plugin/a44c4680932ee2cf5cb41478ec7fcba8cd8af7d9/Sample/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /Sample/app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zengcanxiang/Android-pack-plugin/a44c4680932ee2cf5cb41478ec7fcba8cd8af7d9/Sample/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /Sample/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zengcanxiang/Android-pack-plugin/a44c4680932ee2cf5cb41478ec7fcba8cd8af7d9/Sample/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /Sample/app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zengcanxiang/Android-pack-plugin/a44c4680932ee2cf5cb41478ec7fcba8cd8af7d9/Sample/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /Sample/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zengcanxiang/Android-pack-plugin/a44c4680932ee2cf5cb41478ec7fcba8cd8af7d9/Sample/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /Sample/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zengcanxiang/Android-pack-plugin/a44c4680932ee2cf5cb41478ec7fcba8cd8af7d9/Sample/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /Sample/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zengcanxiang/Android-pack-plugin/a44c4680932ee2cf5cb41478ec7fcba8cd8af7d9/Sample/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /Sample/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zengcanxiang/Android-pack-plugin/a44c4680932ee2cf5cb41478ec7fcba8cd8af7d9/Sample/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /Sample/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zengcanxiang/Android-pack-plugin/a44c4680932ee2cf5cb41478ec7fcba8cd8af7d9/Sample/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /Sample/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zengcanxiang/Android-pack-plugin/a44c4680932ee2cf5cb41478ec7fcba8cd8af7d9/Sample/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /Sample/app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #6200EE 4 | #3700B3 5 | #03DAC5 6 | 7 | -------------------------------------------------------------------------------- /Sample/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | AndroidPackPluginSample 3 | 4 | -------------------------------------------------------------------------------- /Sample/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /Sample/app/src/test/java/cn/zengcanxiang/packpluginSample/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package cn.zengcanxiang.packpluginSample 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sample/build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | ext.kotlin_version = '1.3.61' 5 | repositories { 6 | google() 7 | jcenter() 8 | mavenCentral() 9 | 10 | } 11 | dependencies { 12 | classpath 'com.android.tools.build:gradle:3.2.1' 13 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 14 | classpath 'cn.zengcanxiang:android-pack-plugin:1.0.0' 15 | // NOTE: Do not place your application dependencies here; they belong 16 | // in the individual module build.gradle files 17 | } 18 | } 19 | 20 | allprojects { 21 | repositories { 22 | google() 23 | jcenter() 24 | mavenCentral() 25 | } 26 | } 27 | 28 | task clean(type: Delete) { 29 | delete rootProject.buildDir 30 | } 31 | -------------------------------------------------------------------------------- /Sample/gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx1536m 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app's APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Automatically convert third-party libraries to use AndroidX 19 | android.enableJetifier=true 20 | # Kotlin code style for this project: "official" or "obsolete": 21 | kotlin.code.style=official 22 | -------------------------------------------------------------------------------- /Sample/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zengcanxiang/Android-pack-plugin/a44c4680932ee2cf5cb41478ec7fcba8cd8af7d9/Sample/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /Sample/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Apr 10 14:01:29 CST 2020 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip 7 | -------------------------------------------------------------------------------- /Sample/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /Sample/gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /Sample/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name='AndroidPackPluginSample' 2 | include ':app' 3 | -------------------------------------------------------------------------------- /android-pack-plugin/.gitignore: -------------------------------------------------------------------------------- 1 | packPlugin/build/ 2 | repo/ 3 | .gradle/buildOutputCleanup/ 4 | .gradle/ 5 | .idea/caches/build_file_checksums.ser 6 | -------------------------------------------------------------------------------- /android-pack-plugin/.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /workspace.xml -------------------------------------------------------------------------------- /android-pack-plugin/.idea/.name: -------------------------------------------------------------------------------- 1 | pack-plugin -------------------------------------------------------------------------------- /android-pack-plugin/.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 15 | 16 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /android-pack-plugin/.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /android-pack-plugin/.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /android-pack-plugin/.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 19 | 20 | -------------------------------------------------------------------------------- /android-pack-plugin/.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | 29 | 30 | -------------------------------------------------------------------------------- /android-pack-plugin/.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /android-pack-plugin/.idea/uiDesigner.xml: -------------------------------------------------------------------------------- 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 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | -------------------------------------------------------------------------------- /android-pack-plugin/.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /android-pack-plugin/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | jcenter() 4 | mavenCentral() 5 | google() 6 | } 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:3.2.1' 9 | classpath "org.jfrog.buildinfo:build-info-extractor-gradle:4.5.2" 10 | classpath 'com.github.dcendents:android-maven-gradle-plugin:1.4.1' 11 | classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.+' 12 | 13 | } 14 | } 15 | 16 | allprojects { 17 | repositories { 18 | jcenter() 19 | mavenCentral() 20 | google() 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /android-pack-plugin/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zengcanxiang/Android-pack-plugin/a44c4680932ee2cf5cb41478ec7fcba8cd8af7d9/android-pack-plugin/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /android-pack-plugin/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /android-pack-plugin/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /android-pack-plugin/gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /android-pack-plugin/local.properties: -------------------------------------------------------------------------------- 1 | ## This file must *NOT* be checked into Version Control Systems, 2 | # as it contains information specific to your local configuration. 3 | # 4 | # Location of the SDK. This is only used by Gradle. 5 | # For customization when using a Version Control System, please read the 6 | # header note. 7 | #Wed Mar 06 11:24:17 CST 2019 8 | sdk.dir=/Users/liuf/Library/Android/sdk 9 | #bintray.user= 10 | #bintray.apikey= -------------------------------------------------------------------------------- /android-pack-plugin/packPlugin/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'groovy' 2 | apply plugin: 'maven' 3 | apply plugin: 'maven-publish' 4 | apply plugin: 'com.jfrog.bintray' 5 | 6 | repositories { 7 | jcenter() 8 | google() 9 | mavenCentral() 10 | } 11 | 12 | dependencies { 13 | implementation localGroovy() 14 | implementation gradleApi() 15 | implementation 'commons-io:commons-io:2.4' 16 | implementation 'commons-codec:commons-codec:1.6' 17 | implementation 'org.apache.commons:commons-lang3:3.4' 18 | implementation 'com.android.tools.build:gradle:3.2.1' 19 | } 20 | 21 | group 'cn.zengcanxiang' 22 | def artifact = 'android-pack-plugin' 23 | version '1.0.1' 24 | 25 | publishing { 26 | publications { 27 | MyPublication(MavenPublication) { 28 | from components.java 29 | groupId group 30 | artifactId artifact 31 | version version 32 | } 33 | } 34 | } 35 | 36 | task sourcesJar(type: Jar) { 37 | from sourceSets.main.allSource 38 | classifier 'sources' 39 | } 40 | 41 | //task javadocJar(type: Jar) { 42 | // from javadoc 43 | // classifier 'javadoc' 44 | //} 45 | 46 | artifacts { 47 | // archives javadocJar 48 | archives sourcesJar 49 | } 50 | 51 | Properties properties = new Properties() 52 | properties.load(project.rootProject.file('local.properties').newDataInputStream()) 53 | 54 | bintray { 55 | user = properties.getProperty("bintray.user") 56 | key = properties.getProperty("bintray.apikey") 57 | publications = ['MyPublication'] 58 | configurations = ['archives'] 59 | pkg { 60 | repo = 'maven' 61 | name = artifact 62 | userOrg = user 63 | licenses = ['Apache-2.0'] 64 | vcsUrl = 'https://github.com/zengcanxiang/Android-pack-plugin.git' 65 | publicDownloadNumbers = true 66 | } 67 | } 68 | 69 | 70 | uploadArchives { 71 | repositories { 72 | mavenDeployer { 73 | repository(url: uri('../repo')) 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /android-pack-plugin/packPlugin/src/main/groovy/cn/zengcanxiang/packplugin/PluginEntranceImpl.groovy: -------------------------------------------------------------------------------- 1 | package cn.zengcanxiang.packplugin 2 | 3 | import cn.zengcanxiang.packplugin.task.DownloadTask 4 | import cn.zengcanxiang.packplugin.task.FirmTask 5 | import cn.zengcanxiang.packplugin.task.MultiChannelTask 6 | import org.gradle.api.Plugin 7 | import org.gradle.api.Project 8 | 9 | class PluginEntranceImpl implements Plugin { 10 | @Override 11 | void apply(Project project) { 12 | project.extensions.create("androidPackPlugin", PluginExtension) 13 | def downloadTask = project.tasks.create("downTask", DownloadTask) 14 | def firmTask = project.tasks.create("firmTask", FirmTask) 15 | def multiChannelTask = project.tasks.create("multiChannelTask", MultiChannelTask) 16 | 17 | // 设置两个任务之间的依赖 18 | firmTask.dependsOn(downloadTask) 19 | multiChannelTask.dependsOn(firmTask) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /android-pack-plugin/packPlugin/src/main/groovy/cn/zengcanxiang/packplugin/PluginExtension.groovy: -------------------------------------------------------------------------------- 1 | package cn.zengcanxiang.packplugin 2 | 3 | import com.android.build.gradle.BaseExtension 4 | import com.android.build.gradle.internal.dsl.SigningConfig 5 | import org.gradle.api.GradleException 6 | import org.gradle.api.Project 7 | 8 | class PluginExtension { 9 | static File apkFile 10 | static File channelOutputFolder 11 | Boolean isNeedFirm = true 12 | //输出总路径 13 | String outPath 14 | // 加固原文件 15 | String apkFilePath 16 | // 原文件mapping.txt 17 | String mappingPath 18 | //360加固账号 19 | String firmAccountName 20 | //360加固密码 21 | String firmAccountPwd 22 | // 渠道文件路径 23 | File channelConfigFile 24 | // Android SDK 目录 25 | File sdkDir 26 | // 指定的Android build-tools版本 27 | String buildToolsName 28 | //apk签名文件路径 29 | String apkJksPath 30 | //apk签名文件密码 31 | String apkJksStorePwd 32 | //apk签名文件别名 33 | String apkJksAlias 34 | //apk签名文件密码 35 | String apkJksPwd 36 | 37 | static PluginExtension getConfig(Project project) { 38 | def config = project.getExtensions().findByType(PluginExtension.class) 39 | if (config == null) { 40 | throw new GradleException("打包配置为空") 41 | } 42 | if (config.outPath == null || config.outPath.length() == 0) { 43 | config.outPath = project.buildDir 44 | } 45 | return config 46 | } 47 | 48 | def initFirm() { 49 | if (isNeedFirm && (firmAccountName == null || firmAccountPwd == null)) { 50 | throw new GradleException("360加固账号密码没有配置") 51 | } 52 | if (apkFilePath == null || apkFilePath.length() == 0 || !new File(apkFilePath).exists()) { 53 | throw new GradleException("apk文件不存在") 54 | } 55 | } 56 | 57 | def initSignConfig(Project project) { 58 | if (this.apkJksPath == null || this.apkJksAlias == null 59 | || this.apkJksStorePwd == null || this.apkJksPwd == null) { 60 | BaseExtension extension = project.extensions.getByName("android") as BaseExtension 61 | Collection signingConfigs = extension.getSigningConfigs() 62 | signingConfigs.forEach { signingConfig -> 63 | if (signingConfig.name == "release") { 64 | this.apkJksPath = signingConfig.storeFile.absolutePath 65 | this.apkJksAlias = signingConfig.keyAlias 66 | this.apkJksStorePwd = signingConfig.storePassword 67 | this.apkJksPwd = signingConfig.keyPassword 68 | } 69 | } 70 | if (this.apkJksPath == null || this.apkJksAlias == null 71 | || this.apkJksStorePwd == null || this.apkJksPwd == null) { 72 | throw new GradleException("签名配置错误(获取项目配置签名失败),至少需要配置签名和360加固账号相关数据\napkJksPath = $apkJksPath, apkJksAlias = $apkJksAlias, apkJksStorePwd = $apkJksStorePwd, apkJksPwd = $apkJksPwd") 73 | } 74 | } 75 | } 76 | 77 | def initSdkDir(Project project) { 78 | if (sdkDir == null || !sdkDir.exists()) { 79 | Properties properties = new Properties() 80 | InputStream inputStream = project.rootProject.file('local.properties').newDataInputStream() 81 | properties.load(inputStream) 82 | def sdkDirPath = properties.getProperty('sdk.dir') 83 | if (sdkDirPath != null && sdkDirPath.length() > 0) { 84 | sdkDir = new File(sdkDirPath) 85 | } 86 | if (!sdkDir.exists()) { 87 | //去读取环境变量 88 | properties = System.getProperties() 89 | sdkDirPath = properties.getProperty("ANDROID_HOME") 90 | if (sdkDirPath != null && sdkDirPath.length() > 0) { 91 | sdkDir = new File(sdkDirPath) 92 | } 93 | } 94 | if (!sdkDir.exists()) { 95 | throw new GradleException("获取AndroidSDK目录失败(请配置文件或者再local.properties添加sdk_dir或者配置ANDROID_HOME环境变量)") 96 | } 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /android-pack-plugin/packPlugin/src/main/groovy/cn/zengcanxiang/packplugin/task/CopySourceTask.groovy: -------------------------------------------------------------------------------- 1 | package cn.zengcanxiang.packplugin.task 2 | 3 | import cn.zengcanxiang.packplugin.PluginExtension 4 | import com.android.build.gradle.BaseExtension 5 | import org.gradle.api.Action 6 | import org.gradle.api.Project 7 | import org.gradle.api.file.CopySpec 8 | 9 | class CopySourceTask { 10 | private PluginExtension config 11 | private Project project 12 | 13 | CopySourceTask(Project project) { 14 | this.project = project 15 | config = PluginExtension.getConfig(project) 16 | } 17 | 18 | def copySource() { 19 | println("开始复制文件") 20 | def extension = project.extensions.getByName("android") as BaseExtension 21 | def versionName = extension.defaultConfig.versionName 22 | def versionCode = extension.defaultConfig.versionCode 23 | File out = new File(PluginExtension.channelOutputFolder, "${versionName}_${versionCode}_source") 24 | if(config.apkFilePath != null){ 25 | File sourceApk = new File(config.apkFilePath) 26 | if (sourceApk.exists()) { 27 | println("开始复制apk") 28 | copy(sourceApk, out) 29 | } 30 | } 31 | if(config.mappingPath != null){ 32 | File sourceMapping = new File(config.mappingPath) 33 | if (sourceMapping.exists()) { 34 | println("开始复制mapping") 35 | copy(sourceMapping, out) 36 | } 37 | } 38 | } 39 | 40 | private def copy(File source, File out) { 41 | project.copy(new Action() { 42 | @Override 43 | void execute(CopySpec copySpec) { 44 | copySpec.from(source) 45 | .into(out) 46 | } 47 | }) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /android-pack-plugin/packPlugin/src/main/groovy/cn/zengcanxiang/packplugin/task/DownloadTask.groovy: -------------------------------------------------------------------------------- 1 | package cn.zengcanxiang.packplugin.task 2 | 3 | import cn.zengcanxiang.packplugin.PluginExtension 4 | import org.gradle.api.Action 5 | import org.gradle.api.DefaultTask 6 | import org.gradle.api.file.CopySpec 7 | import org.gradle.api.tasks.TaskAction 8 | 9 | class DownloadTask extends DefaultTask { 10 | static final String down_url_mac = "http://down.360safe.com/360Jiagu/360jiagubao_mac.zip" 11 | 12 | static final String down_url_linux = "http://down.360safe.com/360Jiagu/360jiagubao_linux_64.zip" 13 | 14 | static final String down_url_win = "http://down.360safe.com/360Jiagu/360jiagubao_windows_32.zip" 15 | 16 | String downUrl = down_url_mac 17 | 18 | // private def walle_cli_url = "https://github.com/Meituan-Dianping/walle/releases/download/v1.1.6/walle-cli-all.jar" 19 | //TODO 由于美团官方暂时没有提供最新的jar。所以下载一个第三方编译的 20 | private def walle_cli_url = "https://github.com/zengcanxiang/Android-pack-plugin/blob/master/walle_cli.jar" 21 | 22 | private File firmZipFile 23 | 24 | private File firmJarParentPath 25 | 26 | private final def firmJarPath = "jiagu/jiagu.jar" 27 | 28 | private PluginExtension config 29 | 30 | DownloadTask() { 31 | group = "android-pack" 32 | description = "下载必要的文件(包含360加固和walle-cli.jar)" 33 | config = PluginExtension.getConfig(project) 34 | } 35 | 36 | private def initConfig() { 37 | firmZipFile = new File(config.outPath, "360加固文件压缩包.zip") 38 | firmJarParentPath = new File(config.outPath, "360") 39 | def os = System.getProperty("os.name").toLowerCase() 40 | if (os.contains("linux")) { 41 | downUrl = down_url_linux 42 | } else if (os.contains("mac")) { 43 | downUrl = down_url_mac 44 | } else { 45 | downUrl = down_url_win 46 | } 47 | } 48 | 49 | @TaskAction 50 | def download() { 51 | initConfig() 52 | if (!isNeedDownload()) { 53 | println("检测到本地已存在360相关文件") 54 | } else { 55 | downLoadFile(downUrl, firmZipFile) 56 | unZip() 57 | } 58 | def walleFile = new File(config.outPath, "walle_cli.jar") 59 | if (!walleFile.exists()) { 60 | downLoadFile(walle_cli_url, walleFile) 61 | } 62 | } 63 | 64 | private def unZip() { 65 | println("开始解压文件") 66 | project.copy(new Action() { 67 | @Override 68 | void execute(CopySpec copySpec) { 69 | copySpec.from(project.zipTree(firmZipFile)) 70 | .into(firmJarParentPath) 71 | println("解压文件结束") 72 | } 73 | }) 74 | } 75 | 76 | private def downLoadFile(String downUrl, File saveFile) { 77 | println("下载文件:$downUrl") 78 | def connection = new URL(downUrl).openStream() 79 | def stream2 = new URL(downUrl).openConnection() 80 | def total = stream2.getContentLength() 81 | def len 82 | def hasRead = 0 83 | byte[] arr = new byte[1024 * 5] 84 | def out = new FileOutputStream(saveFile) 85 | def lastResult = 0 86 | while ((len = connection.read(arr)) != -1) { 87 | out.write(arr, 0, len) 88 | hasRead += len 89 | def decimal = hasRead / total * 100 + "" 90 | 91 | if (decimal != "100") 92 | decimal = decimal.substring(0, decimal.indexOf(".")) 93 | 94 | if (lastResult == Integer.parseInt(decimal)) { 95 | lastResult++ 96 | println("下载进度:" + decimal + "%") 97 | } 98 | } 99 | connection.close() 100 | out.close() 101 | println("下载完成") 102 | } 103 | 104 | private Boolean isNeedDownload() { 105 | def firmJar = new File(firmJarParentPath, firmJarPath) 106 | if (!firmJar.exists()) { 107 | if (!firmZipFile.exists()) { 108 | return true 109 | } else { 110 | println("检测到本地已存在下载的压缩包") 111 | unZip() 112 | } 113 | } 114 | return false 115 | } 116 | 117 | public File getFirmZipFile() { 118 | return firmZipFile 119 | } 120 | 121 | public File getFirmJarParentPath() { 122 | return firmJarParentPath 123 | } 124 | 125 | public String getDownUrl() { 126 | return downUrl 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /android-pack-plugin/packPlugin/src/main/groovy/cn/zengcanxiang/packplugin/task/FirmTask.groovy: -------------------------------------------------------------------------------- 1 | package cn.zengcanxiang.packplugin.task 2 | 3 | import cn.zengcanxiang.packplugin.PluginExtension 4 | import groovy.io.FileType 5 | import org.gradle.api.DefaultTask 6 | import org.gradle.api.GradleException 7 | import org.gradle.api.tasks.TaskAction 8 | 9 | class FirmTask extends DefaultTask { 10 | private PluginExtension config 11 | 12 | File jar 13 | File firmJarParentPath 14 | private def firmJarPath = "jiagu/jiagu.jar" 15 | 16 | FirmTask() { 17 | group = "android-pack" 18 | description = "执行360加固" 19 | config = PluginExtension.getConfig(project) 20 | } 21 | 22 | @TaskAction 23 | def firm() { 24 | firmJarParentPath = new File(config.outPath, "360") 25 | jar = new File(firmJarParentPath, firmJarPath) 26 | if (!config.isNeedFirm || !login()) { 27 | return 28 | } 29 | config.initFirm() 30 | clearFirmService() 31 | println("开始360加固") 32 | def firmResultPath = new File(new File(config.outPath, "firmResult"), 33 | new Date().format("yyyy_MM_dd_HH_mm_ss") 34 | ) 35 | firmResultPath.mkdirs() 36 | def firmShell = "java -jar $jar.absolutePath -jiagu $config.apkFilePath $firmResultPath.absolutePath" 37 | def out = new StringBuilder(), err = new StringBuilder() 38 | // 10分钟的执行时间 39 | executeShell(firmShell, out, err, 1000 * 60 * 10) 40 | println("判断360加固是否完成") 41 | if (err.length() > 0) { 42 | println(err.toString()) 43 | if (!err.contains("error=13, Permission denied")) { 44 | println("加固 失败") 45 | return 46 | } 47 | } 48 | if (out.length() <= 0 || !(out.contains("已加固") || out.contains("任务完成"))) { 49 | println("加固 验证成功条件不符合,可能存在失败情况") 50 | println(out.toString()) 51 | } 52 | println("加固 完成") 53 | firmResultPath.eachFileMatch(FileType.FILES, ~/.*\.apk/) { 54 | PluginExtension.apkFile = it 55 | } 56 | } 57 | 58 | private Boolean login() { 59 | if (!jar.exists()) { 60 | def os = System.getProperty("os.name").toLowerCase() 61 | if (os.contains("linux")) { 62 | // 360加固linux 的文件夹里面的摆放和其他的不一样,需要处理 63 | firmJarParentPath.eachFile { child -> 64 | "mv ${new File(child, "jiagu").absolutePath} $child.parent".execute() 65 | } 66 | } 67 | } 68 | String loginShell = "java -jar $jar.absolutePath -login $config.firmAccountName $config.firmAccountPwd" 69 | def out = new StringBuilder(), err = new StringBuilder() 70 | executeShell(loginShell, out, err, 5000) 71 | if (out.length() <= 0 || !out.contains("login success")) { 72 | println(out.toString()) 73 | println(err.toString()) 74 | println(loginShell) 75 | throw new GradleException("加固 登录失败") 76 | } 77 | return true 78 | } 79 | 80 | private def clearFirmService() { 81 | println("加固 清除打包额外配置") 82 | def clearFirmServiceShell = "java -jar $jar.absolutePath -config -nocert" 83 | clearFirmServiceShell.execute() 84 | } 85 | 86 | static def executeShell(String shellStr, 87 | StringBuilder out, 88 | StringBuilder err, 89 | int millis) { 90 | def proc = shellStr.execute() 91 | proc.consumeProcessOutput(out, err) 92 | proc.waitForOrKill(millis) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /android-pack-plugin/packPlugin/src/main/groovy/cn/zengcanxiang/packplugin/task/MultiChannelTask.groovy: -------------------------------------------------------------------------------- 1 | package cn.zengcanxiang.packplugin.task 2 | 3 | import cn.zengcanxiang.packplugin.PluginExtension 4 | import com.android.apksigner.core.ApkVerifier 5 | import com.android.apksigner.core.internal.util.ByteBufferDataSource 6 | import com.android.apksigner.core.util.DataSource 7 | import com.android.build.gradle.BaseExtension 8 | import groovy.io.FileType 9 | import org.apache.commons.io.IOUtils 10 | import org.gradle.api.DefaultTask 11 | import org.gradle.api.GradleException 12 | import org.gradle.api.tasks.TaskAction 13 | 14 | import java.nio.ByteBuffer 15 | import java.nio.channels.FileChannel 16 | 17 | class MultiChannelTask extends DefaultTask { 18 | 19 | File zipAlignFile 20 | 21 | File signFile 22 | 23 | PluginExtension config 24 | 25 | BaseExtension extension 26 | 27 | MultiChannelTask() { 28 | group = "android-pack" 29 | description = "注入多渠道" 30 | config = PluginExtension.getConfig(project) 31 | } 32 | 33 | @TaskAction 34 | def multiChannel() { 35 | println("多渠道注入任务开始") 36 | if (config.channelConfigFile == null || !config.channelConfigFile.exists()) { 37 | println("多渠道打包配置文件不存在,将不执行渠道注入任务") 38 | return 39 | } 40 | 41 | def androidExtensions = project.extensions.getByName("android") 42 | if( androidExtensions != null instanceof BaseExtension){ 43 | extension = androidExtensions as BaseExtension 44 | }else{ 45 | println("当前不在android工程内。无法获取项目版本相关信息和android签名工具路径") 46 | return 47 | } 48 | 49 | def apkFile = PluginExtension.apkFile 50 | if (apkFile == null) { 51 | apkFile = new File(config.apkFilePath) 52 | } 53 | if (apkFile == null || !apkFile.exists()) { 54 | throw new GradleException("多渠道原apk:${apkFile}, is not existed!") 55 | } 56 | 57 | Map nameVariantMap = [ 58 | 'appName' : project.name, 59 | 'projectName' : project.rootProject.name, 60 | 'applicationId': extension.defaultConfig.applicationId, 61 | 'versionName' : extension.defaultConfig.versionName, 62 | 'versionCode' : extension.defaultConfig.versionCode.toString() 63 | ] 64 | println("对apk进行签名") 65 | def signApkPath = generateApkSinger(apkFile) 66 | if (config.channelConfigFile != null && config.channelConfigFile.exists()) { 67 | println("开始注入多渠道") 68 | File channelOutputFolderParent = new File( 69 | new File(config.outPath, "channelResult"), 70 | nameVariantMap["applicationId"] 71 | ) 72 | channelOutputFolderParent.mkdirs() 73 | File channelOutputFolder = new File( 74 | channelOutputFolderParent, new Date().format("yyyy-MM-dd-HH-mm-s") 75 | ) 76 | channelOutputFolder.mkdirs() 77 | PluginExtension.channelOutputFolder = channelOutputFolder 78 | generateChannelApkByConfigFile(config.channelConfigFile, 79 | signApkPath, 80 | channelOutputFolder, 81 | nameVariantMap 82 | ) 83 | } 84 | } 85 | 86 | private def generateChannelApkByConfigFile(File configFile, 87 | String apkFile, 88 | File channelOutputFolder, 89 | Map nameVariantMap 90 | ) { 91 | def walleJarFile = new File(config.outPath, "walle_cli.jar") 92 | if (!walleJarFile.exists()) { 93 | println("请下载walle_cli.jar文件") 94 | return 95 | } 96 | def writeChannelShell = "java -jar $walleJarFile.absolutePath batch2 -f $configFile.absolutePath $apkFile $channelOutputFolder.absolutePath" 97 | def out = new StringBuilder(), err = new StringBuilder() 98 | println("注入渠道命令为:$writeChannelShell") 99 | FirmTask.executeShell(writeChannelShell, out, err, 1000 * 60 * 10) 100 | new CopySourceTask(project).copySource() 101 | } 102 | 103 | String generateApkSinger(File apkFile) { 104 | def apkPath = apkFile.absolutePath 105 | 106 | getBuildPath(extension.buildToolsVersion) 107 | config.initSignConfig(project) 108 | String zip_aligned_apk_path = apkPath.substring(0, apkPath.length() - 4) + "_zip.apk" 109 | String signed_apk_path = zip_aligned_apk_path.substring(0, zip_aligned_apk_path.length() - 4) + "_signer.apk" 110 | def out = new StringBuilder(), err = new StringBuilder() 111 | // APK zip对齐命令 xxx/zipalign -v 4 xx.apk xx_aligned.apk 112 | def zipAlignShell = "$zipAlignFile.absolutePath -v 4 $apkPath $zip_aligned_apk_path" 113 | //APK 签名命令 114 | def signedShell = "$signFile.absolutePath sign --ks $config.apkJksPath --ks-key-alias $config.apkJksAlias --ks-pass pass:$config.apkJksStorePwd --key-pass pass:$config.apkJksPwd --out $signed_apk_path $zip_aligned_apk_path" 115 | println("对齐命令为:$zipAlignShell") 116 | FirmTask.executeShell(zipAlignShell, out, err, 1000 * 60 * 10) 117 | if (err != null && err.length() > 0) { 118 | println("对齐错误:$zipAlignShell") 119 | println(err.toString()) 120 | throw new GradleException(err.toString()) 121 | } 122 | println("签名命令为:$signedShell") 123 | FirmTask.executeShell(signedShell, out, err, 1000 * 60 * 10) 124 | if (err != null && err.length() > 0) { 125 | println("签名错误:$signedShell") 126 | println(err.toString()) 127 | throw new GradleException(err.toString()) 128 | } 129 | checkV2Signature(project.file(signed_apk_path)) 130 | return signed_apk_path 131 | } 132 | 133 | def getBuildPath(String buildVersion) { 134 | config.initSdkDir(project) 135 | def buildToolParent = new File(config.sdkDir, "build-tools") 136 | File apkBuild 137 | if (config.buildToolsName != null && config.buildToolsName.length() > 0) { 138 | apkBuild = new File(buildToolParent, config.buildToolsName) 139 | } else { 140 | apkBuild = new File(buildToolParent, buildVersion) 141 | } 142 | println("获取的sdk build-tools目录为:$apkBuild.absolutePath") 143 | if (apkBuild.exists()) { 144 | apkBuild.eachFile { childFile -> 145 | if (childFile.name.contains("zipalign")) { 146 | zipAlignFile = childFile 147 | } 148 | if (childFile.name.contains("apksigner")) { 149 | signFile = childFile 150 | } 151 | } 152 | } 153 | // 如果这两个有一个为空 则去遍历android_home/build-tools/目录 154 | if (zipAlignFile == null || signFile == null) { 155 | buildToolParent.eachFileRecurse(FileType.DIRECTORIES) { dir -> 156 | dir.eachFile { childFile -> 157 | if (childFile.name.contains("zipalign")) { 158 | zipAlignFile = childFile 159 | } 160 | if (childFile.name.contains("apksigner")) { 161 | signFile = childFile 162 | } 163 | } 164 | } 165 | if (zipAlignFile == null || signFile == null) { 166 | throw new GradleException("无法找到build_tools工具,请下载最新的build_tools工具") 167 | } 168 | } 169 | } 170 | 171 | private static def checkV2Signature(File apkFile) { 172 | println("检查apk v2签名空间") 173 | FileInputStream fIn = null 174 | FileChannel fChan = null 175 | try { 176 | fIn = new FileInputStream(apkFile) 177 | fChan = fIn.getChannel() 178 | long fSize = fChan.size() 179 | ByteBuffer byteBuffer = ByteBuffer.allocate((int) fSize) 180 | fChan.read(byteBuffer) 181 | byteBuffer.rewind() 182 | DataSource dataSource = new ByteBufferDataSource(byteBuffer) 183 | ApkVerifier apkVerifier = new ApkVerifier() 184 | ApkVerifier.Result result = apkVerifier.verify(dataSource, 0) 185 | if (!result.verified || !result.verifiedUsingV2Scheme) { 186 | throw new GradleException("${apkFile} has no v2 signature in Apk Signing Block!") 187 | } 188 | } catch (IOException ignore) { 189 | ignore.printStackTrace() 190 | } finally { 191 | IOUtils.closeQuietly(fChan) 192 | IOUtils.closeQuietly(fIn) 193 | } 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /android-pack-plugin/packPlugin/src/main/java/com/android/apksigner/core/ApkSignerEngine.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.android.apksigner.core; 18 | 19 | import com.android.apksigner.core.util.DataSink; 20 | import com.android.apksigner.core.util.DataSource; 21 | 22 | import java.io.Closeable; 23 | import java.io.IOException; 24 | import java.security.InvalidKeyException; 25 | import java.security.SignatureException; 26 | import java.util.List; 27 | 28 | /** 29 | * APK signing logic which is independent of how input and output APKs are stored, parsed, and 30 | * generated. 31 | * 32 | *

Operating Model

33 | * 34 | * The abstract operating model is that there is an input APK which is being signed, thus producing 35 | * an output APK. In reality, there may be just an output APK being built from scratch, or the input APK and 36 | * the output APK may be the same file. Because this engine does not deal with reading and writing 37 | * files, it can handle all of these scenarios. 38 | * 39 | *

The engine is stateful and thus cannot be used for signing multiple APKs. However, once 40 | * the engine signed an APK, the engine can be used to re-sign the APK after it has been modified. 41 | * This may be more efficient than signing the APK using a new instance of the engine. See 42 | * Incremental Operation. 43 | * 44 | *

In the engine's operating model, a signed APK is produced as follows. 45 | *

    46 | *
  1. JAR entries to be signed are output,
  2. 47 | *
  3. JAR archive is signed using JAR signing, thus adding the so-called v1 signature to the 48 | * output,
  4. 49 | *
  5. JAR archive is signed using APK Signature Scheme v2, thus adding the so-called v2 signature 50 | * to the output.
  6. 51 | *
52 | * 53 | *

The input APK may contain JAR entries which, depending on the engine's configuration, may or 54 | * may not be output (e.g., existing signatures may need to be preserved or stripped) or which the 55 | * engine will overwrite as part of signing. The engine thus offers {@link #inputJarEntry(String)} 56 | * which tells the client whether the input JAR entry needs to be output. This avoids the need for 57 | * the client to hard-code the aspects of APK signing which determine which parts of input must be 58 | * ignored. Similarly, the engine offers {@link #inputApkSigningBlock(DataSource)} to help the 59 | * client avoid dealing with preserving or stripping APK Signature Scheme v2 signature of the input 60 | * APK. 61 | * 62 | *

To use the engine to sign an input APK (or a collection of JAR entries), follow these 63 | * steps: 64 | *

    65 | *
  1. Obtain a new instance of the engine -- engine instances are stateful and thus cannot be used 66 | * for signing multiple APKs.
  2. 67 | *
  3. Locate the input APK's APK Signing Block and provide it to 68 | * {@link #inputApkSigningBlock(DataSource)}.
  4. 69 | *
  5. For each JAR entry in the input APK, invoke {@link #inputJarEntry(String)} to determine 70 | * whether this entry should be output. The engine may request to inspect the entry.
  6. 71 | *
  7. For each output JAR entry, invoke {@link #outputJarEntry(String)} which may request to 72 | * inspect the entry.
  8. 73 | *
  9. Once all JAR entries have been output, invoke {@link #outputJarEntries()} which may request 74 | * that additional JAR entries are output. These entries comprise the output APK's JAR 75 | * signature.
  10. 76 | *
  11. Locate the ZIP Central Directory and ZIP End of Central Directory sections in the output and 77 | * invoke {@link #outputZipSections(DataSource, DataSource, DataSource)} which may request that 78 | * an APK Signature Block is inserted before the ZIP Central Directory. The block contains the 79 | * output APK's APK Signature Scheme v2 signature.
  12. 80 | *
  13. Invoke {@link #outputDone()} to signal that the APK was output in full. The engine will 81 | * confirm that the output APK is signed.
  14. 82 | *
  15. Invoke {@link #close()} to signal that the engine will no longer be used. This lets the 83 | * engine free any resources it no longer needs. 84 | *
85 | * 86 | *

Some invocations of the engine may provide the client with a task to perform. The client is 87 | * expected to perform all requested tasks before proceeding to the next stage of signing. See 88 | * documentation of each method about the deadlines for performing the tasks requested by the 89 | * method. 90 | * 91 | *

Incremental Operation

92 | * 93 | * The engine supports incremental operation where a signed APK is produced, then modified and 94 | * re-signed. This may be useful for IDEs, where an app is frequently re-signed after small changes 95 | * by the developer. Re-signing may be more efficient than signing from scratch. 96 | * 97 | *

To use the engine in incremental mode, keep notifying the engine of changes to the APK through 98 | * {@link #inputApkSigningBlock(DataSource)}, {@link #inputJarEntry(String)}, 99 | * {@link #inputJarEntryRemoved(String)}, {@link #outputJarEntry(String)}, 100 | * and {@link #outputJarEntryRemoved(String)}, perform the tasks requested by the engine through 101 | * these methods, and, when a new signed APK is desired, run through steps 5 onwards to re-sign the 102 | * APK. 103 | * 104 | *

Output-only Operation

105 | * 106 | * The engine's abstract operating model consists of an input APK and an output APK. However, it is 107 | * possible to use the engine in output-only mode where the engine's {@code input...} methods are 108 | * not invoked. In this mode, the engine has less control over output because it cannot request that 109 | * some JAR entries are not output. Nevertheless, the engine will attempt to make the output APK 110 | * signed and will report an error if cannot do so. 111 | */ 112 | public interface ApkSignerEngine extends Closeable { 113 | 114 | /** 115 | * Indicates to this engine that the input APK contains the provided APK Signing Block. The 116 | * block may contain signatures of the input APK, such as APK Signature Scheme v2 signatures. 117 | * 118 | * @param apkSigningBlock APK signing block of the input APK. The provided data source is 119 | * guaranteed to not be used by the engine after this method terminates. 120 | * 121 | * @throws IllegalStateException if this engine is closed 122 | */ 123 | void inputApkSigningBlock(DataSource apkSigningBlock) throws IllegalStateException; 124 | 125 | /** 126 | * Indicates to this engine that the specified JAR entry was encountered in the input APK. 127 | * 128 | *

When an input entry is updated/changed, it's OK to not invoke 129 | * {@link #inputJarEntryRemoved(String)} before invoking this method. 130 | * 131 | * @return instructions about how to proceed with this entry 132 | * 133 | * @throws IllegalStateException if this engine is closed 134 | */ 135 | InputJarEntryInstructions inputJarEntry(String entryName) throws IllegalStateException; 136 | 137 | /** 138 | * Indicates to this engine that the specified JAR entry was output. 139 | * 140 | *

It is unnecessary to invoke this method for entries added to output by this engine (e.g., 141 | * requested by {@link #outputJarEntries()}) provided the entries were output with exactly the 142 | * data requested by the engine. 143 | * 144 | *

When an already output entry is updated/changed, it's OK to not invoke 145 | * {@link #outputJarEntryRemoved(String)} before invoking this method. 146 | * 147 | * @return request to inspect the entry or {@code null} if the engine does not need to inspect 148 | * the entry. The request must be fulfilled before {@link #outputJarEntries()} is 149 | * invoked. 150 | * 151 | * @throws IllegalStateException if this engine is closed 152 | */ 153 | InspectJarEntryRequest outputJarEntry(String entryName) throws IllegalStateException; 154 | 155 | /** 156 | * Indicates to this engine that the specified JAR entry was removed from the input. It's safe 157 | * to invoke this for entries for which {@link #inputJarEntry(String)} hasn't been invoked. 158 | * 159 | * @return output policy of this JAR entry. The policy indicates how this input entry affects 160 | * the output APK. The client of this engine should use this information to determine 161 | * how the removal of this input APK's JAR entry affects the output APK. 162 | * 163 | * @throws IllegalStateException if this engine is closed 164 | */ 165 | InputJarEntryInstructions.OutputPolicy inputJarEntryRemoved(String entryName) 166 | throws IllegalStateException; 167 | 168 | /** 169 | * Indicates to this engine that the specified JAR entry was removed from the output. It's safe 170 | * to invoke this for entries for which {@link #outputJarEntry(String)} hasn't been invoked. 171 | * 172 | * @throws IllegalStateException if this engine is closed 173 | */ 174 | void outputJarEntryRemoved(String entryName) throws IllegalStateException; 175 | 176 | /** 177 | * Indicates to this engine that all JAR entries have been output. 178 | * 179 | * 180 | * @return request to add JAR signature to the output or {@code null} if there is no need to add 181 | * a JAR signature. The request will contain additional JAR entries to be output. The 182 | * request must be fulfilled before 183 | * {@link #outputZipSections(DataSource, DataSource, DataSource)} is invoked. 184 | * 185 | * @throws InvalidKeyException if a signature could not be generated because a signing key is 186 | * not suitable for generating the signature 187 | * @throws SignatureException if an error occurred while generating the JAR signature 188 | * @throws IllegalStateException if there are unfulfilled requests, such as to inspect some JAR 189 | * entries, or if the engine is closed 190 | */ 191 | OutputJarSignatureRequest outputJarEntries() throws InvalidKeyException, SignatureException; 192 | 193 | /** 194 | * Indicates to this engine that the ZIP sections comprising the output APK have been output. 195 | * 196 | *

The provided data sources are guaranteed to not be used by the engine after this method 197 | * terminates. 198 | * 199 | * @param zipEntries the section of ZIP archive containing Local File Header records and data of 200 | * the ZIP entries. In a well-formed archive, this section starts at the start of the 201 | * archive and extends all the way to the ZIP Central Directory. 202 | * @param zipCentralDirectory ZIP Central Directory section 203 | * @param zipEocd ZIP End of Central Directory (EoCD) record 204 | * 205 | * @return request to add an APK Signing Block to the output or {@code null} if the output must 206 | * not contain an APK Signing Block. The request must be fulfilled before 207 | * {@link #outputDone()} is invoked. 208 | * 209 | * @throws IOException if an I/O error occurs while reading the provided ZIP sections 210 | * @throws InvalidKeyException if a signature could not be generated because a signing key is 211 | * not suitable for generating the signature 212 | * @throws SignatureException if an error occurred while generating the APK's signature 213 | * @throws IllegalStateException if there are unfulfilled requests, such as to inspect some JAR 214 | * entries or to output JAR signature, or if the engine is closed 215 | */ 216 | OutputApkSigningBlockRequest outputZipSections( 217 | DataSource zipEntries, 218 | DataSource zipCentralDirectory, 219 | DataSource zipEocd) throws IOException, InvalidKeyException, SignatureException; 220 | 221 | /** 222 | * Indicates to this engine that the signed APK was output. 223 | * 224 | *

This does not change the output APK. The method helps the client confirm that the current 225 | * output is signed. 226 | * 227 | * @throws IllegalStateException if there are unfulfilled requests, such as to inspect some JAR 228 | * entries or to output signatures, or if the engine is closed 229 | */ 230 | void outputDone() throws IllegalStateException; 231 | 232 | /** 233 | * Indicates to this engine that it will no longer be used. Invoking this on an already closed 234 | * engine is OK. 235 | * 236 | *

This does not change the output APK. For example, if the output APK is not yet fully 237 | * signed, it will remain so after this method terminates. 238 | */ 239 | @Override 240 | void close(); 241 | 242 | /** 243 | * Instructions about how to handle an input APK's JAR entry. 244 | * 245 | *

The instructions indicate whether to output the entry (see {@link #getOutputPolicy()}) and 246 | * may contain a request to inspect the entry (see {@link #getInspectJarEntryRequest()}), in 247 | * which case the request must be fulfilled before {@link ApkSignerEngine#outputJarEntries()} is 248 | * invoked. 249 | */ 250 | public static class InputJarEntryInstructions { 251 | private final OutputPolicy mOutputPolicy; 252 | private final InspectJarEntryRequest mInspectJarEntryRequest; 253 | 254 | /** 255 | * Constructs a new {@code InputJarEntryInstructions} instance with the provided entry 256 | * output policy and without a request to inspect the entry. 257 | */ 258 | public InputJarEntryInstructions(OutputPolicy outputPolicy) { 259 | this(outputPolicy, null); 260 | } 261 | 262 | /** 263 | * Constructs a new {@code InputJarEntryInstructions} instance with the provided entry 264 | * output mode and with the provided request to inspect the entry. 265 | * 266 | * @param inspectJarEntryRequest request to inspect the entry or {@code null} if there's no 267 | * need to inspect the entry. 268 | */ 269 | public InputJarEntryInstructions( 270 | OutputPolicy outputPolicy, 271 | InspectJarEntryRequest inspectJarEntryRequest) { 272 | mOutputPolicy = outputPolicy; 273 | mInspectJarEntryRequest = inspectJarEntryRequest; 274 | } 275 | 276 | /** 277 | * Returns the output policy for this entry. 278 | */ 279 | public OutputPolicy getOutputPolicy() { 280 | return mOutputPolicy; 281 | } 282 | 283 | /** 284 | * Returns the request to inspect the JAR entry or {@code null} if there is no need to 285 | * inspect the entry. 286 | */ 287 | public InspectJarEntryRequest getInspectJarEntryRequest() { 288 | return mInspectJarEntryRequest; 289 | } 290 | 291 | /** 292 | * Output policy for an input APK's JAR entry. 293 | */ 294 | public static enum OutputPolicy { 295 | /** Entry must not be output. */ 296 | SKIP, 297 | 298 | /** Entry should be output. */ 299 | OUTPUT, 300 | 301 | /** Entry will be output by the engine. The client can thus ignore this input entry. */ 302 | OUTPUT_BY_ENGINE, 303 | } 304 | } 305 | 306 | /** 307 | * Request to inspect the specified JAR entry. 308 | * 309 | *

The entry's uncompressed data must be provided to the data sink returned by 310 | * {@link #getDataSink()}. Once the entry's data has been provided to the sink, {@link #done()} 311 | * must be invoked. 312 | */ 313 | interface InspectJarEntryRequest { 314 | 315 | /** 316 | * Returns the data sink into which the entry's uncompressed data should be sent. 317 | */ 318 | DataSink getDataSink(); 319 | 320 | /** 321 | * Indicates that entry's data has been provided in full. 322 | */ 323 | void done(); 324 | 325 | /** 326 | * Returns the name of the JAR entry. 327 | */ 328 | String getEntryName(); 329 | } 330 | 331 | /** 332 | * Request to add JAR signature (aka v1 signature) to the output APK. 333 | * 334 | *

Entries listed in {@link #getAdditionalJarEntries()} must be added to the output APK after 335 | * which {@link #done()} must be invoked. 336 | */ 337 | interface OutputJarSignatureRequest { 338 | 339 | /** 340 | * Returns JAR entries that must be added to the output APK. 341 | */ 342 | List getAdditionalJarEntries(); 343 | 344 | /** 345 | * Indicates that the JAR entries contained in this request were added to the output APK. 346 | */ 347 | void done(); 348 | 349 | /** 350 | * JAR entry. 351 | */ 352 | public static class JarEntry { 353 | private final String mName; 354 | private final byte[] mData; 355 | 356 | /** 357 | * Constructs a new {@code JarEntry} with the provided name and data. 358 | * 359 | * @param data uncompressed data of the entry. Changes to this array will not be 360 | * reflected in {@link #getData()}. 361 | */ 362 | public JarEntry(String name, byte[] data) { 363 | mName = name; 364 | mData = data.clone(); 365 | } 366 | 367 | /** 368 | * Returns the name of this ZIP entry. 369 | */ 370 | public String getName() { 371 | return mName; 372 | } 373 | 374 | /** 375 | * Returns the uncompressed data of this JAR entry. 376 | */ 377 | public byte[] getData() { 378 | return mData.clone(); 379 | } 380 | } 381 | } 382 | 383 | /** 384 | * Request to add the specified APK Signing Block to the output APK. APK Signature Scheme v2 385 | * signature(s) of the APK are contained in this block. 386 | * 387 | *

The APK Signing Block returned by {@link #getApkSigningBlock()} must be placed into the 388 | * output APK such that the block is immediately before the ZIP Central Directory, the offset of 389 | * ZIP Central Directory in the ZIP End of Central Directory record must be adjusted 390 | * accordingly, and then {@link #done()} must be invoked. 391 | * 392 | *

If the output contains an APK Signing Block, that block must be replaced by the block 393 | * contained in this request. 394 | */ 395 | interface OutputApkSigningBlockRequest { 396 | 397 | /** 398 | * Returns the APK Signing Block. 399 | */ 400 | byte[] getApkSigningBlock(); 401 | 402 | /** 403 | * Indicates that the APK Signing Block was output as requested. 404 | */ 405 | void done(); 406 | } 407 | } 408 | -------------------------------------------------------------------------------- /android-pack-plugin/packPlugin/src/main/java/com/android/apksigner/core/ApkVerifier.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.android.apksigner.core; 18 | 19 | import com.android.apksigner.core.apk.ApkUtils; 20 | import com.android.apksigner.core.internal.apk.v2.ContentDigestAlgorithm; 21 | import com.android.apksigner.core.internal.apk.v2.SignatureAlgorithm; 22 | import com.android.apksigner.core.internal.apk.v2.V2SchemeVerifier; 23 | import com.android.apksigner.core.util.DataSource; 24 | import com.android.apksigner.core.zip.ZipFormatException; 25 | 26 | import java.io.IOException; 27 | import java.security.cert.X509Certificate; 28 | import java.util.ArrayList; 29 | import java.util.List; 30 | 31 | /** 32 | * APK signature verifier which mimics the behavior of the Android platform. 33 | * 34 | *

The verifier is designed to closely mimic the behavior of Android platforms. This is to enable 35 | * the verifier to be used for checking whether an APK's signatures will verify on Android. 36 | */ 37 | public class ApkVerifier { 38 | 39 | /** 40 | * Verifies the APK's signatures and returns the result of verification. The APK can be 41 | * considered verified iff the result's {@link Result#isVerified()} returns {@code true}. 42 | * The verification result also includes errors, warnings, and information about signers. 43 | * 44 | * @param apk APK file contents 45 | * @param minSdkVersion API Level of the oldest Android platform on which the APK's signatures 46 | * may need to be verified 47 | * 48 | * @throws IOException if an I/O error is encountered while reading the APK 49 | * @throws ZipFormatException if the APK is malformed at ZIP format level 50 | */ 51 | public Result verify(DataSource apk, int minSdkVersion) throws IOException, ZipFormatException { 52 | ApkUtils.ZipSections zipSections = ApkUtils.findZipSections(apk); 53 | 54 | // Attempt to verify the APK using APK Signature Scheme v2 55 | Result result = new Result(); 56 | try { 57 | V2SchemeVerifier.Result v2Result = V2SchemeVerifier.verify(apk, zipSections); 58 | result.mergeFrom(v2Result); 59 | } catch (V2SchemeVerifier.SignatureNotFoundException ignored) {} 60 | if (result.containsErrors()) { 61 | return result; 62 | } 63 | 64 | // TODO: Verify JAR signature if necessary 65 | if (!result.isVerifiedUsingV2Scheme()) { 66 | return result; 67 | } 68 | 69 | // Verified 70 | result.setVerified(); 71 | for (Result.V2SchemeSignerInfo signerInfo : result.getV2SchemeSigners()) { 72 | result.addSignerCertificate(signerInfo.getCertificate()); 73 | } 74 | 75 | return result; 76 | } 77 | 78 | /** 79 | * Result of verifying an APKs signatures. The APK can be considered verified iff 80 | * {@link #isVerified()} returns {@code true}. 81 | */ 82 | public static class Result { 83 | private final List mErrors = new ArrayList<>(); 84 | private final List mWarnings = new ArrayList<>(); 85 | private final List mSignerCerts = new ArrayList<>(); 86 | private final List mV2SchemeSigners = new ArrayList<>(); 87 | 88 | private boolean mVerified; 89 | private boolean mVerifiedUsingV2Scheme; 90 | 91 | /** 92 | * Returns {@code true} if the APK's signatures verified. 93 | */ 94 | public boolean isVerified() { 95 | return mVerified; 96 | } 97 | 98 | private void setVerified() { 99 | mVerified = true; 100 | } 101 | 102 | /** 103 | * Returns {@code true} if the APK's APK Signature Scheme v2 signatures verified. 104 | */ 105 | public boolean isVerifiedUsingV2Scheme() { 106 | return mVerifiedUsingV2Scheme; 107 | } 108 | 109 | /** 110 | * Returns the verified signers' certificates, one per signer. 111 | */ 112 | public List getSignerCertificates() { 113 | return mSignerCerts; 114 | } 115 | 116 | private void addSignerCertificate(X509Certificate cert) { 117 | mSignerCerts.add(cert); 118 | } 119 | 120 | /** 121 | * Returns information about APK Signature Scheme v2 signers associated with the APK's 122 | * signature. 123 | */ 124 | public List getV2SchemeSigners() { 125 | return mV2SchemeSigners; 126 | } 127 | 128 | /** 129 | * Returns errors encountered while verifying the APK's signatures. 130 | */ 131 | public List getErrors() { 132 | return mErrors; 133 | } 134 | 135 | /** 136 | * Returns warnings encountered while verifying the APK's signatures. 137 | */ 138 | public List getWarnings() { 139 | return mWarnings; 140 | } 141 | 142 | private void mergeFrom(V2SchemeVerifier.Result source) { 143 | mVerifiedUsingV2Scheme = source.verified; 144 | mErrors.addAll(source.getErrors()); 145 | mWarnings.addAll(source.getWarnings()); 146 | for (V2SchemeVerifier.Result.SignerInfo signer : source.signers) { 147 | mV2SchemeSigners.add(new V2SchemeSignerInfo(signer)); 148 | } 149 | } 150 | 151 | /** 152 | * Returns {@code true} if an error was encountered while verifying the APK. Any error 153 | * prevents the APK from being considered verified. 154 | */ 155 | public boolean containsErrors() { 156 | if (!mErrors.isEmpty()) { 157 | return true; 158 | } 159 | if (!mV2SchemeSigners.isEmpty()) { 160 | for (V2SchemeSignerInfo signer : mV2SchemeSigners) { 161 | if (signer.containsErrors()) { 162 | return true; 163 | } 164 | } 165 | } 166 | 167 | return false; 168 | } 169 | 170 | /** 171 | * Information about an APK Signature Scheme v2 signer associated with the APK's signature. 172 | */ 173 | public static class V2SchemeSignerInfo { 174 | private final int mIndex; 175 | private final List mCerts; 176 | 177 | private final List mErrors; 178 | private final List mWarnings; 179 | 180 | private V2SchemeSignerInfo(V2SchemeVerifier.Result.SignerInfo result) { 181 | mIndex = result.index; 182 | mCerts = result.certs; 183 | mErrors = result.getErrors(); 184 | mWarnings = result.getWarnings(); 185 | } 186 | 187 | /** 188 | * Returns this signer's {@code 0}-based index in the list of signers contained in the 189 | * APK's APK Signature Scheme v2 signature. 190 | */ 191 | public int getIndex() { 192 | return mIndex; 193 | } 194 | 195 | /** 196 | * Returns this signer's signing certificate or {@code null} if not available. The 197 | * certificate is guaranteed to be available if no errors were encountered during 198 | * verification (see {@link #containsErrors()}. 199 | * 200 | *

This certificate contains the signer's public key. 201 | */ 202 | public X509Certificate getCertificate() { 203 | return mCerts.isEmpty() ? null : mCerts.get(0); 204 | } 205 | 206 | /** 207 | * Returns this signer's certificates. The first certificate is for the signer's public 208 | * key. An empty list may be returned if an error was encountered during verification 209 | * (see {@link #containsErrors()}). 210 | */ 211 | public List getCertificates() { 212 | return mCerts; 213 | } 214 | 215 | public boolean containsErrors() { 216 | return !mErrors.isEmpty(); 217 | } 218 | 219 | public List getErrors() { 220 | return mErrors; 221 | } 222 | 223 | public List getWarnings() { 224 | return mWarnings; 225 | } 226 | } 227 | } 228 | 229 | /** 230 | * Error or warning encountered while verifying an APK's signatures. 231 | */ 232 | public static enum Issue { 233 | 234 | /** 235 | * Failed to parse the list of signers contained in the APK Signature Scheme v2 signature. 236 | */ 237 | V2_SIG_MALFORMED_SIGNERS("Malformed list of signers"), 238 | 239 | /** 240 | * Failed to parse this signer's signer block contained in the APK Signature Scheme v2 241 | * signature. 242 | */ 243 | V2_SIG_MALFORMED_SIGNER("Malformed signer block"), 244 | 245 | /** 246 | * Public key embedded in the APK Signature Scheme v2 signature of this signer could not be 247 | * parsed. 248 | * 249 | *

    250 | *
  • Parameter 1: error details ({@code Throwable})
  • 251 | *
252 | */ 253 | V2_SIG_MALFORMED_PUBLIC_KEY("Malformed public key: %1$s"), 254 | 255 | /** 256 | * This APK Signature Scheme v2 signer's certificate could not be parsed. 257 | * 258 | *
    259 | *
  • Parameter 1: index ({@code 0}-based) of the certificate in the signer's list of 260 | * certificates ({@code Integer})
  • 261 | *
  • Parameter 2: sequence number ({@code 1}-based) of the certificate in the signer's 262 | * list of certificates ({@code Integer})
  • 263 | *
  • Parameter 3: error details ({@code Throwable})
  • 264 | *
265 | */ 266 | V2_SIG_MALFORMED_CERTIFICATE("Malformed certificate #%2$d: %3$s"), 267 | 268 | /** 269 | * Failed to parse this signer's signature record contained in the APK Signature Scheme v2 270 | * signature. 271 | * 272 | *
    273 | *
  • Parameter 1: record number (first record is {@code 1}) ({@code Integer})
  • 274 | *
275 | */ 276 | V2_SIG_MALFORMED_SIGNATURE("Malformed APK Signature Scheme v2 signature record #%1$d"), 277 | 278 | /** 279 | * Failed to parse this signer's digest record contained in the APK Signature Scheme v2 280 | * signature. 281 | * 282 | *
    283 | *
  • Parameter 1: record number (first record is {@code 1}) ({@code Integer})
  • 284 | *
285 | */ 286 | V2_SIG_MALFORMED_DIGEST("Malformed APK Signature Scheme v2 digest record #%1$d"), 287 | 288 | /** 289 | * This APK Signature Scheme v2 signer contains a malformed additional attribute. 290 | * 291 | *
    292 | *
  • Parameter 1: attribute number (first attribute is {@code 1}) {@code Integer})
  • 293 | *
294 | */ 295 | V2_SIG_MALFORMED_ADDITIONAL_ATTRIBUTE("Malformed additional attribute #%1$d"), 296 | 297 | /** 298 | * APK Signature Scheme v2 signature contains no signers. 299 | */ 300 | V2_SIG_NO_SIGNERS("No signers in APK Signature Scheme v2 signature"), 301 | 302 | /** 303 | * This APK Signature Scheme v2 signer contains a signature produced using an unknown 304 | * algorithm. 305 | * 306 | *
    307 | *
  • Parameter 1: algorithm ID ({@code Integer})
  • 308 | *
309 | */ 310 | V2_SIG_UNKNOWN_SIG_ALGORITHM("Unknown signature algorithm: %1$#x"), 311 | 312 | /** 313 | * This APK Signature Scheme v2 signer contains an unknown additional attribute. 314 | * 315 | *
    316 | *
  • Parameter 1: attribute ID ({@code Integer})
  • 317 | *
318 | */ 319 | V2_SIG_UNKNOWN_ADDITIONAL_ATTRIBUTE("Unknown additional attribute: ID %1$#x"), 320 | 321 | /** 322 | * An exception was encountered while verifying APK Signature Scheme v2 signature of this 323 | * signer. 324 | * 325 | *
    326 | *
  • Parameter 1: signature algorithm ({@link SignatureAlgorithm})
  • 327 | *
  • Parameter 2: exception ({@code Throwable})
  • 328 | *
329 | */ 330 | V2_SIG_VERIFY_EXCEPTION("Failed to verify %1$s signature: %2$s"), 331 | 332 | /** 333 | * APK Signature Scheme v2 signature over this signer's signed-data block did not verify. 334 | * 335 | *
    336 | *
  • Parameter 1: signature algorithm ({@link SignatureAlgorithm})
  • 337 | *
338 | */ 339 | V2_SIG_DID_NOT_VERIFY("%1$s signature over signed-data did not verify"), 340 | 341 | /** 342 | * This APK Signature Scheme v2 signer offers no signatures. 343 | */ 344 | V2_SIG_NO_SIGNATURES("No signatures"), 345 | 346 | /** 347 | * This APK Signature Scheme v2 signer offers signatures but none of them are supported. 348 | */ 349 | V2_SIG_NO_SUPPORTED_SIGNATURES("No supported signatures"), 350 | 351 | /** 352 | * This APK Signature Scheme v2 signer offers no certificates. 353 | */ 354 | V2_SIG_NO_CERTIFICATES("No certificates"), 355 | 356 | /** 357 | * This APK Signature Scheme v2 signer's public key listed in the signer's certificate does 358 | * not match the public key listed in the signatures record. 359 | * 360 | *
    361 | *
  • Parameter 1: hex-encoded public key from certificate ({@code String})
  • 362 | *
  • Parameter 2: hex-encoded public key from signatures record ({@code String})
  • 363 | *
364 | */ 365 | V2_SIG_PUBLIC_KEY_MISMATCH_BETWEEN_CERTIFICATE_AND_SIGNATURES_RECORD( 366 | "Public key mismatch between certificate and signature record: <%1$s> vs <%2$s>"), 367 | 368 | /** 369 | * This APK Signature Scheme v2 signer's signature algorithms listed in the signatures 370 | * record do not match the signature algorithms listed in the signatures record. 371 | * 372 | *
    373 | *
  • Parameter 1: signature algorithms from signatures record ({@code List})
  • 374 | *
  • Parameter 2: signature algorithms from digests record ({@code List})
  • 375 | *
376 | */ 377 | V2_SIG_SIG_ALG_MISMATCH_BETWEEN_SIGNATURES_AND_DIGESTS_RECORDS( 378 | "Signature algorithms mismatch between signatures and digests records" 379 | + ": %1$s vs %2$s"), 380 | 381 | /** 382 | * The APK's digest does not match the digest contained in the APK Signature Scheme v2 383 | * signature. 384 | * 385 | *
    386 | *
  • Parameter 1: content digest algorithm ({@link ContentDigestAlgorithm})
  • 387 | *
  • Parameter 2: hex-encoded expected digest of the APK ({@code String})
  • 388 | *
  • Parameter 3: hex-encoded actual digest of the APK ({@code String})
  • 389 | *
390 | */ 391 | V2_SIG_APK_DIGEST_DID_NOT_VERIFY( 392 | "APK integrity check failed. %1$s digest mismatch." 393 | + " Expected: <%2$s>, actual: <%3$s>"), 394 | 395 | /** 396 | * APK Signing Block contains an unknown entry. 397 | * 398 | *
    399 | *
  • Parameter 1: entry ID ({@code Integer})
  • 400 | *
401 | */ 402 | APK_SIG_BLOCK_UNKNOWN_ENTRY_ID("APK Signing Block contains unknown entry: ID %1$#x"); 403 | 404 | private final String mFormat; 405 | 406 | private Issue(String format) { 407 | mFormat = format; 408 | } 409 | 410 | /** 411 | * Returns the format string suitable for combining the parameters of this issue into a 412 | * readable string. See {@link java.util.Formatter} for format. 413 | */ 414 | private String getFormat() { 415 | return mFormat; 416 | } 417 | } 418 | 419 | /** 420 | * {@link Issue} with associated parameters. {@link #toString()} produces a readable formatted 421 | * form. 422 | */ 423 | public static class IssueWithParams { 424 | private final Issue mIssue; 425 | private final Object[] mParams; 426 | 427 | /** 428 | * Constructs a new {@code IssueWithParams} of the specified type and with provided 429 | * parameters. 430 | */ 431 | public IssueWithParams(Issue issue, Object[] params) { 432 | mIssue = issue; 433 | mParams = params; 434 | } 435 | 436 | /** 437 | * Returns the type of this issue. 438 | */ 439 | public Issue getIssue() { 440 | return mIssue; 441 | } 442 | 443 | /** 444 | * Returns the parameters of this issue. 445 | */ 446 | public Object[] getParams() { 447 | return mParams.clone(); 448 | } 449 | 450 | /** 451 | * Returns a readable form of this issue. 452 | */ 453 | @Override 454 | public String toString() { 455 | return String.format(mIssue.getFormat(), mParams); 456 | } 457 | } 458 | } 459 | -------------------------------------------------------------------------------- /android-pack-plugin/packPlugin/src/main/java/com/android/apksigner/core/apk/ApkUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.android.apksigner.core.apk; 18 | 19 | import com.android.apksigner.core.internal.util.Pair; 20 | import com.android.apksigner.core.internal.zip.ZipUtils; 21 | import com.android.apksigner.core.util.DataSource; 22 | import com.android.apksigner.core.zip.ZipFormatException; 23 | 24 | import java.io.IOException; 25 | import java.nio.ByteBuffer; 26 | import java.nio.ByteOrder; 27 | 28 | /** 29 | * APK utilities. 30 | */ 31 | public class ApkUtils { 32 | 33 | private ApkUtils() {} 34 | 35 | /** 36 | * Finds the main ZIP sections of the provided APK. 37 | * 38 | * @throws IOException if an I/O error occurred while reading the APK 39 | * @throws ZipFormatException if the APK is malformed 40 | */ 41 | public static ZipSections findZipSections(DataSource apk) 42 | throws IOException, ZipFormatException { 43 | Pair eocdAndOffsetInFile = 44 | ZipUtils.findZipEndOfCentralDirectoryRecord(apk); 45 | if (eocdAndOffsetInFile == null) { 46 | throw new ZipFormatException("ZIP End of Central Directory record not found"); 47 | } 48 | 49 | ByteBuffer eocdBuf = eocdAndOffsetInFile.getFirst(); 50 | long eocdOffset = eocdAndOffsetInFile.getSecond(); 51 | if (ZipUtils.isZip64EndOfCentralDirectoryLocatorPresent(apk, eocdOffset)) { 52 | throw new ZipFormatException("ZIP64 APK not supported"); 53 | } 54 | eocdBuf.order(ByteOrder.LITTLE_ENDIAN); 55 | long cdStartOffset = ZipUtils.getZipEocdCentralDirectoryOffset(eocdBuf); 56 | if (cdStartOffset >= eocdOffset) { 57 | throw new ZipFormatException( 58 | "ZIP Central Directory start offset out of range: " + cdStartOffset 59 | + ". ZIP End of Central Directory offset: " + eocdOffset); 60 | } 61 | 62 | long cdSizeBytes = ZipUtils.getZipEocdCentralDirectorySizeBytes(eocdBuf); 63 | long cdEndOffset = cdStartOffset + cdSizeBytes; 64 | if (cdEndOffset > eocdOffset) { 65 | throw new ZipFormatException( 66 | "ZIP Central Directory overlaps with End of Central Directory" 67 | + ". CD end: " + cdEndOffset 68 | + ", EoCD start: " + eocdOffset); 69 | } 70 | 71 | int cdRecordCount = ZipUtils.getZipEocdCentralDirectoryTotalRecordCount(eocdBuf); 72 | 73 | return new ZipSections( 74 | cdStartOffset, 75 | cdSizeBytes, 76 | cdRecordCount, 77 | eocdOffset, 78 | eocdBuf); 79 | } 80 | 81 | /** 82 | * Information about the ZIP sections of an APK. 83 | */ 84 | public static class ZipSections { 85 | private final long mCentralDirectoryOffset; 86 | private final long mCentralDirectorySizeBytes; 87 | private final int mCentralDirectoryRecordCount; 88 | private final long mEocdOffset; 89 | private final ByteBuffer mEocd; 90 | 91 | public ZipSections( 92 | long centralDirectoryOffset, 93 | long centralDirectorySizeBytes, 94 | int centralDirectoryRecordCount, 95 | long eocdOffset, 96 | ByteBuffer eocd) { 97 | mCentralDirectoryOffset = centralDirectoryOffset; 98 | mCentralDirectorySizeBytes = centralDirectorySizeBytes; 99 | mCentralDirectoryRecordCount = centralDirectoryRecordCount; 100 | mEocdOffset = eocdOffset; 101 | mEocd = eocd; 102 | } 103 | 104 | /** 105 | * Returns the start offset of the ZIP Central Directory. This value is taken from the 106 | * ZIP End of Central Directory record. 107 | */ 108 | public long getZipCentralDirectoryOffset() { 109 | return mCentralDirectoryOffset; 110 | } 111 | 112 | /** 113 | * Returns the size (in bytes) of the ZIP Central Directory. This value is taken from the 114 | * ZIP End of Central Directory record. 115 | */ 116 | public long getZipCentralDirectorySizeBytes() { 117 | return mCentralDirectorySizeBytes; 118 | } 119 | 120 | /** 121 | * Returns the number of records in the ZIP Central Directory. This value is taken from the 122 | * ZIP End of Central Directory record. 123 | */ 124 | public int getZipCentralDirectoryRecordCount() { 125 | return mCentralDirectoryRecordCount; 126 | } 127 | 128 | /** 129 | * Returns the start offset of the ZIP End of Central Directory record. The record extends 130 | * until the very end of the APK. 131 | */ 132 | public long getZipEndOfCentralDirectoryOffset() { 133 | return mEocdOffset; 134 | } 135 | 136 | /** 137 | * Returns the contents of the ZIP End of Central Directory. 138 | */ 139 | public ByteBuffer getZipEndOfCentralDirectory() { 140 | return mEocd; 141 | } 142 | } 143 | 144 | /** 145 | * Sets the offset of the start of the ZIP Central Directory in the APK's ZIP End of Central 146 | * Directory record. 147 | * 148 | * @param zipEndOfCentralDirectory APK's ZIP End of Central Directory record 149 | * @param offset offset of the ZIP Central Directory relative to the start of the archive. Must 150 | * be between {@code 0} and {@code 2^32 - 1} inclusive. 151 | */ 152 | public static void setZipEocdCentralDirectoryOffset( 153 | ByteBuffer zipEndOfCentralDirectory, long offset) { 154 | ByteBuffer eocd = zipEndOfCentralDirectory.slice(); 155 | eocd.order(ByteOrder.LITTLE_ENDIAN); 156 | ZipUtils.setZipEocdCentralDirectoryOffset(eocd, offset); 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /android-pack-plugin/packPlugin/src/main/java/com/android/apksigner/core/internal/apk/v1/DigestAlgorithm.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.android.apksigner.core.internal.apk.v1; 18 | 19 | /** 20 | * Digest algorithm used with JAR signing (aka v1 signing scheme). 21 | */ 22 | public enum DigestAlgorithm { 23 | /** SHA-1 */ 24 | SHA1("SHA-1"), 25 | 26 | /** SHA2-256 */ 27 | SHA256("SHA-256"); 28 | 29 | private final String mJcaMessageDigestAlgorithm; 30 | 31 | private DigestAlgorithm(String jcaMessageDigestAlgoritm) { 32 | mJcaMessageDigestAlgorithm = jcaMessageDigestAlgoritm; 33 | } 34 | 35 | /** 36 | * Returns the {@link java.security.MessageDigest} algorithm represented by this digest 37 | * algorithm. 38 | */ 39 | String getJcaMessageDigestAlgorithm() { 40 | return mJcaMessageDigestAlgorithm; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /android-pack-plugin/packPlugin/src/main/java/com/android/apksigner/core/internal/apk/v1/V1SchemeSigner.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.android.apksigner.core.internal.apk.v1; 18 | 19 | import com.android.apksigner.core.internal.jar.ManifestWriter; 20 | import com.android.apksigner.core.internal.jar.SignatureFileWriter; 21 | import com.android.apksigner.core.internal.util.Pair; 22 | import org.bouncycastle.asn1.ASN1InputStream; 23 | import org.bouncycastle.asn1.ASN1ObjectIdentifier; 24 | import org.bouncycastle.asn1.DERNull; 25 | import org.bouncycastle.asn1.DEROutputStream; 26 | import org.bouncycastle.asn1.x509.AlgorithmIdentifier; 27 | import org.bouncycastle.asn1.x9.X9ObjectIdentifiers; 28 | import org.bouncycastle.cert.jcajce.JcaCertStore; 29 | import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder; 30 | import org.bouncycastle.cms.*; 31 | import org.bouncycastle.operator.ContentSigner; 32 | import org.bouncycastle.operator.OperatorCreationException; 33 | import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; 34 | import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder; 35 | 36 | import java.io.ByteArrayInputStream; 37 | import java.io.ByteArrayOutputStream; 38 | import java.io.IOException; 39 | import java.security.*; 40 | import java.security.cert.CertificateEncodingException; 41 | import java.security.cert.X509Certificate; 42 | import java.util.*; 43 | import java.util.jar.Attributes; 44 | import java.util.jar.Manifest; 45 | 46 | /** 47 | * APK signer which uses JAR signing (aka v1 signing scheme). 48 | * 49 | * @see Signed JAR File 50 | */ 51 | public abstract class V1SchemeSigner { 52 | 53 | public static final String MANIFEST_ENTRY_NAME = "META-INF/MANIFEST.MF"; 54 | 55 | private static final Attributes.Name ATTRIBUTE_NAME_CREATED_BY = 56 | new Attributes.Name("Created-By"); 57 | private static final String ATTRIBUTE_DEFALT_VALUE_CREATED_BY = "1.0 (Android apksigner)"; 58 | private static final String ATTRIBUTE_VALUE_MANIFEST_VERSION = "1.0"; 59 | private static final String ATTRIBUTE_VALUE_SIGNATURE_VERSION = "1.0"; 60 | 61 | private static final Attributes.Name SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME = 62 | new Attributes.Name("X-Android-APK-Signed"); 63 | 64 | /** 65 | * Signer configuration. 66 | */ 67 | public static class SignerConfig { 68 | /** Name. */ 69 | public String name; 70 | 71 | /** Private key. */ 72 | public PrivateKey privateKey; 73 | 74 | /** 75 | * Certificates, with the first certificate containing the public key corresponding to 76 | * {@link #privateKey}. 77 | */ 78 | public List certificates; 79 | 80 | /** 81 | * Digest algorithm used for the signature. 82 | */ 83 | public DigestAlgorithm signatureDigestAlgorithm; 84 | 85 | /** 86 | * Digest algorithm used for digests of JAR entries and MANIFEST.MF. 87 | */ 88 | public DigestAlgorithm contentDigestAlgorithm; 89 | } 90 | 91 | /** Hidden constructor to prevent instantiation. */ 92 | private V1SchemeSigner() {} 93 | 94 | /** 95 | * Gets the JAR signing digest algorithm to be used for signing an APK using the provided key. 96 | * 97 | * @param minSdkVersion minimum API Level of the platform on which the APK may be installed (see 98 | * AndroidManifest.xml minSdkVersion attribute) 99 | * 100 | * @throws InvalidKeyException if the provided key is not suitable for signing APKs using 101 | * JAR signing (aka v1 signature scheme) 102 | */ 103 | public static DigestAlgorithm getSuggestedSignatureDigestAlgorithm( 104 | PublicKey signingKey, int minSdkVersion) throws InvalidKeyException { 105 | String keyAlgorithm = signingKey.getAlgorithm(); 106 | if ("RSA".equalsIgnoreCase(keyAlgorithm)) { 107 | // Prior to API Level 18, only SHA-1 can be used with RSA. 108 | if (minSdkVersion < 18) { 109 | return DigestAlgorithm.SHA1; 110 | } 111 | return DigestAlgorithm.SHA256; 112 | } else if ("DSA".equalsIgnoreCase(keyAlgorithm)) { 113 | // Prior to API Level 21, only SHA-1 can be used with DSA 114 | if (minSdkVersion < 21) { 115 | return DigestAlgorithm.SHA1; 116 | } else { 117 | return DigestAlgorithm.SHA256; 118 | } 119 | } else if ("EC".equalsIgnoreCase(keyAlgorithm)) { 120 | if (minSdkVersion < 18) { 121 | throw new InvalidKeyException( 122 | "ECDSA signatures only supported for minSdkVersion 18 and higher"); 123 | } 124 | // Prior to API Level 21, only SHA-1 can be used with ECDSA 125 | if (minSdkVersion < 21) { 126 | return DigestAlgorithm.SHA1; 127 | } else { 128 | return DigestAlgorithm.SHA256; 129 | } 130 | } else { 131 | throw new InvalidKeyException("Unsupported key algorithm: " + keyAlgorithm); 132 | } 133 | } 134 | 135 | /** 136 | * Returns the JAR signing digest algorithm to be used for JAR entry digests. 137 | * 138 | * @param minSdkVersion minimum API Level of the platform on which the APK may be installed (see 139 | * AndroidManifest.xml minSdkVersion attribute) 140 | */ 141 | public static DigestAlgorithm getSuggestedContentDigestAlgorithm(int minSdkVersion) { 142 | return (minSdkVersion >= 18) ? DigestAlgorithm.SHA256 : DigestAlgorithm.SHA1; 143 | } 144 | 145 | /** 146 | * Returns a new {@link MessageDigest} instance corresponding to the provided digest algorithm. 147 | */ 148 | public static MessageDigest getMessageDigestInstance(DigestAlgorithm digestAlgorithm) { 149 | String jcaAlgorithm = digestAlgorithm.getJcaMessageDigestAlgorithm(); 150 | try { 151 | return MessageDigest.getInstance(jcaAlgorithm); 152 | } catch (NoSuchAlgorithmException e) { 153 | throw new RuntimeException("Failed to obtain " + jcaAlgorithm + " MessageDigest", e); 154 | } 155 | } 156 | 157 | /** 158 | * Returns the JCA {@link MessageDigest} algorithm corresponding to the provided digest 159 | * algorithm. 160 | */ 161 | public static String getJcaMessageDigestAlgorithm(DigestAlgorithm digestAlgorithm) { 162 | return digestAlgorithm.getJcaMessageDigestAlgorithm(); 163 | } 164 | 165 | /** 166 | * Returns {@code true} if the provided JAR entry must be mentioned in signed JAR archive's 167 | * manifest. 168 | */ 169 | public static boolean isJarEntryDigestNeededInManifest(String entryName) { 170 | // See https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Signed_JAR_File 171 | 172 | // Entries outside of META-INF must be listed in the manifest. 173 | if (!entryName.startsWith("META-INF/")) { 174 | return true; 175 | } 176 | // Entries in subdirectories of META-INF must be listed in the manifest. 177 | if (entryName.indexOf('/', "META-INF/".length()) != -1) { 178 | return true; 179 | } 180 | 181 | // Ignored file names (case-insensitive) in META-INF directory: 182 | // MANIFEST.MF 183 | // *.SF 184 | // *.RSA 185 | // *.DSA 186 | // *.EC 187 | // SIG-* 188 | String fileNameLowerCase = 189 | entryName.substring("META-INF/".length()).toLowerCase(Locale.US); 190 | if (("manifest.mf".equals(fileNameLowerCase)) 191 | || (fileNameLowerCase.endsWith(".sf")) 192 | || (fileNameLowerCase.endsWith(".rsa")) 193 | || (fileNameLowerCase.endsWith(".dsa")) 194 | || (fileNameLowerCase.endsWith(".ec")) 195 | || (fileNameLowerCase.startsWith("sig-"))) { 196 | return false; 197 | } 198 | return true; 199 | } 200 | 201 | /** 202 | * Signs the provided APK using JAR signing (aka v1 signature scheme) and returns the list of 203 | * JAR entries which need to be added to the APK as part of the signature. 204 | * 205 | * @param signerConfigs signer configurations, one for each signer. At least one signer config 206 | * must be provided. 207 | * 208 | * @throws InvalidKeyException if a signing key is not suitable for this signature scheme or 209 | * cannot be used in general 210 | * @throws SignatureException if an error occurs when computing digests of generating 211 | * signatures 212 | */ 213 | public static List> sign( 214 | List signerConfigs, 215 | DigestAlgorithm jarEntryDigestAlgorithm, 216 | Map jarEntryDigests, 217 | List apkSigningSchemeIds, 218 | byte[] sourceManifestBytes) 219 | throws InvalidKeyException, CertificateEncodingException, SignatureException { 220 | if (signerConfigs.isEmpty()) { 221 | throw new IllegalArgumentException("At least one signer config must be provided"); 222 | } 223 | OutputManifestFile manifest = 224 | generateManifestFile(jarEntryDigestAlgorithm, jarEntryDigests, sourceManifestBytes); 225 | 226 | return signManifest(signerConfigs, jarEntryDigestAlgorithm, apkSigningSchemeIds, manifest); 227 | } 228 | 229 | /** 230 | * Signs the provided APK using JAR signing (aka v1 signature scheme) and returns the list of 231 | * JAR entries which need to be added to the APK as part of the signature. 232 | * 233 | * @param signerConfigs signer configurations, one for each signer. At least one signer config 234 | * must be provided. 235 | * 236 | * @throws InvalidKeyException if a signing key is not suitable for this signature scheme or 237 | * cannot be used in general 238 | * @throws SignatureException if an error occurs when computing digests of generating 239 | * signatures 240 | */ 241 | public static List> signManifest( 242 | List signerConfigs, 243 | DigestAlgorithm digestAlgorithm, 244 | List apkSigningSchemeIds, 245 | OutputManifestFile manifest) 246 | throws InvalidKeyException, CertificateEncodingException, SignatureException { 247 | if (signerConfigs.isEmpty()) { 248 | throw new IllegalArgumentException("At least one signer config must be provided"); 249 | } 250 | 251 | // For each signer output .SF and .(RSA|DSA|EC) file, then output MANIFEST.MF. 252 | List> signatureJarEntries = 253 | new ArrayList<>(2 * signerConfigs.size() + 1); 254 | byte[] sfBytes = 255 | generateSignatureFile(apkSigningSchemeIds, digestAlgorithm, manifest); 256 | for (SignerConfig signerConfig : signerConfigs) { 257 | String signerName = signerConfig.name; 258 | byte[] signatureBlock; 259 | try { 260 | signatureBlock = generateSignatureBlock(signerConfig, sfBytes); 261 | } catch (InvalidKeyException e) { 262 | throw new InvalidKeyException( 263 | "Failed to sign using signer \"" + signerName + "\"", e); 264 | } catch (CertificateEncodingException e) { 265 | throw new CertificateEncodingException( 266 | "Failed to sign using signer \"" + signerName + "\"", e); 267 | } catch (SignatureException e) { 268 | throw new SignatureException( 269 | "Failed to sign using signer \"" + signerName + "\"", e); 270 | } 271 | signatureJarEntries.add(Pair.of("META-INF/" + signerName + ".SF", sfBytes)); 272 | PublicKey publicKey = signerConfig.certificates.get(0).getPublicKey(); 273 | String signatureBlockFileName = 274 | "META-INF/" + signerName + "." 275 | + publicKey.getAlgorithm().toUpperCase(Locale.US); 276 | signatureJarEntries.add( 277 | Pair.of(signatureBlockFileName, signatureBlock)); 278 | } 279 | signatureJarEntries.add(Pair.of(MANIFEST_ENTRY_NAME, manifest.contents)); 280 | return signatureJarEntries; 281 | } 282 | 283 | /** 284 | * Returns the names of JAR entries which this signer will produce as part of v1 signature. 285 | */ 286 | public static Set getOutputEntryNames(List signerConfigs) { 287 | Set result = new HashSet<>(2 * signerConfigs.size() + 1); 288 | for (SignerConfig signerConfig : signerConfigs) { 289 | String signerName = signerConfig.name; 290 | result.add("META-INF/" + signerName + ".SF"); 291 | PublicKey publicKey = signerConfig.certificates.get(0).getPublicKey(); 292 | String signatureBlockFileName = 293 | "META-INF/" + signerName + "." 294 | + publicKey.getAlgorithm().toUpperCase(Locale.US); 295 | result.add(signatureBlockFileName); 296 | } 297 | result.add(MANIFEST_ENTRY_NAME); 298 | return result; 299 | } 300 | 301 | /** 302 | * Generated and returns the {@code META-INF/MANIFEST.MF} file based on the provided (optional) 303 | * input {@code MANIFEST.MF} and digests of JAR entries covered by the manifest. 304 | */ 305 | public static OutputManifestFile generateManifestFile( 306 | DigestAlgorithm jarEntryDigestAlgorithm, 307 | Map jarEntryDigests, 308 | byte[] sourceManifestBytes) { 309 | Manifest sourceManifest = null; 310 | if (sourceManifestBytes != null) { 311 | try { 312 | sourceManifest = new Manifest(new ByteArrayInputStream(sourceManifestBytes)); 313 | } catch (IOException e) { 314 | throw new IllegalArgumentException("Failed to parse source MANIFEST.MF", e); 315 | } 316 | } 317 | ByteArrayOutputStream manifestOut = new ByteArrayOutputStream(); 318 | Attributes mainAttrs = new Attributes(); 319 | // Copy the main section from the source manifest (if provided). Otherwise use defaults. 320 | if (sourceManifest != null) { 321 | mainAttrs.putAll(sourceManifest.getMainAttributes()); 322 | } else { 323 | mainAttrs.put(Attributes.Name.MANIFEST_VERSION, ATTRIBUTE_VALUE_MANIFEST_VERSION); 324 | mainAttrs.put(ATTRIBUTE_NAME_CREATED_BY, ATTRIBUTE_DEFALT_VALUE_CREATED_BY); 325 | } 326 | 327 | try { 328 | ManifestWriter.writeMainSection(manifestOut, mainAttrs); 329 | } catch (IOException e) { 330 | throw new RuntimeException("Failed to write in-memory MANIFEST.MF", e); 331 | } 332 | 333 | List sortedEntryNames = new ArrayList<>(jarEntryDigests.keySet()); 334 | Collections.sort(sortedEntryNames); 335 | SortedMap invidualSectionsContents = new TreeMap<>(); 336 | String entryDigestAttributeName = getEntryDigestAttributeName(jarEntryDigestAlgorithm); 337 | for (String entryName : sortedEntryNames) { 338 | byte[] entryDigest = jarEntryDigests.get(entryName); 339 | Attributes entryAttrs = new Attributes(); 340 | entryAttrs.putValue( 341 | entryDigestAttributeName, 342 | Base64.getEncoder().encodeToString(entryDigest)); 343 | ByteArrayOutputStream sectionOut = new ByteArrayOutputStream(); 344 | byte[] sectionBytes; 345 | try { 346 | ManifestWriter.writeIndividualSection(sectionOut, entryName, entryAttrs); 347 | sectionBytes = sectionOut.toByteArray(); 348 | manifestOut.write(sectionBytes); 349 | } catch (IOException e) { 350 | throw new RuntimeException("Failed to write in-memory MANIFEST.MF", e); 351 | } 352 | invidualSectionsContents.put(entryName, sectionBytes); 353 | } 354 | 355 | OutputManifestFile result = new OutputManifestFile(); 356 | result.contents = manifestOut.toByteArray(); 357 | result.mainSectionAttributes = mainAttrs; 358 | result.individualSectionsContents = invidualSectionsContents; 359 | return result; 360 | } 361 | 362 | public static class OutputManifestFile { 363 | public byte[] contents; 364 | public SortedMap individualSectionsContents; 365 | public Attributes mainSectionAttributes; 366 | } 367 | 368 | private static byte[] generateSignatureFile( 369 | List apkSignatureSchemeIds, 370 | DigestAlgorithm manifestDigestAlgorithm, 371 | OutputManifestFile manifest) { 372 | Manifest sf = new Manifest(); 373 | Attributes mainAttrs = sf.getMainAttributes(); 374 | mainAttrs.put(Attributes.Name.SIGNATURE_VERSION, ATTRIBUTE_VALUE_SIGNATURE_VERSION); 375 | mainAttrs.put(ATTRIBUTE_NAME_CREATED_BY, ATTRIBUTE_DEFALT_VALUE_CREATED_BY); 376 | if (!apkSignatureSchemeIds.isEmpty()) { 377 | // Add APK Signature Scheme v2 (and newer) signature stripping protection. 378 | // This attribute indicates that this APK is supposed to have been signed using one or 379 | // more APK-specific signature schemes in addition to the standard JAR signature scheme 380 | // used by this code. APK signature verifier should reject the APK if it does not 381 | // contain a signature for the signature scheme the verifier prefers out of this set. 382 | StringBuilder attrValue = new StringBuilder(); 383 | for (int id : apkSignatureSchemeIds) { 384 | if (attrValue.length() > 0) { 385 | attrValue.append(", "); 386 | } 387 | attrValue.append(String.valueOf(id)); 388 | } 389 | mainAttrs.put( 390 | SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME, 391 | attrValue.toString()); 392 | } 393 | 394 | // Add main attribute containing the digest of MANIFEST.MF. 395 | MessageDigest md = getMessageDigestInstance(manifestDigestAlgorithm); 396 | mainAttrs.putValue( 397 | getManifestDigestAttributeName(manifestDigestAlgorithm), 398 | Base64.getEncoder().encodeToString(md.digest(manifest.contents))); 399 | ByteArrayOutputStream out = new ByteArrayOutputStream(); 400 | try { 401 | SignatureFileWriter.writeMainSection(out, mainAttrs); 402 | } catch (IOException e) { 403 | throw new RuntimeException("Failed to write in-memory .SF file", e); 404 | } 405 | String entryDigestAttributeName = getEntryDigestAttributeName(manifestDigestAlgorithm); 406 | for (Map.Entry manifestSection 407 | : manifest.individualSectionsContents.entrySet()) { 408 | String sectionName = manifestSection.getKey(); 409 | byte[] sectionContents = manifestSection.getValue(); 410 | byte[] sectionDigest = md.digest(sectionContents); 411 | Attributes attrs = new Attributes(); 412 | attrs.putValue( 413 | entryDigestAttributeName, 414 | Base64.getEncoder().encodeToString(sectionDigest)); 415 | 416 | try { 417 | SignatureFileWriter.writeIndividualSection(out, sectionName, attrs); 418 | } catch (IOException e) { 419 | throw new RuntimeException("Failed to write in-memory .SF file", e); 420 | } 421 | } 422 | 423 | // A bug in the java.util.jar implementation of Android platforms up to version 1.6 will 424 | // cause a spurious IOException to be thrown if the length of the signature file is a 425 | // multiple of 1024 bytes. As a workaround, add an extra CRLF in this case. 426 | if ((out.size() > 0) && ((out.size() % 1024) == 0)) { 427 | try { 428 | SignatureFileWriter.writeSectionDelimiter(out); 429 | } catch (IOException e) { 430 | throw new RuntimeException("Failed to write to ByteArrayOutputStream", e); 431 | } 432 | } 433 | 434 | return out.toByteArray(); 435 | } 436 | 437 | private static byte[] generateSignatureBlock( 438 | SignerConfig signerConfig, byte[] signatureFileBytes) 439 | throws InvalidKeyException, CertificateEncodingException, SignatureException { 440 | JcaCertStore certs = new JcaCertStore(signerConfig.certificates); 441 | X509Certificate signerCert = signerConfig.certificates.get(0); 442 | String jcaSignatureAlgorithm = 443 | getJcaSignatureAlgorithm( 444 | signerCert.getPublicKey(), signerConfig.signatureDigestAlgorithm); 445 | try { 446 | ContentSigner signer = 447 | new JcaContentSignerBuilder(jcaSignatureAlgorithm) 448 | .build(signerConfig.privateKey); 449 | CMSSignedDataGenerator gen = new CMSSignedDataGenerator(); 450 | gen.addSignerInfoGenerator( 451 | new SignerInfoGeneratorBuilder( 452 | new JcaDigestCalculatorProviderBuilder().build(), 453 | SignerInfoSignatureAlgorithmFinder.INSTANCE) 454 | .setDirectSignature(true) 455 | .build(signer, new JcaX509CertificateHolder(signerCert))); 456 | gen.addCertificates(certs); 457 | 458 | CMSSignedData sigData = 459 | gen.generate(new CMSProcessableByteArray(signatureFileBytes), false); 460 | 461 | ByteArrayOutputStream out = new ByteArrayOutputStream(); 462 | try (ASN1InputStream asn1 = new ASN1InputStream(sigData.getEncoded())) { 463 | DEROutputStream dos = new DEROutputStream(out); 464 | dos.writeObject(asn1.readObject()); 465 | } 466 | return out.toByteArray(); 467 | } catch (OperatorCreationException | CMSException | IOException e) { 468 | throw new SignatureException("Failed to generate signature", e); 469 | } 470 | } 471 | 472 | /** 473 | * Chooser of SignatureAlgorithm for PKCS #7 CMS SignerInfo. 474 | */ 475 | private static class SignerInfoSignatureAlgorithmFinder 476 | implements CMSSignatureEncryptionAlgorithmFinder { 477 | private static final SignerInfoSignatureAlgorithmFinder INSTANCE = 478 | new SignerInfoSignatureAlgorithmFinder(); 479 | 480 | private static final AlgorithmIdentifier DSA = 481 | new AlgorithmIdentifier(X9ObjectIdentifiers.id_dsa, DERNull.INSTANCE); 482 | 483 | private final CMSSignatureEncryptionAlgorithmFinder mDefault = 484 | new DefaultCMSSignatureEncryptionAlgorithmFinder(); 485 | 486 | @Override 487 | public AlgorithmIdentifier findEncryptionAlgorithm(AlgorithmIdentifier id) { 488 | // Use the default chooser, but replace dsaWithSha1 with dsa. This is because "dsa" is 489 | // accepted by any Android platform whereas "dsaWithSha1" is accepted only since 490 | // API Level 9. 491 | id = mDefault.findEncryptionAlgorithm(id); 492 | if (id != null) { 493 | ASN1ObjectIdentifier oid = id.getAlgorithm(); 494 | if (X9ObjectIdentifiers.id_dsa_with_sha1.equals(oid)) { 495 | return DSA; 496 | } 497 | } 498 | 499 | return id; 500 | } 501 | } 502 | 503 | private static String getEntryDigestAttributeName(DigestAlgorithm digestAlgorithm) { 504 | switch (digestAlgorithm) { 505 | case SHA1: 506 | return "SHA1-Digest"; 507 | case SHA256: 508 | return "SHA-256-Digest"; 509 | default: 510 | throw new IllegalArgumentException( 511 | "Unexpected content digest algorithm: " + digestAlgorithm); 512 | } 513 | } 514 | 515 | private static String getManifestDigestAttributeName(DigestAlgorithm digestAlgorithm) { 516 | switch (digestAlgorithm) { 517 | case SHA1: 518 | return "SHA1-Digest-Manifest"; 519 | case SHA256: 520 | return "SHA-256-Digest-Manifest"; 521 | default: 522 | throw new IllegalArgumentException( 523 | "Unexpected content digest algorithm: " + digestAlgorithm); 524 | } 525 | } 526 | 527 | private static String getJcaSignatureAlgorithm( 528 | PublicKey publicKey, DigestAlgorithm digestAlgorithm) throws InvalidKeyException { 529 | String keyAlgorithm = publicKey.getAlgorithm(); 530 | String digestPrefixForSigAlg; 531 | switch (digestAlgorithm) { 532 | case SHA1: 533 | digestPrefixForSigAlg = "SHA1"; 534 | break; 535 | case SHA256: 536 | digestPrefixForSigAlg = "SHA256"; 537 | break; 538 | default: 539 | throw new IllegalArgumentException( 540 | "Unexpected digest algorithm: " + digestAlgorithm); 541 | } 542 | if ("RSA".equalsIgnoreCase(keyAlgorithm)) { 543 | return digestPrefixForSigAlg + "withRSA"; 544 | } else if ("DSA".equalsIgnoreCase(keyAlgorithm)) { 545 | return digestPrefixForSigAlg + "withDSA"; 546 | } else if ("EC".equalsIgnoreCase(keyAlgorithm)) { 547 | return digestPrefixForSigAlg + "withECDSA"; 548 | } else { 549 | throw new InvalidKeyException("Unsupported key algorithm: " + keyAlgorithm); 550 | } 551 | } 552 | } 553 | -------------------------------------------------------------------------------- /android-pack-plugin/packPlugin/src/main/java/com/android/apksigner/core/internal/apk/v2/ContentDigestAlgorithm.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.android.apksigner.core.internal.apk.v2; 18 | 19 | /** 20 | * APK Signature Scheme v2 content digest algorithm. 21 | */ 22 | public enum ContentDigestAlgorithm { 23 | /** SHA2-256 over 1 MB chunks. */ 24 | CHUNKED_SHA256("SHA-256", 256 / 8), 25 | 26 | /** SHA2-512 over 1 MB chunks. */ 27 | CHUNKED_SHA512("SHA-512", 512 / 8); 28 | 29 | private final String mJcaMessageDigestAlgorithm; 30 | private final int mChunkDigestOutputSizeBytes; 31 | 32 | private ContentDigestAlgorithm( 33 | String jcaMessageDigestAlgorithm, int chunkDigestOutputSizeBytes) { 34 | mJcaMessageDigestAlgorithm = jcaMessageDigestAlgorithm; 35 | mChunkDigestOutputSizeBytes = chunkDigestOutputSizeBytes; 36 | } 37 | 38 | /** 39 | * Returns the {@link java.security.MessageDigest} algorithm used for computing digests of 40 | * chunks by this content digest algorithm. 41 | */ 42 | String getJcaMessageDigestAlgorithm() { 43 | return mJcaMessageDigestAlgorithm; 44 | } 45 | 46 | /** 47 | * Returns the size (in bytes) of the digest of a chunk of content. 48 | */ 49 | int getChunkDigestOutputSizeBytes() { 50 | return mChunkDigestOutputSizeBytes; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /android-pack-plugin/packPlugin/src/main/java/com/android/apksigner/core/internal/apk/v2/SignatureAlgorithm.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.android.apksigner.core.internal.apk.v2; 18 | 19 | import com.android.apksigner.core.internal.util.Pair; 20 | 21 | import java.security.spec.AlgorithmParameterSpec; 22 | import java.security.spec.MGF1ParameterSpec; 23 | import java.security.spec.PSSParameterSpec; 24 | 25 | /** 26 | * APK Signature Scheme v2 signature algorithm. 27 | */ 28 | public enum SignatureAlgorithm { 29 | /** 30 | * RSASSA-PSS with SHA2-256 digest, SHA2-256 MGF1, 32 bytes of salt, trailer: 0xbc, content 31 | * digested using SHA2-256 in 1 MB chunks. 32 | */ 33 | RSA_PSS_WITH_SHA256( 34 | 0x0101, 35 | ContentDigestAlgorithm.CHUNKED_SHA256, 36 | "RSA", 37 | Pair.of("SHA256withRSA/PSS", 38 | new PSSParameterSpec( 39 | "SHA-256", "MGF1", MGF1ParameterSpec.SHA256, 256 / 8, 1))), 40 | 41 | /** 42 | * RSASSA-PSS with SHA2-512 digest, SHA2-512 MGF1, 64 bytes of salt, trailer: 0xbc, content 43 | * digested using SHA2-512 in 1 MB chunks. 44 | */ 45 | RSA_PSS_WITH_SHA512( 46 | 0x0102, 47 | ContentDigestAlgorithm.CHUNKED_SHA512, 48 | "RSA", 49 | Pair.of( 50 | "SHA512withRSA/PSS", 51 | new PSSParameterSpec( 52 | "SHA-512", "MGF1", MGF1ParameterSpec.SHA512, 512 / 8, 1))), 53 | 54 | /** RSASSA-PKCS1-v1_5 with SHA2-256 digest, content digested using SHA2-256 in 1 MB chunks. */ 55 | RSA_PKCS1_V1_5_WITH_SHA256( 56 | 0x0103, 57 | ContentDigestAlgorithm.CHUNKED_SHA256, 58 | "RSA", 59 | Pair.of("SHA256withRSA", null)), 60 | 61 | /** RSASSA-PKCS1-v1_5 with SHA2-512 digest, content digested using SHA2-512 in 1 MB chunks. */ 62 | RSA_PKCS1_V1_5_WITH_SHA512( 63 | 0x0104, 64 | ContentDigestAlgorithm.CHUNKED_SHA512, 65 | "RSA", 66 | Pair.of("SHA512withRSA", null)), 67 | 68 | /** ECDSA with SHA2-256 digest, content digested using SHA2-256 in 1 MB chunks. */ 69 | ECDSA_WITH_SHA256( 70 | 0x0201, 71 | ContentDigestAlgorithm.CHUNKED_SHA256, 72 | "EC", 73 | Pair.of("SHA256withECDSA", null)), 74 | 75 | /** ECDSA with SHA2-512 digest, content digested using SHA2-512 in 1 MB chunks. */ 76 | ECDSA_WITH_SHA512( 77 | 0x0202, 78 | ContentDigestAlgorithm.CHUNKED_SHA512, 79 | "EC", 80 | Pair.of("SHA512withECDSA", null)), 81 | 82 | /** DSA with SHA2-256 digest, content digested using SHA2-256 in 1 MB chunks. */ 83 | DSA_WITH_SHA256( 84 | 0x0301, 85 | ContentDigestAlgorithm.CHUNKED_SHA256, 86 | "DSA", 87 | Pair.of("SHA256withDSA", null)); 88 | 89 | private final int mId; 90 | private final String mJcaKeyAlgorithm; 91 | private final ContentDigestAlgorithm mContentDigestAlgorithm; 92 | private final Pair mJcaSignatureAlgAndParams; 93 | 94 | private SignatureAlgorithm(int id, 95 | ContentDigestAlgorithm contentDigestAlgorithm, 96 | String jcaKeyAlgorithm, 97 | Pair jcaSignatureAlgAndParams) { 98 | mId = id; 99 | mContentDigestAlgorithm = contentDigestAlgorithm; 100 | mJcaKeyAlgorithm = jcaKeyAlgorithm; 101 | mJcaSignatureAlgAndParams = jcaSignatureAlgAndParams; 102 | } 103 | 104 | /** 105 | * Returns the ID of this signature algorithm as used in APK Signature Scheme v2 wire format. 106 | */ 107 | int getId() { 108 | return mId; 109 | } 110 | 111 | /** 112 | * Returns the content digest algorithm associated with this signature algorithm. 113 | */ 114 | ContentDigestAlgorithm getContentDigestAlgorithm() { 115 | return mContentDigestAlgorithm; 116 | } 117 | 118 | /** 119 | * Returns the JCA {@link java.security.Key} algorithm used by this signature scheme. 120 | */ 121 | String getJcaKeyAlgorithm() { 122 | return mJcaKeyAlgorithm; 123 | } 124 | 125 | /** 126 | * Returns the {@link java.security.Signature} algorithm and the {@link AlgorithmParameterSpec} 127 | * (or null if not needed) to parameterize the {@code Signature}. 128 | */ 129 | Pair getJcaSignatureAlgorithmAndParams() { 130 | return mJcaSignatureAlgAndParams; 131 | } 132 | 133 | static SignatureAlgorithm findById(int id) { 134 | for (SignatureAlgorithm alg : SignatureAlgorithm.values()) { 135 | if (alg.getId() == id) { 136 | return alg; 137 | } 138 | } 139 | 140 | return null; 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /android-pack-plugin/packPlugin/src/main/java/com/android/apksigner/core/internal/jar/ManifestWriter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.android.apksigner.core.internal.jar; 18 | 19 | import java.io.IOException; 20 | import java.io.OutputStream; 21 | import java.util.Map; 22 | import java.util.Set; 23 | import java.util.SortedMap; 24 | import java.util.TreeMap; 25 | import java.util.jar.Attributes; 26 | 27 | /** 28 | * Producer of {@code META-INF/MANIFEST.MF} file. 29 | */ 30 | public abstract class ManifestWriter { 31 | 32 | private static final byte[] CRLF = new byte[] {'\r', '\n'}; 33 | private static final int MAX_LINE_LENGTH = 70; 34 | 35 | private ManifestWriter() {} 36 | 37 | public static void writeMainSection(OutputStream out, Attributes attributes) 38 | throws IOException { 39 | 40 | // Main section must start with the Manifest-Version attribute. 41 | // See https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Signed_JAR_File. 42 | String manifestVersion = attributes.getValue(Attributes.Name.MANIFEST_VERSION); 43 | if (manifestVersion == null) { 44 | throw new IllegalArgumentException( 45 | "Mandatory " + Attributes.Name.MANIFEST_VERSION + " attribute missing"); 46 | } 47 | writeAttribute(out, Attributes.Name.MANIFEST_VERSION, manifestVersion); 48 | 49 | if (attributes.size() > 1) { 50 | SortedMap namedAttributes = getAttributesSortedByName(attributes); 51 | namedAttributes.remove(Attributes.Name.MANIFEST_VERSION.toString()); 52 | writeAttributes(out, namedAttributes); 53 | } 54 | writeSectionDelimiter(out); 55 | } 56 | 57 | public static void writeIndividualSection(OutputStream out, String name, Attributes attributes) 58 | throws IOException { 59 | writeAttribute(out, "Name", name); 60 | 61 | if (!attributes.isEmpty()) { 62 | writeAttributes(out, getAttributesSortedByName(attributes)); 63 | } 64 | writeSectionDelimiter(out); 65 | } 66 | 67 | static void writeSectionDelimiter(OutputStream out) throws IOException { 68 | out.write(CRLF); 69 | } 70 | 71 | static void writeAttribute(OutputStream out, Attributes.Name name, String value) 72 | throws IOException { 73 | writeAttribute(out, name.toString(), value); 74 | } 75 | 76 | private static void writeAttribute(OutputStream out, String name, String value) 77 | throws IOException { 78 | writeLine(out, name + ": " + value); 79 | } 80 | 81 | private static void writeLine(OutputStream out, String line) throws IOException { 82 | byte[] lineBytes = line.getBytes("UTF-8"); 83 | int offset = 0; 84 | int remaining = lineBytes.length; 85 | boolean firstLine = true; 86 | while (remaining > 0) { 87 | int chunkLength; 88 | if (firstLine) { 89 | // First line 90 | chunkLength = Math.min(remaining, MAX_LINE_LENGTH); 91 | } else { 92 | // Continuation line 93 | out.write(CRLF); 94 | out.write(' '); 95 | chunkLength = Math.min(remaining, MAX_LINE_LENGTH - 1); 96 | } 97 | out.write(lineBytes, offset, chunkLength); 98 | offset += chunkLength; 99 | remaining -= chunkLength; 100 | firstLine = false; 101 | } 102 | out.write(CRLF); 103 | } 104 | 105 | static SortedMap getAttributesSortedByName(Attributes attributes) { 106 | Set> attributesEntries = attributes.entrySet(); 107 | SortedMap namedAttributes = new TreeMap(); 108 | for (Map.Entry attribute : attributesEntries) { 109 | String attrName = attribute.getKey().toString(); 110 | String attrValue = attribute.getValue().toString(); 111 | namedAttributes.put(attrName, attrValue); 112 | } 113 | return namedAttributes; 114 | } 115 | 116 | static void writeAttributes( 117 | OutputStream out, SortedMap attributesSortedByName) throws IOException { 118 | for (Map.Entry attribute : attributesSortedByName.entrySet()) { 119 | String attrName = attribute.getKey(); 120 | String attrValue = attribute.getValue(); 121 | writeAttribute(out, attrName, attrValue); 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /android-pack-plugin/packPlugin/src/main/java/com/android/apksigner/core/internal/jar/SignatureFileWriter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.android.apksigner.core.internal.jar; 18 | 19 | import java.io.IOException; 20 | import java.io.OutputStream; 21 | import java.util.SortedMap; 22 | import java.util.jar.Attributes; 23 | 24 | /** 25 | * Producer of JAR signature file ({@code *.SF}). 26 | */ 27 | public abstract class SignatureFileWriter { 28 | private SignatureFileWriter() {} 29 | 30 | public static void writeMainSection(OutputStream out, Attributes attributes) 31 | throws IOException { 32 | 33 | // Main section must start with the Signature-Version attribute. 34 | // See https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Signed_JAR_File. 35 | String signatureVersion = attributes.getValue(Attributes.Name.SIGNATURE_VERSION); 36 | if (signatureVersion == null) { 37 | throw new IllegalArgumentException( 38 | "Mandatory " + Attributes.Name.SIGNATURE_VERSION + " attribute missing"); 39 | } 40 | ManifestWriter.writeAttribute(out, Attributes.Name.SIGNATURE_VERSION, signatureVersion); 41 | 42 | if (attributes.size() > 1) { 43 | SortedMap namedAttributes = 44 | ManifestWriter.getAttributesSortedByName(attributes); 45 | namedAttributes.remove(Attributes.Name.SIGNATURE_VERSION.toString()); 46 | ManifestWriter.writeAttributes(out, namedAttributes); 47 | } 48 | writeSectionDelimiter(out); 49 | } 50 | 51 | public static void writeIndividualSection(OutputStream out, String name, Attributes attributes) 52 | throws IOException { 53 | ManifestWriter.writeIndividualSection(out, name, attributes); 54 | } 55 | 56 | public static void writeSectionDelimiter(OutputStream out) throws IOException { 57 | ManifestWriter.writeSectionDelimiter(out); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /android-pack-plugin/packPlugin/src/main/java/com/android/apksigner/core/internal/util/ByteArrayOutputStreamSink.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.android.apksigner.core.internal.util; 18 | 19 | import com.android.apksigner.core.util.DataSink; 20 | 21 | import java.io.ByteArrayOutputStream; 22 | import java.nio.ByteBuffer; 23 | 24 | /** 25 | * Data sink which stores all input data into an internal {@link ByteArrayOutputStream}, thus 26 | * accepting an arbitrary amount of data. 27 | */ 28 | public class ByteArrayOutputStreamSink implements DataSink { 29 | 30 | private final ByteArrayOutputStream mBuf = new ByteArrayOutputStream(); 31 | 32 | @Override 33 | public void consume(byte[] buf, int offset, int length) { 34 | mBuf.write(buf, offset, length); 35 | } 36 | 37 | @Override 38 | public void consume(ByteBuffer buf) { 39 | if (!buf.hasRemaining()) { 40 | return; 41 | } 42 | 43 | if (buf.hasArray()) { 44 | mBuf.write( 45 | buf.array(), 46 | buf.arrayOffset() + buf.position(), 47 | buf.remaining()); 48 | buf.position(buf.limit()); 49 | } else { 50 | byte[] tmp = new byte[buf.remaining()]; 51 | buf.get(tmp); 52 | mBuf.write(tmp, 0, tmp.length); 53 | } 54 | } 55 | 56 | /** 57 | * Returns the data received so far. 58 | */ 59 | public byte[] getData() { 60 | return mBuf.toByteArray(); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /android-pack-plugin/packPlugin/src/main/java/com/android/apksigner/core/internal/util/ByteBufferDataSource.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.android.apksigner.core.internal.util; 18 | 19 | import com.android.apksigner.core.util.DataSink; 20 | import com.android.apksigner.core.util.DataSource; 21 | 22 | import java.io.IOException; 23 | import java.nio.ByteBuffer; 24 | 25 | /** 26 | * {@link DataSource} backed by a {@link ByteBuffer}. 27 | */ 28 | public class ByteBufferDataSource implements DataSource { 29 | 30 | private final ByteBuffer mBuffer; 31 | private final int mSize; 32 | 33 | /** 34 | * Constructs a new {@code ByteBufferDigestSource} based on the data contained in the provided 35 | * buffer between the buffer's position and limit. 36 | */ 37 | public ByteBufferDataSource(ByteBuffer buffer) { 38 | this(buffer, true); 39 | } 40 | 41 | /** 42 | * Constructs a new {@code ByteBufferDigestSource} based on the data contained in the provided 43 | * buffer between the buffer's position and limit. 44 | */ 45 | private ByteBufferDataSource(ByteBuffer buffer, boolean sliceRequired) { 46 | mBuffer = (sliceRequired) ? buffer.slice() : buffer; 47 | mSize = buffer.remaining(); 48 | } 49 | 50 | @Override 51 | public long size() { 52 | return mSize; 53 | } 54 | 55 | @Override 56 | public ByteBuffer getByteBuffer(long offset, int size) { 57 | checkChunkValid(offset, size); 58 | 59 | // checkChunkValid ensures that it's OK to cast offset to int. 60 | int chunkPosition = (int) offset; 61 | int chunkLimit = chunkPosition + size; 62 | // Creating a slice of ByteBuffer modifies the state of the source ByteBuffer (position 63 | // and limit fields, to be more specific). We thus use synchronization around these 64 | // state-changing operations to make instances of this class thread-safe. 65 | synchronized (mBuffer) { 66 | // ByteBuffer.limit(int) and .position(int) check that that the position >= limit 67 | // invariant is not broken. Thus, the only way to safely change position and limit 68 | // without caring about their current values is to first set position to 0 or set the 69 | // limit to capacity. 70 | mBuffer.position(0); 71 | 72 | mBuffer.limit(chunkLimit); 73 | mBuffer.position(chunkPosition); 74 | return mBuffer.slice(); 75 | } 76 | } 77 | 78 | @Override 79 | public void copyTo(long offset, int size, ByteBuffer dest) { 80 | dest.put(getByteBuffer(offset, size)); 81 | } 82 | 83 | @Override 84 | public void feed(long offset, long size, DataSink sink) throws IOException { 85 | if ((size < 0) || (size > mSize)) { 86 | throw new IllegalArgumentException("size: " + size + ", source size: " + mSize); 87 | } 88 | sink.consume(getByteBuffer(offset, (int) size)); 89 | } 90 | 91 | @Override 92 | public ByteBufferDataSource slice(long offset, long size) { 93 | if ((offset == 0) && (size == mSize)) { 94 | return this; 95 | } 96 | if ((size < 0) || (size > mSize)) { 97 | throw new IllegalArgumentException("size: " + size + ", source size: " + mSize); 98 | } 99 | return new ByteBufferDataSource( 100 | getByteBuffer(offset, (int) size), 101 | false // no need to slice -- it's already a slice 102 | ); 103 | } 104 | 105 | private void checkChunkValid(long offset, long size) { 106 | if (offset < 0) { 107 | throw new IllegalArgumentException("offset: " + offset); 108 | } 109 | if (size < 0) { 110 | throw new IllegalArgumentException("size: " + size); 111 | } 112 | if (offset > mSize) { 113 | throw new IllegalArgumentException( 114 | "offset (" + offset + ") > source size (" + mSize + ")"); 115 | } 116 | long endOffset = offset + size; 117 | if (endOffset < offset) { 118 | throw new IllegalArgumentException( 119 | "offset (" + offset + ") + size (" + size + ") overflow"); 120 | } 121 | if (endOffset > mSize) { 122 | throw new IllegalArgumentException( 123 | "offset (" + offset + ") + size (" + size + ") > source size (" + mSize +")"); 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /android-pack-plugin/packPlugin/src/main/java/com/android/apksigner/core/internal/util/ByteBufferSink.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.android.apksigner.core.internal.util; 18 | 19 | import com.android.apksigner.core.util.DataSink; 20 | 21 | import java.io.IOException; 22 | import java.nio.BufferOverflowException; 23 | import java.nio.ByteBuffer; 24 | 25 | /** 26 | * Data sink which stores all received data into the associated {@link ByteBuffer}. 27 | */ 28 | public class ByteBufferSink implements DataSink { 29 | 30 | private final ByteBuffer mBuffer; 31 | 32 | public ByteBufferSink(ByteBuffer buffer) { 33 | mBuffer = buffer; 34 | } 35 | 36 | @Override 37 | public void consume(byte[] buf, int offset, int length) throws IOException { 38 | try { 39 | mBuffer.put(buf, offset, length); 40 | } catch (BufferOverflowException e) { 41 | throw new IOException( 42 | "Insufficient space in output buffer for " + length + " bytes", e); 43 | } 44 | } 45 | 46 | @Override 47 | public void consume(ByteBuffer buf) throws IOException { 48 | int length = buf.remaining(); 49 | try { 50 | mBuffer.put(buf); 51 | } catch (BufferOverflowException e) { 52 | throw new IOException( 53 | "Insufficient space in output buffer for " + length + " bytes", e); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /android-pack-plugin/packPlugin/src/main/java/com/android/apksigner/core/internal/util/DelegatingX509Certificate.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.android.apksigner.core.internal.util; 18 | 19 | import java.math.BigInteger; 20 | import java.security.*; 21 | import java.security.cert.*; 22 | import java.util.Date; 23 | import java.util.Set; 24 | 25 | /** 26 | * {@link X509Certificate} which delegates all method invocations to the provided delegate 27 | * {@code X509Certificate}. 28 | */ 29 | public class DelegatingX509Certificate extends X509Certificate { 30 | private final X509Certificate mDelegate; 31 | 32 | public DelegatingX509Certificate(X509Certificate delegate) { 33 | this.mDelegate = delegate; 34 | } 35 | 36 | @Override 37 | public Set getCriticalExtensionOIDs() { 38 | return mDelegate.getCriticalExtensionOIDs(); 39 | } 40 | 41 | @Override 42 | public byte[] getExtensionValue(String oid) { 43 | return mDelegate.getExtensionValue(oid); 44 | } 45 | 46 | @Override 47 | public Set getNonCriticalExtensionOIDs() { 48 | return mDelegate.getNonCriticalExtensionOIDs(); 49 | } 50 | 51 | @Override 52 | public boolean hasUnsupportedCriticalExtension() { 53 | return mDelegate.hasUnsupportedCriticalExtension(); 54 | } 55 | 56 | @Override 57 | public void checkValidity() 58 | throws CertificateExpiredException, CertificateNotYetValidException { 59 | mDelegate.checkValidity(); 60 | } 61 | 62 | @Override 63 | public void checkValidity(Date date) 64 | throws CertificateExpiredException, CertificateNotYetValidException { 65 | mDelegate.checkValidity(date); 66 | } 67 | 68 | @Override 69 | public int getVersion() { 70 | return mDelegate.getVersion(); 71 | } 72 | 73 | @Override 74 | public BigInteger getSerialNumber() { 75 | return mDelegate.getSerialNumber(); 76 | } 77 | 78 | @Override 79 | public Principal getIssuerDN() { 80 | return mDelegate.getIssuerDN(); 81 | } 82 | 83 | @Override 84 | public Principal getSubjectDN() { 85 | return mDelegate.getSubjectDN(); 86 | } 87 | 88 | @Override 89 | public Date getNotBefore() { 90 | return mDelegate.getNotBefore(); 91 | } 92 | 93 | @Override 94 | public Date getNotAfter() { 95 | return mDelegate.getNotAfter(); 96 | } 97 | 98 | @Override 99 | public byte[] getTBSCertificate() throws CertificateEncodingException { 100 | return mDelegate.getTBSCertificate(); 101 | } 102 | 103 | @Override 104 | public byte[] getSignature() { 105 | return mDelegate.getSignature(); 106 | } 107 | 108 | @Override 109 | public String getSigAlgName() { 110 | return mDelegate.getSigAlgName(); 111 | } 112 | 113 | @Override 114 | public String getSigAlgOID() { 115 | return mDelegate.getSigAlgOID(); 116 | } 117 | 118 | @Override 119 | public byte[] getSigAlgParams() { 120 | return mDelegate.getSigAlgParams(); 121 | } 122 | 123 | @Override 124 | public boolean[] getIssuerUniqueID() { 125 | return mDelegate.getIssuerUniqueID(); 126 | } 127 | 128 | @Override 129 | public boolean[] getSubjectUniqueID() { 130 | return mDelegate.getSubjectUniqueID(); 131 | } 132 | 133 | @Override 134 | public boolean[] getKeyUsage() { 135 | return mDelegate.getKeyUsage(); 136 | } 137 | 138 | @Override 139 | public int getBasicConstraints() { 140 | return mDelegate.getBasicConstraints(); 141 | } 142 | 143 | @Override 144 | public byte[] getEncoded() throws CertificateEncodingException { 145 | return mDelegate.getEncoded(); 146 | } 147 | 148 | @Override 149 | public void verify(PublicKey key) throws CertificateException, NoSuchAlgorithmException, 150 | InvalidKeyException, NoSuchProviderException, SignatureException { 151 | mDelegate.verify(key); 152 | } 153 | 154 | @Override 155 | public void verify(PublicKey key, String sigProvider) 156 | throws CertificateException, NoSuchAlgorithmException, InvalidKeyException, 157 | NoSuchProviderException, SignatureException { 158 | mDelegate.verify(key, sigProvider); 159 | } 160 | 161 | @Override 162 | public String toString() { 163 | return mDelegate.toString(); 164 | } 165 | 166 | @Override 167 | public PublicKey getPublicKey() { 168 | return mDelegate.getPublicKey(); 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /android-pack-plugin/packPlugin/src/main/java/com/android/apksigner/core/internal/util/MessageDigestSink.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.android.apksigner.core.internal.util; 17 | 18 | import com.android.apksigner.core.util.DataSink; 19 | 20 | import java.nio.ByteBuffer; 21 | import java.security.MessageDigest; 22 | 23 | /** 24 | * Data sink which feeds all received data into the associated {@link MessageDigest} instances. Each 25 | * {@code MessageDigest} instance receives the same data. 26 | */ 27 | public class MessageDigestSink implements DataSink { 28 | 29 | private final MessageDigest[] mMessageDigests; 30 | 31 | public MessageDigestSink(MessageDigest[] digests) { 32 | mMessageDigests = digests; 33 | } 34 | 35 | @Override 36 | public void consume(byte[] buf, int offset, int length) { 37 | for (MessageDigest md : mMessageDigests) { 38 | md.update(buf, offset, length); 39 | } 40 | } 41 | 42 | @Override 43 | public void consume(ByteBuffer buf) { 44 | int originalPosition = buf.position(); 45 | for (MessageDigest md : mMessageDigests) { 46 | // Reset the position back to the original because the previous iteration's 47 | // MessageDigest.update set the buffer's position to the buffer's limit. 48 | buf.position(originalPosition); 49 | md.update(buf); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /android-pack-plugin/packPlugin/src/main/java/com/android/apksigner/core/internal/util/Pair.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.android.apksigner.core.internal.util; 18 | 19 | /** 20 | * Pair of two elements. 21 | */ 22 | public final class Pair { 23 | private final A mFirst; 24 | private final B mSecond; 25 | 26 | private Pair(A first, B second) { 27 | mFirst = first; 28 | mSecond = second; 29 | } 30 | 31 | public static Pair of(A first, B second) { 32 | return new Pair(first, second); 33 | } 34 | 35 | public A getFirst() { 36 | return mFirst; 37 | } 38 | 39 | public B getSecond() { 40 | return mSecond; 41 | } 42 | 43 | @Override 44 | public int hashCode() { 45 | final int prime = 31; 46 | int result = 1; 47 | result = prime * result + ((mFirst == null) ? 0 : mFirst.hashCode()); 48 | result = prime * result + ((mSecond == null) ? 0 : mSecond.hashCode()); 49 | return result; 50 | } 51 | 52 | @Override 53 | public boolean equals(Object obj) { 54 | if (this == obj) { 55 | return true; 56 | } 57 | if (obj == null) { 58 | return false; 59 | } 60 | if (getClass() != obj.getClass()) { 61 | return false; 62 | } 63 | @SuppressWarnings("rawtypes") 64 | Pair other = (Pair) obj; 65 | if (mFirst == null) { 66 | if (other.mFirst != null) { 67 | return false; 68 | } 69 | } else if (!mFirst.equals(other.mFirst)) { 70 | return false; 71 | } 72 | if (mSecond == null) { 73 | if (other.mSecond != null) { 74 | return false; 75 | } 76 | } else if (!mSecond.equals(other.mSecond)) { 77 | return false; 78 | } 79 | return true; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /android-pack-plugin/packPlugin/src/main/java/com/android/apksigner/core/internal/zip/ZipUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.android.apksigner.core.internal.zip; 18 | 19 | import com.android.apksigner.core.internal.util.Pair; 20 | import com.android.apksigner.core.util.DataSource; 21 | 22 | import java.io.IOException; 23 | import java.nio.ByteBuffer; 24 | import java.nio.ByteOrder; 25 | 26 | /** 27 | * Assorted ZIP format helpers. 28 | * 29 | *

NOTE: Most helper methods operating on {@code ByteBuffer} instances expect that the byte 30 | * order of these buffers is little-endian. 31 | */ 32 | public abstract class ZipUtils { 33 | private ZipUtils() {} 34 | 35 | public static final short COMPRESSION_METHOD_STORED = 0; 36 | public static final short COMPRESSION_METHOD_DEFLATED = 8; 37 | 38 | private static final int ZIP_EOCD_REC_MIN_SIZE = 22; 39 | private static final int ZIP_EOCD_REC_SIG = 0x06054b50; 40 | private static final int ZIP_EOCD_CENTRAL_DIR_TOTAL_RECORD_COUNT_OFFSET = 10; 41 | private static final int ZIP_EOCD_CENTRAL_DIR_SIZE_FIELD_OFFSET = 12; 42 | private static final int ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET = 16; 43 | private static final int ZIP_EOCD_COMMENT_LENGTH_FIELD_OFFSET = 20; 44 | 45 | private static final int ZIP64_EOCD_LOCATOR_SIZE = 20; 46 | private static final int ZIP64_EOCD_LOCATOR_SIG = 0x07064b50; 47 | 48 | private static final int UINT16_MAX_VALUE = 0xffff; 49 | 50 | /** 51 | * Sets the offset of the start of the ZIP Central Directory in the archive. 52 | * 53 | *

NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian. 54 | */ 55 | public static void setZipEocdCentralDirectoryOffset( 56 | ByteBuffer zipEndOfCentralDirectory, long offset) { 57 | assertByteOrderLittleEndian(zipEndOfCentralDirectory); 58 | setUnsignedInt32( 59 | zipEndOfCentralDirectory, 60 | zipEndOfCentralDirectory.position() + ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET, 61 | offset); 62 | } 63 | 64 | /** 65 | * Returns the offset of the start of the ZIP Central Directory in the archive. 66 | * 67 | *

NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian. 68 | */ 69 | public static long getZipEocdCentralDirectoryOffset(ByteBuffer zipEndOfCentralDirectory) { 70 | assertByteOrderLittleEndian(zipEndOfCentralDirectory); 71 | return getUnsignedInt32( 72 | zipEndOfCentralDirectory, 73 | zipEndOfCentralDirectory.position() + ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET); 74 | } 75 | 76 | /** 77 | * Returns the size (in bytes) of the ZIP Central Directory. 78 | * 79 | *

NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian. 80 | */ 81 | public static long getZipEocdCentralDirectorySizeBytes(ByteBuffer zipEndOfCentralDirectory) { 82 | assertByteOrderLittleEndian(zipEndOfCentralDirectory); 83 | return getUnsignedInt32( 84 | zipEndOfCentralDirectory, 85 | zipEndOfCentralDirectory.position() + ZIP_EOCD_CENTRAL_DIR_SIZE_FIELD_OFFSET); 86 | } 87 | 88 | /** 89 | * Returns the total number of records in ZIP Central Directory. 90 | * 91 | *

NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian. 92 | */ 93 | public static int getZipEocdCentralDirectoryTotalRecordCount( 94 | ByteBuffer zipEndOfCentralDirectory) { 95 | assertByteOrderLittleEndian(zipEndOfCentralDirectory); 96 | return getUnsignedInt16( 97 | zipEndOfCentralDirectory, 98 | zipEndOfCentralDirectory.position() 99 | + ZIP_EOCD_CENTRAL_DIR_TOTAL_RECORD_COUNT_OFFSET); 100 | } 101 | 102 | /** 103 | * Returns the ZIP End of Central Directory record of the provided ZIP file. 104 | * 105 | * @return contents of the ZIP End of Central Directory record and the record's offset in the 106 | * file or {@code null} if the file does not contain the record. 107 | * 108 | * @throws IOException if an I/O error occurs while reading the file. 109 | */ 110 | public static Pair findZipEndOfCentralDirectoryRecord(DataSource zip) 111 | throws IOException { 112 | // ZIP End of Central Directory (EOCD) record is located at the very end of the ZIP archive. 113 | // The record can be identified by its 4-byte signature/magic which is located at the very 114 | // beginning of the record. A complication is that the record is variable-length because of 115 | // the comment field. 116 | // The algorithm for locating the ZIP EOCD record is as follows. We search backwards from 117 | // end of the buffer for the EOCD record signature. Whenever we find a signature, we check 118 | // the candidate record's comment length is such that the remainder of the record takes up 119 | // exactly the remaining bytes in the buffer. The search is bounded because the maximum 120 | // size of the comment field is 65535 bytes because the field is an unsigned 16-bit number. 121 | 122 | long fileSize = zip.size(); 123 | if (fileSize < ZIP_EOCD_REC_MIN_SIZE) { 124 | return null; 125 | } 126 | 127 | // Optimization: 99.99% of APKs have a zero-length comment field in the EoCD record and thus 128 | // the EoCD record offset is known in advance. Try that offset first to avoid unnecessarily 129 | // reading more data. 130 | Pair result = findZipEndOfCentralDirectoryRecord(zip, 0); 131 | if (result != null) { 132 | return result; 133 | } 134 | 135 | // EoCD does not start where we expected it to. Perhaps it contains a non-empty comment 136 | // field. Expand the search. The maximum size of the comment field in EoCD is 65535 because 137 | // the comment length field is an unsigned 16-bit number. 138 | return findZipEndOfCentralDirectoryRecord(zip, UINT16_MAX_VALUE); 139 | } 140 | 141 | /** 142 | * Returns the ZIP End of Central Directory record of the provided ZIP file. 143 | * 144 | * @param maxCommentSize maximum accepted size (in bytes) of EoCD comment field. The permitted 145 | * value is from 0 to 65535 inclusive. The smaller the value, the faster this method 146 | * locates the record, provided its comment field is no longer than this value. 147 | * 148 | * @return contents of the ZIP End of Central Directory record and the record's offset in the 149 | * file or {@code null} if the file does not contain the record. 150 | * 151 | * @throws IOException if an I/O error occurs while reading the file. 152 | */ 153 | private static Pair findZipEndOfCentralDirectoryRecord( 154 | DataSource zip, int maxCommentSize) throws IOException { 155 | // ZIP End of Central Directory (EOCD) record is located at the very end of the ZIP archive. 156 | // The record can be identified by its 4-byte signature/magic which is located at the very 157 | // beginning of the record. A complication is that the record is variable-length because of 158 | // the comment field. 159 | // The algorithm for locating the ZIP EOCD record is as follows. We search backwards from 160 | // end of the buffer for the EOCD record signature. Whenever we find a signature, we check 161 | // the candidate record's comment length is such that the remainder of the record takes up 162 | // exactly the remaining bytes in the buffer. The search is bounded because the maximum 163 | // size of the comment field is 65535 bytes because the field is an unsigned 16-bit number. 164 | 165 | if ((maxCommentSize < 0) || (maxCommentSize > UINT16_MAX_VALUE)) { 166 | throw new IllegalArgumentException("maxCommentSize: " + maxCommentSize); 167 | } 168 | 169 | long fileSize = zip.size(); 170 | if (fileSize < ZIP_EOCD_REC_MIN_SIZE) { 171 | // No space for EoCD record in the file. 172 | return null; 173 | } 174 | // Lower maxCommentSize if the file is too small. 175 | maxCommentSize = (int) Math.min(maxCommentSize, fileSize - ZIP_EOCD_REC_MIN_SIZE); 176 | 177 | int maxEocdSize = ZIP_EOCD_REC_MIN_SIZE + maxCommentSize; 178 | long bufOffsetInFile = fileSize - maxEocdSize; 179 | ByteBuffer buf = zip.getByteBuffer(bufOffsetInFile, maxEocdSize); 180 | buf.order(ByteOrder.LITTLE_ENDIAN); 181 | int eocdOffsetInBuf = findZipEndOfCentralDirectoryRecord(buf); 182 | if (eocdOffsetInBuf == -1) { 183 | // No EoCD record found in the buffer 184 | return null; 185 | } 186 | // EoCD found 187 | buf.position(eocdOffsetInBuf); 188 | ByteBuffer eocd = buf.slice(); 189 | eocd.order(ByteOrder.LITTLE_ENDIAN); 190 | return Pair.of(eocd, bufOffsetInFile + eocdOffsetInBuf); 191 | } 192 | 193 | /** 194 | * Returns the position at which ZIP End of Central Directory record starts in the provided 195 | * buffer or {@code -1} if the record is not present. 196 | * 197 | *

NOTE: Byte order of {@code zipContents} must be little-endian. 198 | */ 199 | private static int findZipEndOfCentralDirectoryRecord(ByteBuffer zipContents) { 200 | assertByteOrderLittleEndian(zipContents); 201 | 202 | // ZIP End of Central Directory (EOCD) record is located at the very end of the ZIP archive. 203 | // The record can be identified by its 4-byte signature/magic which is located at the very 204 | // beginning of the record. A complication is that the record is variable-length because of 205 | // the comment field. 206 | // The algorithm for locating the ZIP EOCD record is as follows. We search backwards from 207 | // end of the buffer for the EOCD record signature. Whenever we find a signature, we check 208 | // the candidate record's comment length is such that the remainder of the record takes up 209 | // exactly the remaining bytes in the buffer. The search is bounded because the maximum 210 | // size of the comment field is 65535 bytes because the field is an unsigned 16-bit number. 211 | 212 | int archiveSize = zipContents.capacity(); 213 | if (archiveSize < ZIP_EOCD_REC_MIN_SIZE) { 214 | return -1; 215 | } 216 | int maxCommentLength = Math.min(archiveSize - ZIP_EOCD_REC_MIN_SIZE, UINT16_MAX_VALUE); 217 | int eocdWithEmptyCommentStartPosition = archiveSize - ZIP_EOCD_REC_MIN_SIZE; 218 | for (int expectedCommentLength = 0; expectedCommentLength < maxCommentLength; 219 | expectedCommentLength++) { 220 | int eocdStartPos = eocdWithEmptyCommentStartPosition - expectedCommentLength; 221 | if (zipContents.getInt(eocdStartPos) == ZIP_EOCD_REC_SIG) { 222 | int actualCommentLength = 223 | getUnsignedInt16( 224 | zipContents, eocdStartPos + ZIP_EOCD_COMMENT_LENGTH_FIELD_OFFSET); 225 | if (actualCommentLength == expectedCommentLength) { 226 | return eocdStartPos; 227 | } 228 | } 229 | } 230 | 231 | return -1; 232 | } 233 | 234 | /** 235 | * Returns {@code true} if the provided file contains a ZIP64 End of Central Directory 236 | * Locator. 237 | * 238 | * @param zipEndOfCentralDirectoryPosition offset of the ZIP End of Central Directory record 239 | * in the file. 240 | * 241 | * @throws IOException if an I/O error occurs while reading the data source 242 | */ 243 | public static final boolean isZip64EndOfCentralDirectoryLocatorPresent( 244 | DataSource zip, long zipEndOfCentralDirectoryPosition) throws IOException { 245 | 246 | // ZIP64 End of Central Directory Locator immediately precedes the ZIP End of Central 247 | // Directory Record. 248 | long locatorPosition = zipEndOfCentralDirectoryPosition - ZIP64_EOCD_LOCATOR_SIZE; 249 | if (locatorPosition < 0) { 250 | return false; 251 | } 252 | 253 | ByteBuffer sig = zip.getByteBuffer(locatorPosition, 4); 254 | sig.order(ByteOrder.LITTLE_ENDIAN); 255 | return sig.getInt(0) == ZIP64_EOCD_LOCATOR_SIG; 256 | } 257 | 258 | private static void assertByteOrderLittleEndian(ByteBuffer buffer) { 259 | if (buffer.order() != ByteOrder.LITTLE_ENDIAN) { 260 | throw new IllegalArgumentException("ByteBuffer byte order must be little endian"); 261 | } 262 | } 263 | 264 | private static int getUnsignedInt16(ByteBuffer buffer, int offset) { 265 | return buffer.getShort(offset) & 0xffff; 266 | } 267 | 268 | private static void setUnsignedInt32(ByteBuffer buffer, int offset, long value) { 269 | if ((value < 0) || (value > 0xffffffffL)) { 270 | throw new IllegalArgumentException("uint32 value of out range: " + value); 271 | } 272 | buffer.putInt(offset, (int) value); 273 | } 274 | 275 | private static long getUnsignedInt32(ByteBuffer buffer, int offset) { 276 | return buffer.getInt(offset) & 0xffffffffL; 277 | } 278 | } 279 | -------------------------------------------------------------------------------- /android-pack-plugin/packPlugin/src/main/java/com/android/apksigner/core/util/DataSink.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.android.apksigner.core.util; 18 | 19 | import java.io.IOException; 20 | import java.nio.ByteBuffer; 21 | 22 | /** 23 | * Consumer of input data which may be provided in one go or in chunks. 24 | */ 25 | public interface DataSink { 26 | 27 | /** 28 | * Consumes the provided chunk of data. 29 | * 30 | *

This data sink guarantees to not hold references to the provided buffer after this method 31 | * terminates. 32 | */ 33 | void consume(byte[] buf, int offset, int length) throws IOException; 34 | 35 | /** 36 | * Consumes all remaining data in the provided buffer and advances the buffer's position 37 | * to the buffer's limit. 38 | * 39 | *

This data sink guarantees to not hold references to the provided buffer after this method 40 | * terminates. 41 | */ 42 | void consume(ByteBuffer buf) throws IOException; 43 | } 44 | -------------------------------------------------------------------------------- /android-pack-plugin/packPlugin/src/main/java/com/android/apksigner/core/util/DataSource.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.android.apksigner.core.util; 18 | 19 | import java.io.IOException; 20 | import java.nio.ByteBuffer; 21 | 22 | /** 23 | * Abstract representation of a source of data. 24 | * 25 | *

This abstraction serves three purposes: 26 | *

    27 | *
  • Transparent handling of different types of sources, such as {@code byte[]}, 28 | * {@link ByteBuffer}, {@link java.io.RandomAccessFile}, memory-mapped file.
  • 29 | *
  • Support sources larger than 2 GB. If all sources were smaller than 2 GB, {@code ByteBuffer} 30 | * may have worked as the unifying abstraction.
  • 31 | *
  • Support sources which do not fit into logical memory as a contiguous region.
  • 32 | *
33 | * 34 | *

There are following ways to obtain a chunk of data from the data source: 35 | *

    36 | *
  • Stream the chunk's data into a {@link DataSink} using 37 | * {@link #feed(long, long, DataSink) feed}. This is best suited for scenarios where there is no 38 | * need to have the chunk's data accessible at the same time, for example, when computing the 39 | * digest of the chunk. If you need to keep the chunk's data around after {@code feed} 40 | * completes, you must create a copy during {@code feed}. However, in that case the following 41 | * methods of obtaining the chunk's data may be more appropriate.
  • 42 | *
  • Obtain a {@link ByteBuffer} containing the chunk's data using 43 | * {@link #getByteBuffer(long, int) getByteBuffer}. Depending on the data source, the chunk's 44 | * data may or may not be copied by this operation. This is best suited for scenarios where 45 | * you need to access the chunk's data in arbitrary order, but don't need to modify the data and 46 | * thus don't require a copy of the data.
  • 47 | *
  • Copy the chunk's data to a {@link ByteBuffer} using 48 | * {@link #copyTo(long, int, ByteBuffer) copyTo}. This is best suited for scenarios where 49 | * you require a copy of the chunk's data, such as to when you need to modify the data. 50 | *
  • 51 | *
52 | */ 53 | public interface DataSource { 54 | 55 | /** 56 | * Returns the amount of data (in bytes) contained in this data source. 57 | */ 58 | long size(); 59 | 60 | /** 61 | * Feeds the specified chunk from this data source into the provided sink. 62 | * 63 | * @param offset index (in bytes) at which the chunk starts inside data source 64 | * @param size size (in bytes) of the chunk 65 | */ 66 | void feed(long offset, long size, DataSink sink) throws IOException; 67 | 68 | /** 69 | * Returns a buffer holding the contents of the specified chunk of data from this data source. 70 | * Changes to the data source are not guaranteed to be reflected in the returned buffer. 71 | * Similarly, changes in the buffer are not guaranteed to be reflected in the data source. 72 | * 73 | *

The returned buffer's position is {@code 0}, and the buffer's limit and capacity is 74 | * {@code size}. 75 | * 76 | * @param offset index (in bytes) at which the chunk starts inside data source 77 | * @param size size (in bytes) of the chunk 78 | */ 79 | ByteBuffer getByteBuffer(long offset, int size) throws IOException; 80 | 81 | /** 82 | * Copies the specified chunk from this data source into the provided destination buffer, 83 | * advancing the destination buffer's position by {@code size}. 84 | * 85 | * @param offset index (in bytes) at which the chunk starts inside data source 86 | * @param size size (in bytes) of the chunk 87 | */ 88 | void copyTo(long offset, int size, ByteBuffer dest) throws IOException; 89 | 90 | /** 91 | * Returns a data source representing the specified region of data of this data source. Changes 92 | * to data represented by this data source will also be visible in the returned data source. 93 | * 94 | * @param offset index (in bytes) at which the region starts inside data source 95 | * @param size size (in bytes) of the region 96 | */ 97 | DataSource slice(long offset, long size); 98 | } 99 | -------------------------------------------------------------------------------- /android-pack-plugin/packPlugin/src/main/java/com/android/apksigner/core/util/DataSources.java: -------------------------------------------------------------------------------- 1 | package com.android.apksigner.core.util; 2 | 3 | 4 | import com.android.apksigner.core.internal.util.ByteBufferDataSource; 5 | 6 | import java.nio.ByteBuffer; 7 | 8 | /** 9 | * Utility methods for working with {@link DataSource} abstraction. 10 | */ 11 | public abstract class DataSources { 12 | private DataSources() {} 13 | 14 | /** 15 | * Returns a {@link DataSource} backed by the provided {@link ByteBuffer}. The data source 16 | * represents the data contained between the position and limit of the buffer. Changes to the 17 | * buffer's contents will be visible in the data source. 18 | */ 19 | public static DataSource asDataSource(ByteBuffer buffer) { 20 | if (buffer == null) { 21 | throw new NullPointerException(); 22 | } 23 | return new ByteBufferDataSource(buffer); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /android-pack-plugin/packPlugin/src/main/java/com/android/apksigner/core/zip/ZipFormatException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.android.apksigner.core.zip; 18 | 19 | /** 20 | * Indicates that a ZIP archive is not well-formed. 21 | */ 22 | public class ZipFormatException extends Exception { 23 | private static final long serialVersionUID = 1L; 24 | 25 | public ZipFormatException(String message) { 26 | super(message); 27 | } 28 | 29 | public ZipFormatException(String message, Throwable cause) { 30 | super(message, cause); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /android-pack-plugin/packPlugin/src/main/resources/META-INF/gradle-plugins/cn.zengcanxiang.androidPackPlugin.properties: -------------------------------------------------------------------------------- 1 | implementation-class=cn.zengcanxiang.packplugin.PluginEntranceImpl 2 | -------------------------------------------------------------------------------- /android-pack-plugin/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'pack-plugin' 2 | include 'packPlugin' 3 | findProject(':packPlugin')?.name = 'android-pack-plugin' 4 | 5 | -------------------------------------------------------------------------------- /walle_cli.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zengcanxiang/Android-pack-plugin/a44c4680932ee2cf5cb41478ec7fcba8cd8af7d9/walle_cli.jar --------------------------------------------------------------------------------