diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e14e1d6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,72 @@ +# Created by .ignore support plugin (hsz.mobi) +### Java template +*.class + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.ear + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* +### Gradle template +.gradle +build/ + +# Ignore Gradle GUI config +gradle-app.setting + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar + +# Cache of project +.gradletasknamecache + +# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 +# gradle/wrapper/gradle-wrapper.properties +### Android template +# Built application files +*.apk +*.ap_ + +# Files for the ART/Dalvik VM +*.dex + +# Java class files +*.class + +# Generated files +bin/ +gen/ +out/ + +# Gradle files +.gradle/ +build/ + +# Local configuration file (sdk path, etc) +local.properties + +# Proguard folder generated by Eclipse +proguard/ + +# Log Files +*.log + +# Android Studio Navigation editor temp files +.navigation/ + +# Android Studio captures folder +captures/ + +# Intellij +*.iml +.idea/workspace.xml +.idea + +# Keystore files +*.jks + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e69de29 diff --git a/README.md b/README.md new file mode 100644 index 0000000..5033e75 --- /dev/null +++ b/README.md @@ -0,0 +1,84 @@ +## Walle +[![license](http://img.shields.io/badge/license-BSD3-brightgreen.svg?style=flat)](https://github.com/Tencent/tinker/blob/master/LICENSE) + +支持Android Signature V2 Scheme的渠道包打包神器 + +![walle.png](assets/walle.png) + +## 使用说明 +在位于项目的根目录 `build.gradle` 文件中添加walle plugin的依赖 +Add tinker-gradle-plugin as a dependency in your main in the root of your project: + +```gradle +buildscript { + dependencies { + classpath ('com.meituan.android.walle:plugin:0.0.1') + } +} +``` + +Then you need to "apply" the plugin and add dependencies by adding the following lines to your `app/build.gradle`. + +```gradle +dependencies { + //optional, help to generate the final application + compile('com.tencent.tinker:tinker-android-anno:1.6.0') + //tinker's main Android lib + compile('com.tencent.tinker:tinker-android-lib:1.6.0') +} +... +... +apply plugin: 'com.tencent.tinker.patch' +``` + +If your app has a class that subclasses android.app.Application, then you need to modify that class, and move all its implements to [SampleApplicationLike](https://github.com/Tencent/tinker/blob/master/tinker-sample-android/app/src/main/java/tinker/sample/android/app/SampleApplicationLike.java) rather than Application: + +```java +-public class YourApplication extends Application { ++public class SampleApplicationLike extends DefaultApplicationLike { +``` + +Now you should change your `Application` class, make it a subclass of [TinkerApplication](https://github.com/Tencent/tinker/blob/master/tinker-android/tinker-android-loader/src/main/java/com/tencent/tinker/loader/app/TinkerApplication.java). As you can see from its API, it is an abstract class that does not have a default constructor, so you must define a no-arg constructor: + +```java +public class SampleApplication extends TinkerApplication { + public SampleApplication() { + super( + //tinkerFlags, which types is supported + //dex only, library only, all support + ShareConstants.TINKER_ENABLE_ALL, + // This is passed as a string so the shell application does not + // have a binary dependency on your ApplicationLifeCycle class. + "tinker.sample.android.SampleApplicationLike"); + } +} +``` + +Use `tinker-android-anno` to generate your `Application` is recommended, you just need to add an annotation for your [SampleApplicationLike](https://github.com/Tencent/tinker/blob/master/tinker-sample-android/app/src/main/java/tinker/sample/android/app/SampleApplicationLike.java) class + +```java +@DefaultLifeCycle( +application = "tinker.sample.android.app.SampleApplication", //application name to generate +flags = ShareConstants.TINKER_ENABLE_ALL) //tinkerFlags above +public class SampleApplicationLike extends DefaultApplicationLike +``` + +How to install tinker? learn more at the sample [SampleApplicationLike](https://github.com/Tencent/tinker/blob/master/tinker-sample-android/app/src/main/java/tinker/sample/android/app/SampleApplicationLike.java). + +For proguard, we have already made the proguard config automatic, and tinker will also generate the multiDex keep proguard file for you. + +For more tinker configurations, learn more at the sample [app/build.gradle](https://github.com/Tencent/tinker/blob/master/tinker-sample-android/app/build.gradle). + +## Support +Any problem? + +1. Learn more from [tinker-sample-android](https://github.com/Tencent/tinker/tree/master/tinker-sample-android). +2. Read the [source code](https://github.com/Tencent/tinker/tree/master). +3. Read the [wiki](https://github.com/Tencent/tinker/wiki) or [FAQ](https://github.com/Tencent/tinker/wiki/Tinker-%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98) for help. +4. Contact us for help. + +## Contributing + + +## License + diff --git a/assets/walle.png b/assets/walle.png new file mode 100644 index 0000000..20412f9 Binary files /dev/null and b/assets/walle.png differ diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..024bea4 --- /dev/null +++ b/build.gradle @@ -0,0 +1,53 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. +buildscript { + repositories { + mavenLocal() + jcenter() + } + dependencies { + classpath 'com.android.tools.build:gradle:2.2.0' + } +} + +allprojects { + repositories { + mavenLocal() + jcenter() + } + + tasks.withType(Javadoc).all { + enabled = false + options.encoding = 'UTF-8' + } + + tasks.withType(JavaCompile) { + options.encoding = "UTF-8" + } +} + +ext { + minSdkVersion = 15 + compileSdkVersion = 24 + targetSdkVersion = compileSdkVersion + buildToolsVersion = '24.0.1' + javaVersion = JavaVersion.VERSION_1_7 + + GROUP = 'com.meituan.android.walle' + VERSION_NAME = "${VERSION_NAME_PREFIX}${VERSION_NAME_SUFFIX}" + + POM_PACKAGING = "pom" + POM_DESCRIPTION = "walle" + POM_DESCRIPTION = 'Android Gradle Plugin For Multi -Channel' + POM_URL = '' + POM_SCM_URL = '' + POM_SCM_CONNECTION = '' + POM_SCM_DEV_CONNECTION = '' + POM_LICENCE_NAME = '' + POM_LICENCE_URL = '' + POM_LICENCE_DIST = 'repo' + POM_DEVELOPER_ID = 'achellies' + POM_DEVELOPER_NAME = 'achellies' + POM_DEVELOPER_EMAIL = 'achellies @163.com' + RELEASE_REPOSITORY_URL = 'http://nexus:8081/nexus/content/repositories/releases/' + SNAPSHOT_REPOSITORY_URL = 'http://nexus:8081/nexus/content/repositories/snapshots/' +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..3459294 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,21 @@ +# Project-wide Gradle settings. + +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. + +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html + +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +# Default value: -Xmx10248m -XX:MaxPermSize=256m + org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 + +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true + +VERSION_NAME_PREFIX=0.0.1-SNAPSHOT +VERSION_NAME_SUFFIX= \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..05ef575 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..f469fe2 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,3 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip diff --git a/gradle_mvn_push.gradle b/gradle_mvn_push.gradle new file mode 100644 index 0000000..c23fc6e --- /dev/null +++ b/gradle_mvn_push.gradle @@ -0,0 +1,206 @@ +/* + * Copyright 2015 achellies + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +apply plugin: 'maven' +apply plugin: 'signing' + +def isReleaseBuild() { + return VERSION_NAME.contains("SNAPSHOT") == false +} + +def getReleaseRepositoryUrl() { + return hasProperty('RELEASE_REPOSITORY_URL') ? RELEASE_REPOSITORY_URL + : "https://oss.sonatype.org/service/local/staging/deploy/maven2/" +} + +def getSnapshotRepositoryUrl() { + return hasProperty('SNAPSHOT_REPOSITORY_URL') ? SNAPSHOT_REPOSITORY_URL + : "https://oss.sonatype.org/content/repositories/snapshots/" +} + +def getRepositoryUsername() { + return hasProperty('NEXUS_USERNAME') ? NEXUS_USERNAME : "meituan" +} + +def getRepositoryPassword() { + return hasProperty('NEXUS_PASSWORD') ? NEXUS_PASSWORD : "RBK1ZqAwTcNbiUq" +} + +afterEvaluate { project -> + uploadArchives { + repositories { + mavenDeployer { + beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) } + + pom.groupId = GROUP + pom.artifactId = POM_ARTIFACT_ID + pom.version = VERSION_NAME + + repository(url: getReleaseRepositoryUrl()) { + authentication(userName: getRepositoryUsername(), password: getRepositoryPassword()) + } + snapshotRepository(url: getSnapshotRepositoryUrl()) { + authentication(userName: getRepositoryUsername(), password: getRepositoryPassword()) + } + + pom.project { + name POM_NAME + packaging POM_PACKAGING + description POM_DESCRIPTION + url POM_URL + + scm { + url POM_SCM_URL + connection POM_SCM_CONNECTION + developerConnection POM_SCM_DEV_CONNECTION + } + + licenses { + license { + name POM_LICENCE_NAME + url POM_LICENCE_URL + distribution POM_LICENCE_DIST + } + } + + developers { + developer { + id POM_DEVELOPER_ID + name POM_DEVELOPER_NAME + } + } + } + } + } + } + + signing { + required { isReleaseBuild() && gradle.taskGraph.hasTask("uploadArchives") } + sign configurations.archives + } + + if (project.getPlugins().hasPlugin('com.android.application') || project.getPlugins().hasPlugin('com.android.library')) { + task install(type: Upload, dependsOn: assemble) { + repositories.mavenInstaller { + configuration = configurations.archives + + pom.groupId = GROUP + pom.artifactId = POM_ARTIFACT_ID + pom.version = VERSION_NAME + + pom.project { + name POM_NAME + packaging POM_PACKAGING + description POM_DESCRIPTION + url POM_URL + + scm { + url POM_SCM_URL + connection POM_SCM_CONNECTION + developerConnection POM_SCM_DEV_CONNECTION + } + + licenses { + license { + name POM_LICENCE_NAME + url POM_LICENCE_URL + distribution POM_LICENCE_DIST + } + } + + developers { + developer { + id POM_DEVELOPER_ID + name POM_DEVELOPER_NAME + } + } + } + } + } + + task androidJavadocs(type: Javadoc) { + failOnError false + source = android.sourceSets.main.java.source + classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) + } + + task androidJavadocsJar(type: Jar, dependsOn: androidJavadocs) { + classifier = 'javadoc' + from androidJavadocs.destinationDir + } + + task androidSourcesJar(type: Jar) { + classifier = 'sources' + from android.sourceSets.main.java.source + } + } else { + install { + repositories.mavenInstaller { + pom.groupId = GROUP + pom.artifactId = POM_ARTIFACT_ID + pom.version = VERSION_NAME + + pom.project { + name POM_NAME + packaging POM_PACKAGING + description POM_DESCRIPTION + url POM_URL + + scm { + url POM_SCM_URL + connection POM_SCM_CONNECTION + developerConnection POM_SCM_DEV_CONNECTION + } + + licenses { + license { + name POM_LICENCE_NAME + url POM_LICENCE_URL + distribution POM_LICENCE_DIST + } + } + + developers { + developer { + id POM_DEVELOPER_ID + name POM_DEVELOPER_NAME + } + } + } + } + } + + task sourcesJar(type: Jar, dependsOn:classes) { + classifier = 'sources' + from sourceSets.main.allSource + } + + task javadocJar(type: Jar, dependsOn:javadoc) { + classifier = 'javadoc' + from javadoc.destinationDir + } + } + + artifacts { + if (project.getPlugins().hasPlugin('com.android.application') || project.getPlugins().hasPlugin('com.android.library')) { + archives androidSourcesJar + archives androidJavadocsJar + } else { + archives sourcesJar + archives javadocJar + } + } +} \ No newline at end of file diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..9d82f78 --- /dev/null +++ b/gradlew @@ -0,0 +1,160 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..8a0b282 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/library/build.gradle b/library/build.gradle new file mode 100644 index 0000000..34a949d --- /dev/null +++ b/library/build.gradle @@ -0,0 +1,27 @@ +apply plugin: 'com.android.library' +apply from: file('../gradle_mvn_push.gradle') + +android { + compileSdkVersion rootProject.ext.compileSdkVersion + buildToolsVersion rootProject.ext.buildToolsVersion + + defaultConfig { + minSdkVersion rootProject.ext.minSdkVersion + targetSdkVersion rootProject.ext.targetSdkVersion + } + + buildTypes { + debug { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + release { + minifyEnabled true + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + compile fileTree(dir: 'libs', include: ['*.jar']) +} \ No newline at end of file diff --git a/library/gradle.properties b/library/gradle.properties new file mode 100644 index 0000000..fb6855e --- /dev/null +++ b/library/gradle.properties @@ -0,0 +1,3 @@ +POM_ARTIFACT_ID=library +POM_NAME=Walle Android library +POM_PACKAGING=aar \ No newline at end of file diff --git a/library/proguard-rules.pro b/library/proguard-rules.pro new file mode 100644 index 0000000..ca7ea79 --- /dev/null +++ b/library/proguard-rules.pro @@ -0,0 +1,17 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /Users/zhangshaowen/Library/Android/sdk/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/library/src/main/AndroidManifest.xml b/library/src/main/AndroidManifest.xml new file mode 100644 index 0000000..838cbc3 --- /dev/null +++ b/library/src/main/AndroidManifest.xml @@ -0,0 +1,8 @@ + + + + + + + diff --git a/library/src/main/java/com/meituan/android/walle/ChannelReader.java b/library/src/main/java/com/meituan/android/walle/ChannelReader.java new file mode 100644 index 0000000..a712f78 --- /dev/null +++ b/library/src/main/java/com/meituan/android/walle/ChannelReader.java @@ -0,0 +1,8 @@ +package com.meituan.android.walle; + +/** + * Created by achellies on 16/11/10. + */ + +public class ChannelReader { +} diff --git a/plugin/build.gradle b/plugin/build.gradle new file mode 100755 index 0000000..3a31eff --- /dev/null +++ b/plugin/build.gradle @@ -0,0 +1,13 @@ +apply plugin: 'groovy' +apply from: file('../gradle_mvn_push.gradle') + +dependencies { + compile gradleApi() + compile localGroovy() + compile fileTree(dir: "./src/main/libs", include: ['*.jar']) + compile 'commons-io:commons-io:2.4' + compile 'commons-codec:commons-codec:1.6' + compile 'org.apache.commons:commons-lang3:3.4' + compile 'com.android.tools.build:gradle:2.2.0' +} + diff --git a/plugin/gradle.properties b/plugin/gradle.properties new file mode 100644 index 0000000..d47b2ba --- /dev/null +++ b/plugin/gradle.properties @@ -0,0 +1,3 @@ +POM_ARTIFACT_ID=plugin +POM_NAME=Walle Gradle Plugin +POM_PACKAGING=jar \ No newline at end of file diff --git a/plugin/src/main/groovy/com/meituan/android/walle/ApkSigningBlock.groovy b/plugin/src/main/groovy/com/meituan/android/walle/ApkSigningBlock.groovy new file mode 100644 index 0000000..43fbf4b --- /dev/null +++ b/plugin/src/main/groovy/com/meituan/android/walle/ApkSigningBlock.groovy @@ -0,0 +1,101 @@ +package com.meituan.android.walle + +import com.android.apksigner.core.internal.apk.v2.V2SchemeVerifier + +import java.nio.ByteBuffer +import java.nio.ByteOrder + +/** + * https://source.android.com/security/apksigning/v2.html + * https://en.wikipedia.org/wiki/Zip_(file_format) + * + */ +class ApkSigningBlock { + // The format of the APK Signing Block is as follows (all numeric fields are little-endian): + + // .size of block in bytes (excluding this field) (uint64) + // .Sequence of uint64-length-prefixed ID-value pairs: + // *ID (uint32) + // *value (variable-length: length of the pair - 4 bytes) + // .size of block in bytes—same as the very first field (uint64) + // .magic “APK Sig Block 42” (16 bytes) + + // FORMAT: + // OFFSET DATA TYPE DESCRIPTION + // * @+0 bytes uint64: size in bytes (excluding this field) + // * @+8 bytes payload + // * @-24 bytes uint64: size in bytes (same as the one above) + // * @-16 bytes uint128: magic + + // payload 有 8字节的大小,4字节的ID,还有payload的内容组成 + + private final List payloads; + + ApkSigningBlock() { + super(); + + payloads = new ArrayList<>(); + } + + public final List getPayloads() { + return payloads; + } + + public void addPayload(ApkSigningPayload payload) { + payloads.add(payload); + } + + public long writeApkSigningBlock(DataOutput dataOutput) { + long length = 24; + for (int index = 0; index < payloads.size(); ++index) { + ApkSigningPayload payload = payloads.get(index); + byte[] bytes = payload.getByteBuffer(); + length += 4 + 8 + bytes.length; + } + + ByteBuffer byteBuffer = ByteBuffer.allocate(Long.BYTES); + byteBuffer.order(ByteOrder.LITTLE_ENDIAN); + byteBuffer.putLong(length); + byteBuffer.flip(); + dataOutput.write(byteBuffer.array()); + + for (int index = 0; index < payloads.size(); ++index) { + ApkSigningPayload payload = payloads.get(index); + byte[] bytes = payload.getByteBuffer(); + + byteBuffer = ByteBuffer.allocate(Long.BYTES); + byteBuffer.order(ByteOrder.LITTLE_ENDIAN); + byteBuffer.putLong(bytes.length + (Long.BYTES - Integer.BYTES)); + byteBuffer.flip(); + dataOutput.write(byteBuffer.array()); + + byteBuffer = ByteBuffer.allocate(Integer.BYTES); + byteBuffer.order(ByteOrder.LITTLE_ENDIAN); + byteBuffer.putInt(payload.getId()); + byteBuffer.flip(); + dataOutput.write(byteBuffer.array()); + + dataOutput.write(bytes); + } + + byteBuffer = ByteBuffer.allocate(Long.BYTES); + byteBuffer.order(ByteOrder.LITTLE_ENDIAN); + byteBuffer.putLong(length); + byteBuffer.flip(); + dataOutput.write(byteBuffer.array()); + + byteBuffer = ByteBuffer.allocate(Long.BYTES); + byteBuffer.order(ByteOrder.LITTLE_ENDIAN); + byteBuffer.putLong(V2SchemeVerifier.APK_SIG_BLOCK_MAGIC_LO); + byteBuffer.flip(); + dataOutput.write(byteBuffer.array()); + + byteBuffer = ByteBuffer.allocate(Long.BYTES); + byteBuffer.order(ByteOrder.LITTLE_ENDIAN); + byteBuffer.putLong(V2SchemeVerifier.APK_SIG_BLOCK_MAGIC_HI); + byteBuffer.flip(); + dataOutput.write(byteBuffer.array()); + + return length; + } +} diff --git a/plugin/src/main/groovy/com/meituan/android/walle/ApkSigningPayload.groovy b/plugin/src/main/groovy/com/meituan/android/walle/ApkSigningPayload.groovy new file mode 100644 index 0000000..7c62731 --- /dev/null +++ b/plugin/src/main/groovy/com/meituan/android/walle/ApkSigningPayload.groovy @@ -0,0 +1,29 @@ +package com.meituan.android.walle + +import java.nio.ByteBuffer +import java.nio.ByteOrder + +class ApkSigningPayload { + private final int id; + private final ByteBuffer buffer + + ApkSigningPayload(int id, ByteBuffer buffer) { + super(); + this.id = id; + if (buffer.order() != ByteOrder.LITTLE_ENDIAN) { + throw new IllegalArgumentException("ByteBuffer byte order must be little endian"); + } + this.buffer = buffer; + } + + public int getId() { + return id; + } + + public byte[] getByteBuffer() { + final byte[] array = buffer.array(); + final int arrayOffset = buffer.arrayOffset(); + return Arrays.copyOfRange(array, arrayOffset + buffer.position(), + arrayOffset + buffer.limit()); ; + } +} diff --git a/plugin/src/main/groovy/com/meituan/android/walle/ChannelMaker.groovy b/plugin/src/main/groovy/com/meituan/android/walle/ChannelMaker.groovy new file mode 100644 index 0000000..e1b6226 --- /dev/null +++ b/plugin/src/main/groovy/com/meituan/android/walle/ChannelMaker.groovy @@ -0,0 +1,200 @@ +package com.meituan.android.walle + +import com.android.apksigner.core.ApkVerifier +import com.android.apksigner.core.apk.ApkUtils +import com.android.apksigner.core.internal.apk.v2.V2SchemeVerifier +import com.android.apksigner.core.internal.util.ByteBufferDataSource +import com.android.apksigner.core.internal.util.Pair +import com.android.apksigner.core.util.DataSource +import com.android.build.gradle.api.BaseVariant +import com.google.gson.JsonObject +import org.apache.commons.io.IOUtils +import org.gradle.api.DefaultTask +import org.gradle.api.GradleException +import org.gradle.api.Project +import org.gradle.api.tasks.TaskAction + +import java.nio.ByteBuffer +import java.nio.ByteOrder +import java.nio.channels.FileChannel + +class ChannelMaker extends DefaultTask { + public BaseVariant variant; + public Project project; + public File apkFile; + + public void setup() { + description "Make Multi-Channel" + group "Package" + } + + @TaskAction + public void packaging() { + if (apkFile == null || !apkFile.exists()) { + throw new GradleException("${apkFile} is not existed!"); + } + + ApkVerifier.Result result = verifyV2SignatureScheme(apkFile); + if (!result.verified || !result.verifiedUsingV2Scheme) { + throw new GradleException("${apkFile} has no v2 signature in Apk Signing Block!"); + } + + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("channel", (variant.flavorName != null && variant.flavorName.length() > 0) ? variant.flavorName : 'undefined'); + jsonObject.addProperty("buildType:", variant.buildType.name); + jsonObject.addProperty("timestamp", System.currentTimeMillis()); + + addSaltForV2SignatureScheme(apkFile, jsonObject); + } + + ApkVerifier.Result verifyV2SignatureScheme(File apkFile) { + FileInputStream fIn; + FileChannel fChan; + long fSize; + ByteBuffer byteBuffer; + + ApkVerifier.Result result = new ApkVerifier.Result(); + try { + fIn = new FileInputStream(apkFile); + fChan = fIn.getChannel(); + fSize = fChan.size(); + byteBuffer = ByteBuffer.allocate((int) fSize); + fChan.read(byteBuffer); + byteBuffer.rewind(); + + ApkVerifier apkVerifier = new ApkVerifier(); + + result = apkVerifier.verify(new ByteBufferDataSource(byteBuffer), 0); + } catch (IOException ignore) { + ignore.printStackTrace(); + } finally { + IOUtils.closeQuietly(fChan); + IOUtils.closeQuietly(fIn); + } + + return result; + } + + void addSaltForV2SignatureScheme(File apkFile, JsonObject jsonObject) { + FileInputStream fIn; + FileChannel fChan; + long fSize; + ByteBuffer byteBuffer; + + ApkSigningBlock apkSigningBlock = new ApkSigningBlock(); + long centralDirStartOffset = 0; + long apkSigningBlockOffset = 0; + try { + fIn = new FileInputStream(apkFile); + fChan = fIn.getChannel(); + fSize = fChan.size(); + byteBuffer = ByteBuffer.allocate((int) fSize); + fChan.read(byteBuffer); + byteBuffer.rewind(); + + byte[] zipData = byteBuffer.array(); + + // For a zip with no archive comment, the + // end-of-central-directory record will be 22 bytes long, so + // we expect to find the EOCD marker 22 bytes from the end. + if (zipData[zipData.length - 22] != 0x50 || + zipData[zipData.length - 21] != 0x4b || + zipData[zipData.length - 20] != 0x05 || + zipData[zipData.length - 19] != 0x06) { + throw new IllegalArgumentException("zip data already has an archive comment"); + } + + + V2SchemeVerifier.Result result = new V2SchemeVerifier.Result(); + + DataSource apk = new ByteBufferDataSource(byteBuffer); + ApkUtils.ZipSections zipSections = ApkUtils.findZipSections(apk); + + centralDirStartOffset = zipSections.getZipCentralDirectoryOffset(); + long centralDirEndOffset = + centralDirStartOffset + zipSections.getZipCentralDirectorySizeBytes(); + long eocdStartOffset = zipSections.getZipEndOfCentralDirectoryOffset(); + if (centralDirEndOffset != eocdStartOffset) { + throw new V2SchemeVerifier.SignatureNotFoundException( + "ZIP Central Directory is not immediately followed by End of Central Directory" + + ". CD end: " + centralDirEndOffset + + ", EoCD start: " + eocdStartOffset); + } + + // Find the APK Signing Block. The block immediately precedes the Central Directory. + ByteBuffer eocd = zipSections.getZipEndOfCentralDirectory(); + Pair apkSigningBlockAndOffset = + V2SchemeVerifier.findApkSigningBlock(apk, centralDirStartOffset); + ByteBuffer apkSigningBlock2 = apkSigningBlockAndOffset.getFirst(); + apkSigningBlockOffset = apkSigningBlockAndOffset.getSecond(); + + // Find the APK Signature Scheme v2 Block inside the APK Signing Block. + ByteBuffer apkSignatureSchemeV2Block = + V2SchemeVerifier.findApkSignatureSchemeV2Block(apkSigningBlock2, result); + + ApkSigningPayload payload = new ApkSigningPayload(V2SchemeVerifier.APK_SIGNATURE_SCHEME_V2_BLOCK_ID, apkSignatureSchemeV2Block); + apkSigningBlock.addPayload(payload); + } catch (IOException ignore) { + ignore.printStackTrace(); + } finally { + IOUtils.closeQuietly(fChan); + IOUtils.closeQuietly(fIn); + } + + def salt = jsonObject.toString(); + println "********* add ID-value ${salt} to Apk Signing Block."; + byte[] bytes = salt.bytes; + ByteBuffer byteBuffer1 = ByteBuffer.allocate(bytes.length); + byteBuffer1.order(ByteOrder.LITTLE_ENDIAN); + byteBuffer1.put(bytes, 0, bytes.length); + byteBuffer1.flip(); + ApkSigningPayload payload = new ApkSigningPayload(0xff01, byteBuffer1); + apkSigningBlock.addPayload(payload); + + if (!apkSigningBlock.getPayloads().isEmpty() && apkSigningBlockOffset != 0 && centralDirStartOffset != 0) { + RandomAccessFile randomAccessFile = null; + try { + randomAccessFile = new RandomAccessFile(apkFile, "rw"); + + randomAccessFile.seek(centralDirStartOffset); + // 读取CentralDir + byte[] centralDirBytes = new byte[randomAccessFile.getChannel().size() - centralDirStartOffset]; + randomAccessFile.read(centralDirBytes); + + randomAccessFile.setLength(apkSigningBlockOffset); + randomAccessFile.seek(apkSigningBlockOffset); + + long length = apkSigningBlock.writeApkSigningBlock(randomAccessFile); + + // 存储CentralDir + randomAccessFile.write(centralDirBytes); + + // 更新CentralDir Offset + + // End of central directory record (EOCD) + // Offset Bytes Description[23] + // 0 4 End of central directory signature = 0x06054b50 + // 4 2 Number of this disk + // 6 2 Disk where central directory starts + // 8 2 Number of central directory records on this disk + // 10 2 Total number of central directory records + // 12 4 Size of central directory (bytes) + // 16 4 Offset of start of central directory, relative to start of archive + // 20 2 Comment length (n) + // 22 n Comment + + randomAccessFile.seek(randomAccessFile.getChannel().size() - 6); + ByteBuffer temp = ByteBuffer.allocate(Integer.BYTES); + temp.order(ByteOrder.LITTLE_ENDIAN); + temp.putInt((int) (centralDirStartOffset + length + 8 - (centralDirStartOffset - apkSigningBlockOffset))); + temp.flip(); + randomAccessFile.write(temp.array()); + + } catch (IOException ignore) { + ignore.printStackTrace(); + } finally { + IOUtils.closeQuietly(randomAccessFile); + } + } + } +} diff --git a/plugin/src/main/groovy/com/meituan/android/walle/GradlePlugin.groovy b/plugin/src/main/groovy/com/meituan/android/walle/GradlePlugin.groovy new file mode 100644 index 0000000..879d028 --- /dev/null +++ b/plugin/src/main/groovy/com/meituan/android/walle/GradlePlugin.groovy @@ -0,0 +1,124 @@ +package com.meituan.android.walle + +import com.android.build.gradle.api.BaseVariant +import com.android.builder.Version +import com.android.builder.model.SigningConfig +import org.gradle.api.GradleException +import org.gradle.api.Project +import org.gradle.api.ProjectConfigurationException + +class GradlePlugin implements org.gradle.api.Plugin { + + public static final String sPluginExtensionName = "walle"; + + @Override + void apply(Project project) { + if (!project.plugins.hasPlugin("com.android.application")) { + throw new ProjectConfigurationException("Plugin requires the 'com.android.application' plugin to be configured.", null); + } + + if (versionCompare(Version.getAndroidGradlePluginVersion(), "2.2.0") < 0) { + throw new ProjectConfigurationException("Plugin requires the 'com.android.tools.build:gradle' version 2.2.0 or above to be configured.", null); + } + + applyExtension(project); + + applyTask(project); + } + + void applyTask(Project project) { + project.afterEvaluate { + project.android.applicationVariants.all { BaseVariant variant -> + def variantName = variant.name.capitalize(); + + if (!isV2SignatureSchemeEnabled(variant)) { + throw new ProjectConfigurationException("Plugin requires 'APK Signature Scheme v2 Enabled' for ${variant.name}.", null); + } + + ChannelMaker channelMaker = project.tasks.create("assemble${variantName}V2SignatureSchemeChannel", ChannelMaker); + def File apkFile = variant.outputs[0].outputFile + channelMaker.project = project; + channelMaker.variant = variant; + channelMaker.apkFile = apkFile; + channelMaker.setup(); + + variant.assemble.finalizedBy channelMaker; + } + } + } + + SigningConfig getSigningConfig(BaseVariant variant) { + return variant.buildType.signingConfig == null ? variant.mergedFlavor.signingConfig : variant.buildType.signingConfig; + } + + boolean isV2SignatureSchemeEnabled(BaseVariant variant) throws GradleException { + def signingConfig = getSigningConfig(variant); + if (signingConfig == null) { + return false; + } + + // check whether APK Signature Scheme v2 is enabled. + if (signingConfig.hasProperty("v2SigningEnabled") && + signingConfig.v2SigningEnabled == true) { + return true; + } + + return false; + } + + /** + * Compares two version strings. + * + * Use this instead of String.compareTo() for a non-lexicographical + * comparison that works for version strings. e.g. "1.10".compareTo("1.6"). + * + * @note It does not work if "1.10" is supposed to be equal to "1.10.0". + * + * @param str1 a string of ordinal numbers separated by decimal points. + * @param str2 a string of ordinal numbers separated by decimal points. + * @return The result is a negative integer if str1 is _numerically_ less than str2. + * The result is a positive integer if str1 is _numerically_ greater than str2. + * The result is zero if the strings are _numerically_ equal. + */ + private static int versionCompare(String str1, String str2) { + String[] vals1 = str1.split("-")[0].split("\\."); + String[] vals2 = str2.split("-")[0].split("\\."); + int i = 0; + // set index to first non-equal ordinal or length of shortest version string + while (i < vals1.length && i < vals2.length && vals1[i].equals(vals2[i])) { + i++; + } + + // compare first non-equal ordinal number + if (i < vals1.length && i < vals2.length) { + int diff = Integer.valueOf(vals1[i]).compareTo(Integer.valueOf(vals2[i])); + return Integer.signum(diff); + } + + // the strings are equal or one string is a substring of the other + // e.g. "1.2.3" = "1.2.3" or "1.2.3" < "1.2.3.4" + else { + return Integer.signum(vals1.length - vals2.length); + } + } + + void applyExtension(Project project) { + project.extensions.create(sPluginExtensionName, Extension); + } + + public static class Extension { + + Extension() { + } + + public static Extension getConfig(Project project) { + Extension config = + project.getExtensions().findByType(Extension.class); + if (config == null) { + config = new Extension(); + } + return config; + } + + } +} diff --git a/plugin/src/main/java/com/android/apksigner/core/ApkSignerEngine.java b/plugin/src/main/java/com/android/apksigner/core/ApkSignerEngine.java new file mode 100644 index 0000000..a0c77f9 --- /dev/null +++ b/plugin/src/main/java/com/android/apksigner/core/ApkSignerEngine.java @@ -0,0 +1,407 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksigner.core; + +import java.io.Closeable; +import java.io.IOException; +import java.security.InvalidKeyException; +import java.security.SignatureException; +import java.util.List; + +import com.android.apksigner.core.util.DataSink; +import com.android.apksigner.core.util.DataSource; + +/** + * APK signing logic which is independent of how input and output APKs are stored, parsed, and + * generated. + * + *

Operating Model

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

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

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

    + *
  1. JAR entries to be signed are output,
  2. + *
  3. JAR archive is signed using JAR signing, thus adding the so-called v1 signature to the + * output,
  4. + *
  5. JAR archive is signed using APK Signature Scheme v2, thus adding the so-called v2 signature + * to the output.
  6. + *
+ * + *

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

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

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

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

Incremental Operation

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

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

Output-only Operation

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

When an input entry is updated/changed, it's OK to not invoke + * {@link #inputJarEntryRemoved(String)} before invoking this method. + * + * @return instructions about how to proceed with this entry + * + * @throws IllegalStateException if this engine is closed + */ + InputJarEntryInstructions inputJarEntry(String entryName) throws IllegalStateException; + + /** + * Indicates to this engine that the specified JAR entry was output. + * + *

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

When an already output entry is updated/changed, it's OK to not invoke + * {@link #outputJarEntryRemoved(String)} before invoking this method. + * + * @return request to inspect the entry or {@code null} if the engine does not need to inspect + * the entry. The request must be fulfilled before {@link #outputJarEntries()} is + * invoked. + * + * @throws IllegalStateException if this engine is closed + */ + InspectJarEntryRequest outputJarEntry(String entryName) throws IllegalStateException; + + /** + * Indicates to this engine that the specified JAR entry was removed from the input. It's safe + * to invoke this for entries for which {@link #inputJarEntry(String)} hasn't been invoked. + * + * @return output policy of this JAR entry. The policy indicates how this input entry affects + * the output APK. The client of this engine should use this information to determine + * how the removal of this input APK's JAR entry affects the output APK. + * + * @throws IllegalStateException if this engine is closed + */ + InputJarEntryInstructions.OutputPolicy inputJarEntryRemoved(String entryName) + throws IllegalStateException; + + /** + * Indicates to this engine that the specified JAR entry was removed from the output. It's safe + * to invoke this for entries for which {@link #outputJarEntry(String)} hasn't been invoked. + * + * @throws IllegalStateException if this engine is closed + */ + void outputJarEntryRemoved(String entryName) throws IllegalStateException; + + /** + * Indicates to this engine that all JAR entries have been output. + * + * + * @return request to add JAR signature to the output or {@code null} if there is no need to add + * a JAR signature. The request will contain additional JAR entries to be output. The + * request must be fulfilled before + * {@link #outputZipSections(DataSource, DataSource, DataSource)} is invoked. + * + * @throws InvalidKeyException if a signature could not be generated because a signing key is + * not suitable for generating the signature + * @throws SignatureException if an error occurred while generating the JAR signature + * @throws IllegalStateException if there are unfulfilled requests, such as to inspect some JAR + * entries, or if the engine is closed + */ + OutputJarSignatureRequest outputJarEntries() throws InvalidKeyException, SignatureException; + + /** + * Indicates to this engine that the ZIP sections comprising the output APK have been output. + * + *

The provided data sources are guaranteed to not be used by the engine after this method + * terminates. + * + * @param zipEntries the section of ZIP archive containing Local File Header records and data of + * the ZIP entries. In a well-formed archive, this section starts at the start of the + * archive and extends all the way to the ZIP Central Directory. + * @param zipCentralDirectory ZIP Central Directory section + * @param zipEocd ZIP End of Central Directory (EoCD) record + * + * @return request to add an APK Signing Block to the output or {@code null} if the output must + * not contain an APK Signing Block. The request must be fulfilled before + * {@link #outputDone()} is invoked. + * + * @throws IOException if an I/O error occurs while reading the provided ZIP sections + * @throws InvalidKeyException if a signature could not be generated because a signing key is + * not suitable for generating the signature + * @throws SignatureException if an error occurred while generating the APK's signature + * @throws IllegalStateException if there are unfulfilled requests, such as to inspect some JAR + * entries or to output JAR signature, or if the engine is closed + */ + OutputApkSigningBlockRequest outputZipSections( + DataSource zipEntries, + DataSource zipCentralDirectory, + DataSource zipEocd) throws IOException, InvalidKeyException, SignatureException; + + /** + * Indicates to this engine that the signed APK was output. + * + *

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

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

The instructions indicate whether to output the entry (see {@link #getOutputPolicy()}) and + * may contain a request to inspect the entry (see {@link #getInspectJarEntryRequest()}), in + * which case the request must be fulfilled before {@link ApkSignerEngine#outputJarEntries()} is + * invoked. + */ + public static class InputJarEntryInstructions { + private final OutputPolicy mOutputPolicy; + private final InspectJarEntryRequest mInspectJarEntryRequest; + + /** + * Constructs a new {@code InputJarEntryInstructions} instance with the provided entry + * output policy and without a request to inspect the entry. + */ + public InputJarEntryInstructions(OutputPolicy outputPolicy) { + this(outputPolicy, null); + } + + /** + * Constructs a new {@code InputJarEntryInstructions} instance with the provided entry + * output mode and with the provided request to inspect the entry. + * + * @param inspectJarEntryRequest request to inspect the entry or {@code null} if there's no + * need to inspect the entry. + */ + public InputJarEntryInstructions( + OutputPolicy outputPolicy, + InspectJarEntryRequest inspectJarEntryRequest) { + mOutputPolicy = outputPolicy; + mInspectJarEntryRequest = inspectJarEntryRequest; + } + + /** + * Returns the output policy for this entry. + */ + public OutputPolicy getOutputPolicy() { + return mOutputPolicy; + } + + /** + * Returns the request to inspect the JAR entry or {@code null} if there is no need to + * inspect the entry. + */ + public InspectJarEntryRequest getInspectJarEntryRequest() { + return mInspectJarEntryRequest; + } + + /** + * Output policy for an input APK's JAR entry. + */ + public static enum OutputPolicy { + /** Entry must not be output. */ + SKIP, + + /** Entry should be output. */ + OUTPUT, + + /** Entry will be output by the engine. The client can thus ignore this input entry. */ + OUTPUT_BY_ENGINE, + } + } + + /** + * Request to inspect the specified JAR entry. + * + *

The entry's uncompressed data must be provided to the data sink returned by + * {@link #getDataSink()}. Once the entry's data has been provided to the sink, {@link #done()} + * must be invoked. + */ + interface InspectJarEntryRequest { + + /** + * Returns the data sink into which the entry's uncompressed data should be sent. + */ + DataSink getDataSink(); + + /** + * Indicates that entry's data has been provided in full. + */ + void done(); + + /** + * Returns the name of the JAR entry. + */ + String getEntryName(); + } + + /** + * Request to add JAR signature (aka v1 signature) to the output APK. + * + *

Entries listed in {@link #getAdditionalJarEntries()} must be added to the output APK after + * which {@link #done()} must be invoked. + */ + interface OutputJarSignatureRequest { + + /** + * Returns JAR entries that must be added to the output APK. + */ + List getAdditionalJarEntries(); + + /** + * Indicates that the JAR entries contained in this request were added to the output APK. + */ + void done(); + + /** + * JAR entry. + */ + public static class JarEntry { + private final String mName; + private final byte[] mData; + + /** + * Constructs a new {@code JarEntry} with the provided name and data. + * + * @param data uncompressed data of the entry. Changes to this array will not be + * reflected in {@link #getData()}. + */ + public JarEntry(String name, byte[] data) { + mName = name; + mData = data.clone(); + } + + /** + * Returns the name of this ZIP entry. + */ + public String getName() { + return mName; + } + + /** + * Returns the uncompressed data of this JAR entry. + */ + public byte[] getData() { + return mData.clone(); + } + } + } + + /** + * Request to add the specified APK Signing Block to the output APK. APK Signature Scheme v2 + * signature(s) of the APK are contained in this block. + * + *

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

If the output contains an APK Signing Block, that block must be replaced by the block + * contained in this request. + */ + interface OutputApkSigningBlockRequest { + + /** + * Returns the APK Signing Block. + */ + byte[] getApkSigningBlock(); + + /** + * Indicates that the APK Signing Block was output as requested. + */ + void done(); + } +} diff --git a/plugin/src/main/java/com/android/apksigner/core/ApkVerifier.java b/plugin/src/main/java/com/android/apksigner/core/ApkVerifier.java new file mode 100644 index 0000000..931c7b2 --- /dev/null +++ b/plugin/src/main/java/com/android/apksigner/core/ApkVerifier.java @@ -0,0 +1,458 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksigner.core; + +import com.android.apksigner.core.apk.ApkUtils; +import com.android.apksigner.core.internal.apk.v2.ContentDigestAlgorithm; +import com.android.apksigner.core.internal.apk.v2.SignatureAlgorithm; +import com.android.apksigner.core.internal.apk.v2.V2SchemeVerifier; +import com.android.apksigner.core.util.DataSource; +import com.android.apksigner.core.zip.ZipFormatException; + +import java.io.IOException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.List; + +/** + * APK signature verifier which mimics the behavior of the Android platform. + * + *

The verifier is designed to closely mimic the behavior of Android platforms. This is to enable + * the verifier to be used for checking whether an APK's signatures will verify on Android. + */ +public class ApkVerifier { + + /** + * Verifies the APK's signatures and returns the result of verification. The APK can be + * considered verified iff the result's {@link Result#isVerified()} returns {@code true}. + * The verification result also includes errors, warnings, and information about signers. + * + * @param apk APK file contents + * @param minSdkVersion API Level of the oldest Android platform on which the APK's signatures + * may need to be verified + * + * @throws IOException if an I/O error is encountered while reading the APK + * @throws ZipFormatException if the APK is malformed at ZIP format level + */ + public Result verify(DataSource apk, int minSdkVersion) throws IOException, ZipFormatException { + ApkUtils.ZipSections zipSections = ApkUtils.findZipSections(apk); + + // Attempt to verify the APK using APK Signature Scheme v2 + Result result = new Result(); + try { + V2SchemeVerifier.Result v2Result = V2SchemeVerifier.verify(apk, zipSections); + result.mergeFrom(v2Result); + } catch (V2SchemeVerifier.SignatureNotFoundException ignored) {} + if (result.containsErrors()) { + return result; + } + + // TODO: Verify JAR signature if necessary + if (!result.isVerifiedUsingV2Scheme()) { + return result; + } + + // Verified + result.setVerified(); + for (Result.V2SchemeSignerInfo signerInfo : result.getV2SchemeSigners()) { + result.addSignerCertificate(signerInfo.getCertificate()); + } + + return result; + } + + /** + * Result of verifying an APKs signatures. The APK can be considered verified iff + * {@link #isVerified()} returns {@code true}. + */ + public static class Result { + private final List mErrors = new ArrayList<>(); + private final List mWarnings = new ArrayList<>(); + private final List mSignerCerts = new ArrayList<>(); + private final List mV2SchemeSigners = new ArrayList<>(); + + private boolean mVerified; + private boolean mVerifiedUsingV2Scheme; + + /** + * Returns {@code true} if the APK's signatures verified. + */ + public boolean isVerified() { + return mVerified; + } + + private void setVerified() { + mVerified = true; + } + + /** + * Returns {@code true} if the APK's APK Signature Scheme v2 signatures verified. + */ + public boolean isVerifiedUsingV2Scheme() { + return mVerifiedUsingV2Scheme; + } + + /** + * Returns the verified signers' certificates, one per signer. + */ + public List getSignerCertificates() { + return mSignerCerts; + } + + private void addSignerCertificate(X509Certificate cert) { + mSignerCerts.add(cert); + } + + /** + * Returns information about APK Signature Scheme v2 signers associated with the APK's + * signature. + */ + public List getV2SchemeSigners() { + return mV2SchemeSigners; + } + + /** + * Returns errors encountered while verifying the APK's signatures. + */ + public List getErrors() { + return mErrors; + } + + /** + * Returns warnings encountered while verifying the APK's signatures. + */ + public List getWarnings() { + return mWarnings; + } + + private void mergeFrom(V2SchemeVerifier.Result source) { + mVerifiedUsingV2Scheme = source.verified; + mErrors.addAll(source.getErrors()); + mWarnings.addAll(source.getWarnings()); + for (V2SchemeVerifier.Result.SignerInfo signer : source.signers) { + mV2SchemeSigners.add(new V2SchemeSignerInfo(signer)); + } + } + + /** + * Returns {@code true} if an error was encountered while verifying the APK. Any error + * prevents the APK from being considered verified. + */ + public boolean containsErrors() { + if (!mErrors.isEmpty()) { + return true; + } + if (!mV2SchemeSigners.isEmpty()) { + for (V2SchemeSignerInfo signer : mV2SchemeSigners) { + if (signer.containsErrors()) { + return true; + } + } + } + + return false; + } + + /** + * Information about an APK Signature Scheme v2 signer associated with the APK's signature. + */ + public static class V2SchemeSignerInfo { + private final int mIndex; + private final List mCerts; + + private final List mErrors; + private final List mWarnings; + + private V2SchemeSignerInfo(V2SchemeVerifier.Result.SignerInfo result) { + mIndex = result.index; + mCerts = result.certs; + mErrors = result.getErrors(); + mWarnings = result.getWarnings(); + } + + /** + * Returns this signer's {@code 0}-based index in the list of signers contained in the + * APK's APK Signature Scheme v2 signature. + */ + public int getIndex() { + return mIndex; + } + + /** + * Returns this signer's signing certificate or {@code null} if not available. The + * certificate is guaranteed to be available if no errors were encountered during + * verification (see {@link #containsErrors()}. + * + *

This certificate contains the signer's public key. + */ + public X509Certificate getCertificate() { + return mCerts.isEmpty() ? null : mCerts.get(0); + } + + /** + * Returns this signer's certificates. The first certificate is for the signer's public + * key. An empty list may be returned if an error was encountered during verification + * (see {@link #containsErrors()}). + */ + public List getCertificates() { + return mCerts; + } + + public boolean containsErrors() { + return !mErrors.isEmpty(); + } + + public List getErrors() { + return mErrors; + } + + public List getWarnings() { + return mWarnings; + } + } + } + + /** + * Error or warning encountered while verifying an APK's signatures. + */ + public static enum Issue { + + /** + * Failed to parse the list of signers contained in the APK Signature Scheme v2 signature. + */ + V2_SIG_MALFORMED_SIGNERS("Malformed list of signers"), + + /** + * Failed to parse this signer's signer block contained in the APK Signature Scheme v2 + * signature. + */ + V2_SIG_MALFORMED_SIGNER("Malformed signer block"), + + /** + * Public key embedded in the APK Signature Scheme v2 signature of this signer could not be + * parsed. + * + *

    + *
  • Parameter 1: error details ({@code Throwable})
  • + *
+ */ + V2_SIG_MALFORMED_PUBLIC_KEY("Malformed public key: %1$s"), + + /** + * This APK Signature Scheme v2 signer's certificate could not be parsed. + * + *
    + *
  • Parameter 1: index ({@code 0}-based) of the certificate in the signer's list of + * certificates ({@code Integer})
  • + *
  • Parameter 2: sequence number ({@code 1}-based) of the certificate in the signer's + * list of certificates ({@code Integer})
  • + *
  • Parameter 3: error details ({@code Throwable})
  • + *
+ */ + V2_SIG_MALFORMED_CERTIFICATE("Malformed certificate #%2$d: %3$s"), + + /** + * Failed to parse this signer's signature record contained in the APK Signature Scheme v2 + * signature. + * + *
    + *
  • Parameter 1: record number (first record is {@code 1}) ({@code Integer})
  • + *
+ */ + V2_SIG_MALFORMED_SIGNATURE("Malformed APK Signature Scheme v2 signature record #%1$d"), + + /** + * Failed to parse this signer's digest record contained in the APK Signature Scheme v2 + * signature. + * + *
    + *
  • Parameter 1: record number (first record is {@code 1}) ({@code Integer})
  • + *
+ */ + V2_SIG_MALFORMED_DIGEST("Malformed APK Signature Scheme v2 digest record #%1$d"), + + /** + * This APK Signature Scheme v2 signer contains a malformed additional attribute. + * + *
    + *
  • Parameter 1: attribute number (first attribute is {@code 1}) {@code Integer})
  • + *
+ */ + V2_SIG_MALFORMED_ADDITIONAL_ATTRIBUTE("Malformed additional attribute #%1$d"), + + /** + * APK Signature Scheme v2 signature contains no signers. + */ + V2_SIG_NO_SIGNERS("No signers in APK Signature Scheme v2 signature"), + + /** + * This APK Signature Scheme v2 signer contains a signature produced using an unknown + * algorithm. + * + *
    + *
  • Parameter 1: algorithm ID ({@code Integer})
  • + *
+ */ + V2_SIG_UNKNOWN_SIG_ALGORITHM("Unknown signature algorithm: %1$#x"), + + /** + * This APK Signature Scheme v2 signer contains an unknown additional attribute. + * + *
    + *
  • Parameter 1: attribute ID ({@code Integer})
  • + *
+ */ + V2_SIG_UNKNOWN_ADDITIONAL_ATTRIBUTE("Unknown additional attribute: ID %1$#x"), + + /** + * An exception was encountered while verifying APK Signature Scheme v2 signature of this + * signer. + * + *
    + *
  • Parameter 1: signature algorithm ({@link SignatureAlgorithm})
  • + *
  • Parameter 2: exception ({@code Throwable})
  • + *
+ */ + V2_SIG_VERIFY_EXCEPTION("Failed to verify %1$s signature: %2$s"), + + /** + * APK Signature Scheme v2 signature over this signer's signed-data block did not verify. + * + *
    + *
  • Parameter 1: signature algorithm ({@link SignatureAlgorithm})
  • + *
+ */ + V2_SIG_DID_NOT_VERIFY("%1$s signature over signed-data did not verify"), + + /** + * This APK Signature Scheme v2 signer offers no signatures. + */ + V2_SIG_NO_SIGNATURES("No signatures"), + + /** + * This APK Signature Scheme v2 signer offers signatures but none of them are supported. + */ + V2_SIG_NO_SUPPORTED_SIGNATURES("No supported signatures"), + + /** + * This APK Signature Scheme v2 signer offers no certificates. + */ + V2_SIG_NO_CERTIFICATES("No certificates"), + + /** + * This APK Signature Scheme v2 signer's public key listed in the signer's certificate does + * not match the public key listed in the signatures record. + * + *
    + *
  • Parameter 1: hex-encoded public key from certificate ({@code String})
  • + *
  • Parameter 2: hex-encoded public key from signatures record ({@code String})
  • + *
+ */ + V2_SIG_PUBLIC_KEY_MISMATCH_BETWEEN_CERTIFICATE_AND_SIGNATURES_RECORD( + "Public key mismatch between certificate and signature record: <%1$s> vs <%2$s>"), + + /** + * This APK Signature Scheme v2 signer's signature algorithms listed in the signatures + * record do not match the signature algorithms listed in the signatures record. + * + *
    + *
  • Parameter 1: signature algorithms from signatures record ({@code List})
  • + *
  • Parameter 2: signature algorithms from digests record ({@code List})
  • + *
+ */ + V2_SIG_SIG_ALG_MISMATCH_BETWEEN_SIGNATURES_AND_DIGESTS_RECORDS( + "Signature algorithms mismatch between signatures and digests records" + + ": %1$s vs %2$s"), + + /** + * The APK's digest does not match the digest contained in the APK Signature Scheme v2 + * signature. + * + *
    + *
  • Parameter 1: content digest algorithm ({@link ContentDigestAlgorithm})
  • + *
  • Parameter 2: hex-encoded expected digest of the APK ({@code String})
  • + *
  • Parameter 3: hex-encoded actual digest of the APK ({@code String})
  • + *
+ */ + V2_SIG_APK_DIGEST_DID_NOT_VERIFY( + "APK integrity check failed. %1$s digest mismatch." + + " Expected: <%2$s>, actual: <%3$s>"), + + /** + * APK Signing Block contains an unknown entry. + * + *
    + *
  • Parameter 1: entry ID ({@code Integer})
  • + *
+ */ + APK_SIG_BLOCK_UNKNOWN_ENTRY_ID("APK Signing Block contains unknown entry: ID %1$#x"); + + private final String mFormat; + + private Issue(String format) { + mFormat = format; + } + + /** + * Returns the format string suitable for combining the parameters of this issue into a + * readable string. See {@link java.util.Formatter} for format. + */ + private String getFormat() { + return mFormat; + } + } + + /** + * {@link Issue} with associated parameters. {@link #toString()} produces a readable formatted + * form. + */ + public static class IssueWithParams { + private final Issue mIssue; + private final Object[] mParams; + + /** + * Constructs a new {@code IssueWithParams} of the specified type and with provided + * parameters. + */ + public IssueWithParams(Issue issue, Object[] params) { + mIssue = issue; + mParams = params; + } + + /** + * Returns the type of this issue. + */ + public Issue getIssue() { + return mIssue; + } + + /** + * Returns the parameters of this issue. + */ + public Object[] getParams() { + return mParams.clone(); + } + + /** + * Returns a readable form of this issue. + */ + @Override + public String toString() { + return String.format(mIssue.getFormat(), mParams); + } + } +} diff --git a/plugin/src/main/java/com/android/apksigner/core/DefaultApkSignerEngine.java b/plugin/src/main/java/com/android/apksigner/core/DefaultApkSignerEngine.java new file mode 100644 index 0000000..612f4fd --- /dev/null +++ b/plugin/src/main/java/com/android/apksigner/core/DefaultApkSignerEngine.java @@ -0,0 +1,893 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksigner.core; + +import com.android.apksigner.core.internal.apk.v1.DigestAlgorithm; +import com.android.apksigner.core.internal.apk.v1.V1SchemeSigner; +import com.android.apksigner.core.internal.apk.v2.V2SchemeSigner; +import com.android.apksigner.core.internal.util.ByteArrayOutputStreamSink; +import com.android.apksigner.core.internal.util.MessageDigestSink; +import com.android.apksigner.core.internal.util.Pair; +import com.android.apksigner.core.util.DataSink; +import com.android.apksigner.core.util.DataSource; + +import java.io.IOException; +import java.security.InvalidKeyException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.SignatureException; +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Default implementation of {@link ApkSignerEngine}. + * + *

Use {@link Builder} to obtain instances of this engine. + */ +public class DefaultApkSignerEngine implements ApkSignerEngine { + + // IMPLEMENTATION NOTE: This engine generates a signed APK as follows: + // 1. The engine asks its client to output input JAR entries which are not part of JAR + // signature. + // 2. If JAR signing (v1 signing) is enabled, the engine inspects the output JAR entries to + // compute their digests, to be placed into output META-INF/MANIFEST.MF. It also inspects + // the contents of input and output META-INF/MANIFEST.MF to borrow the main section of the + // file. It does not care about individual (i.e., JAR entry-specific) sections. It then + // emits the v1 signature (a set of JAR entries) and asks the client to output them. + // 3. If APK Signature Scheme v2 (v2 signing) is enabled, the engine emits an APK Signing Block + // from outputZipSections() and asks its client to insert this block into the output. + + private final boolean mV1SigningEnabled; + private final boolean mV2SigningEnabled; + private final boolean mOtherSignersSignaturesPreserved; + private final List mV1SignerConfigs; + private final DigestAlgorithm mV1ContentDigestAlgorithm; + private final List mV2SignerConfigs; + + private boolean mClosed; + + private boolean mV1SignaturePending; + + /** + * Names of JAR entries which this engine is expected to output as part of v1 signing. + */ + private final Set mSignatureExpectedOutputJarEntryNames; + + /** Requests for digests of output JAR entries. */ + private final Map mOutputJarEntryDigestRequests = + new HashMap<>(); + + /** Digests of output JAR entries. */ + private final Map mOutputJarEntryDigests = new HashMap<>(); + + /** Data of JAR entries emitted by this engine as v1 signature. */ + private final Map mEmittedSignatureJarEntryData = new HashMap<>(); + + /** Requests for data of output JAR entries which comprise the v1 signature. */ + private final Map mOutputSignatureJarEntryDataRequests = + new HashMap<>(); + /** + * Request to obtain the data of MANIFEST.MF or {@code null} if the request hasn't been issued. + */ + private GetJarEntryDataRequest mInputJarManifestEntryDataRequest; + + /** + * Request to output the emitted v1 signature or {@code null} if the request hasn't been issued. + */ + private OutputJarSignatureRequestImpl mAddV1SignatureRequest; + + private boolean mV2SignaturePending; + + /** + * Request to output the emitted v2 signature or {@code null} if the request hasn't been issued. + */ + private OutputApkSigningBlockRequestImpl mAddV2SignatureRequest; + + private DefaultApkSignerEngine( + List signerConfigs, + int minSdkVersion, + boolean v1SigningEnabled, + boolean v2SigningEnabled, + boolean otherSignersSignaturesPreserved) throws InvalidKeyException { + if (signerConfigs.isEmpty()) { + throw new IllegalArgumentException("At least one signer config must be provided"); + } + if (otherSignersSignaturesPreserved) { + throw new UnsupportedOperationException( + "Preserving other signer's signatures is not yet implemented"); + } + + mV1SigningEnabled = v1SigningEnabled; + mV2SigningEnabled = v2SigningEnabled; + mOtherSignersSignaturesPreserved = otherSignersSignaturesPreserved; + mV1SignerConfigs = + (v1SigningEnabled) + ? new ArrayList<>(signerConfigs.size()) : Collections.emptyList(); + mV2SignerConfigs = + (v2SigningEnabled) + ? new ArrayList<>(signerConfigs.size()) : Collections.emptyList(); + mV1ContentDigestAlgorithm = + (v1SigningEnabled) + ? V1SchemeSigner.getSuggestedContentDigestAlgorithm(minSdkVersion) : null; + for (SignerConfig signerConfig : signerConfigs) { + List certificates = signerConfig.getCertificates(); + PublicKey publicKey = certificates.get(0).getPublicKey(); + + if (v1SigningEnabled) { + DigestAlgorithm v1SignatureDigestAlgorithm = + V1SchemeSigner.getSuggestedSignatureDigestAlgorithm( + publicKey, minSdkVersion); + V1SchemeSigner.SignerConfig v1SignerConfig = new V1SchemeSigner.SignerConfig(); + v1SignerConfig.name = signerConfig.getName(); + v1SignerConfig.privateKey = signerConfig.getPrivateKey(); + v1SignerConfig.certificates = certificates; + v1SignerConfig.contentDigestAlgorithm = mV1ContentDigestAlgorithm; + v1SignerConfig.signatureDigestAlgorithm = v1SignatureDigestAlgorithm; + mV1SignerConfigs.add(v1SignerConfig); + } + + if (v2SigningEnabled) { + V2SchemeSigner.SignerConfig v2SignerConfig = new V2SchemeSigner.SignerConfig(); + v2SignerConfig.privateKey = signerConfig.getPrivateKey(); + v2SignerConfig.certificates = certificates; + v2SignerConfig.signatureAlgorithms = + V2SchemeSigner.getSuggestedSignatureAlgorithms(publicKey, minSdkVersion); + mV2SignerConfigs.add(v2SignerConfig); + } + } + mSignatureExpectedOutputJarEntryNames = + (v1SigningEnabled) + ? V1SchemeSigner.getOutputEntryNames(mV1SignerConfigs) + : Collections.emptySet(); + } + + @Override + public void inputApkSigningBlock(DataSource apkSigningBlock) { + checkNotClosed(); + + if ((apkSigningBlock == null) || (apkSigningBlock.size() == 0)) { + return; + } + + if (mOtherSignersSignaturesPreserved) { + // TODO: Preserve blocks other than APK Signature Scheme v2 blocks of signers configured + // in this engine. + return; + } + // TODO: Preserve blocks other than APK Signature Scheme v2 blocks. + } + + @Override + public InputJarEntryInstructions inputJarEntry(String entryName) { + checkNotClosed(); + + InputJarEntryInstructions.OutputPolicy outputPolicy = + getInputJarEntryOutputPolicy(entryName); + switch (outputPolicy) { + case SKIP: + return new InputJarEntryInstructions(InputJarEntryInstructions.OutputPolicy.SKIP); + case OUTPUT: + return new InputJarEntryInstructions(InputJarEntryInstructions.OutputPolicy.OUTPUT); + case OUTPUT_BY_ENGINE: + if (V1SchemeSigner.MANIFEST_ENTRY_NAME.equals(entryName)) { + // We copy the main section of the JAR manifest from input to output. Thus, this + // invalidates v1 signature and we need to see the entry's data. + mInputJarManifestEntryDataRequest = new GetJarEntryDataRequest(entryName); + return new InputJarEntryInstructions( + InputJarEntryInstructions.OutputPolicy.OUTPUT_BY_ENGINE, + mInputJarManifestEntryDataRequest); + } + return new InputJarEntryInstructions( + InputJarEntryInstructions.OutputPolicy.OUTPUT_BY_ENGINE); + default: + throw new RuntimeException("Unsupported output policy: " + outputPolicy); + } + } + + @Override + public InspectJarEntryRequest outputJarEntry(String entryName) { + checkNotClosed(); + invalidateV2Signature(); + if (!mV1SigningEnabled) { + // No need to inspect JAR entries when v1 signing is not enabled. + return null; + } + // v1 signing is enabled + + if (V1SchemeSigner.isJarEntryDigestNeededInManifest(entryName)) { + // This entry is covered by v1 signature. We thus need to inspect the entry's data to + // compute its digest(s) for v1 signature. + + // TODO: Handle the case where other signer's v1 signatures are present and need to be + // preserved. In that scenario we can't modify MANIFEST.MF and add/remove JAR entries + // covered by v1 signature. + invalidateV1Signature(); + GetJarEntryDataDigestRequest dataDigestRequest = + new GetJarEntryDataDigestRequest( + entryName, + V1SchemeSigner.getJcaMessageDigestAlgorithm(mV1ContentDigestAlgorithm)); + mOutputJarEntryDigestRequests.put(entryName, dataDigestRequest); + mOutputJarEntryDigests.remove(entryName); + return dataDigestRequest; + } + + if (mSignatureExpectedOutputJarEntryNames.contains(entryName)) { + // This entry is part of v1 signature generated by this engine. We need to check whether + // the entry's data is as output by the engine. + invalidateV1Signature(); + GetJarEntryDataRequest dataRequest; + if (V1SchemeSigner.MANIFEST_ENTRY_NAME.equals(entryName)) { + dataRequest = new GetJarEntryDataRequest(entryName); + mInputJarManifestEntryDataRequest = dataRequest; + } else { + // If this entry is part of v1 signature which has been emitted by this engine, + // check whether the output entry's data matches what the engine emitted. + dataRequest = + (mEmittedSignatureJarEntryData.containsKey(entryName)) + ? new GetJarEntryDataRequest(entryName) : null; + } + + if (dataRequest != null) { + mOutputSignatureJarEntryDataRequests.put(entryName, dataRequest); + } + return dataRequest; + } + + // This entry is not covered by v1 signature and isn't part of v1 signature. + return null; + } + + @Override + public InputJarEntryInstructions.OutputPolicy inputJarEntryRemoved(String entryName) { + checkNotClosed(); + return getInputJarEntryOutputPolicy(entryName); + } + + @Override + public void outputJarEntryRemoved(String entryName) { + checkNotClosed(); + invalidateV2Signature(); + if (!mV1SigningEnabled) { + return; + } + + if (V1SchemeSigner.isJarEntryDigestNeededInManifest(entryName)) { + // This entry is covered by v1 signature. + invalidateV1Signature(); + mOutputJarEntryDigests.remove(entryName); + mOutputJarEntryDigestRequests.remove(entryName); + mOutputSignatureJarEntryDataRequests.remove(entryName); + return; + } + + if (mSignatureExpectedOutputJarEntryNames.contains(entryName)) { + // This entry is part of the v1 signature generated by this engine. + invalidateV1Signature(); + return; + } + } + + @Override + public OutputJarSignatureRequest outputJarEntries() + throws InvalidKeyException, SignatureException { + checkNotClosed(); + + if (!mV1SignaturePending) { + return null; + } + + if ((mInputJarManifestEntryDataRequest != null) + && (!mInputJarManifestEntryDataRequest.isDone())) { + throw new IllegalStateException( + "Still waiting to inspect input APK's " + + mInputJarManifestEntryDataRequest.getEntryName()); + } + + for (GetJarEntryDataDigestRequest digestRequest + : mOutputJarEntryDigestRequests.values()) { + String entryName = digestRequest.getEntryName(); + if (!digestRequest.isDone()) { + throw new IllegalStateException( + "Still waiting to inspect output APK's " + entryName); + } + mOutputJarEntryDigests.put(entryName, digestRequest.getDigest()); + } + mOutputJarEntryDigestRequests.clear(); + + for (GetJarEntryDataRequest dataRequest : mOutputSignatureJarEntryDataRequests.values()) { + if (!dataRequest.isDone()) { + throw new IllegalStateException( + "Still waiting to inspect output APK's " + dataRequest.getEntryName()); + } + } + + List apkSigningSchemeIds = + (mV2SigningEnabled) ? Collections.singletonList(2) : Collections.emptyList(); + byte[] inputJarManifest = + (mInputJarManifestEntryDataRequest != null) + ? mInputJarManifestEntryDataRequest.getData() : null; + + // Check whether the most recently used signature (if present) is still fine. + List> signatureZipEntries; + if ((mAddV1SignatureRequest == null) || (!mAddV1SignatureRequest.isDone())) { + try { + signatureZipEntries = + V1SchemeSigner.sign( + mV1SignerConfigs, + mV1ContentDigestAlgorithm, + mOutputJarEntryDigests, + apkSigningSchemeIds, + inputJarManifest); + } catch (CertificateEncodingException e) { + throw new SignatureException("Failed to generate v1 signature", e); + } + } else { + V1SchemeSigner.OutputManifestFile newManifest = + V1SchemeSigner.generateManifestFile( + mV1ContentDigestAlgorithm, mOutputJarEntryDigests, inputJarManifest); + byte[] emittedSignatureManifest = + mEmittedSignatureJarEntryData.get(V1SchemeSigner.MANIFEST_ENTRY_NAME); + if (!Arrays.equals(newManifest.contents, emittedSignatureManifest)) { + // Emitted v1 signature is no longer valid. + try { + signatureZipEntries = + V1SchemeSigner.signManifest( + mV1SignerConfigs, + mV1ContentDigestAlgorithm, + apkSigningSchemeIds, + newManifest); + } catch (CertificateEncodingException e) { + throw new SignatureException("Failed to generate v1 signature", e); + } + } else { + // Emitted v1 signature is still valid. Check whether the signature is there in the + // output. + signatureZipEntries = new ArrayList<>(); + for (Map.Entry expectedOutputEntry + : mEmittedSignatureJarEntryData.entrySet()) { + String entryName = expectedOutputEntry.getKey(); + byte[] expectedData = expectedOutputEntry.getValue(); + GetJarEntryDataRequest actualDataRequest = + mOutputSignatureJarEntryDataRequests.get(entryName); + if (actualDataRequest == null) { + // This signature entry hasn't been output. + signatureZipEntries.add(Pair.of(entryName, expectedData)); + continue; + } + byte[] actualData = actualDataRequest.getData(); + if (!Arrays.equals(expectedData, actualData)) { + signatureZipEntries.add(Pair.of(entryName, expectedData)); + } + } + if (signatureZipEntries.isEmpty()) { + // v1 signature in the output is valid + return null; + } + // v1 signature in the output is not valid. + } + } + + if (signatureZipEntries.isEmpty()) { + // v1 signature in the output is valid + mV1SignaturePending = false; + return null; + } + + List sigEntries = + new ArrayList<>(signatureZipEntries.size()); + for (Pair entry : signatureZipEntries) { + String entryName = entry.getFirst(); + byte[] entryData = entry.getSecond(); + sigEntries.add(new OutputJarSignatureRequest.JarEntry(entryName, entryData)); + mEmittedSignatureJarEntryData.put(entryName, entryData); + } + mAddV1SignatureRequest = new OutputJarSignatureRequestImpl(sigEntries); + return mAddV1SignatureRequest; + } + + @Override + public OutputApkSigningBlockRequest outputZipSections( + DataSource zipEntries, + DataSource zipCentralDirectory, + DataSource zipEocd) throws IOException, InvalidKeyException, SignatureException { + checkNotClosed(); + checkV1SigningDoneIfEnabled(); + if (!mV2SigningEnabled) { + return null; + } + invalidateV2Signature(); + + byte[] apkSigningBlock = + V2SchemeSigner.generateApkSigningBlock( + zipEntries, zipCentralDirectory, zipEocd, mV2SignerConfigs); + + mAddV2SignatureRequest = new OutputApkSigningBlockRequestImpl(apkSigningBlock); + return mAddV2SignatureRequest; + } + + @Override + public void outputDone() { + checkNotClosed(); + checkV1SigningDoneIfEnabled(); + checkV2SigningDoneIfEnabled(); + } + + @Override + public void close() { + mClosed = true; + + mAddV1SignatureRequest = null; + mInputJarManifestEntryDataRequest = null; + mOutputJarEntryDigestRequests.clear(); + mOutputJarEntryDigests.clear(); + mEmittedSignatureJarEntryData.clear(); + mOutputSignatureJarEntryDataRequests.clear(); + + mAddV2SignatureRequest = null; + } + + private void invalidateV1Signature() { + if (mV1SigningEnabled) { + mV1SignaturePending = true; + } + invalidateV2Signature(); + } + + private void invalidateV2Signature() { + if (mV2SigningEnabled) { + mV2SignaturePending = true; + mAddV2SignatureRequest = null; + } + } + + private void checkNotClosed() { + if (mClosed) { + throw new IllegalStateException("Engine closed"); + } + } + + private void checkV1SigningDoneIfEnabled() { + if (!mV1SignaturePending) { + return; + } + + if (mAddV1SignatureRequest == null) { + throw new IllegalStateException( + "v1 signature (JAR signature) not yet generated. Skipped outputJarEntries()?"); + } + if (!mAddV1SignatureRequest.isDone()) { + throw new IllegalStateException( + "v1 signature (JAR signature) addition requested by outputJarEntries() hasn't" + + " been fulfilled"); + } + for (Map.Entry expectedOutputEntry + : mEmittedSignatureJarEntryData.entrySet()) { + String entryName = expectedOutputEntry.getKey(); + byte[] expectedData = expectedOutputEntry.getValue(); + GetJarEntryDataRequest actualDataRequest = + mOutputSignatureJarEntryDataRequests.get(entryName); + if (actualDataRequest == null) { + throw new IllegalStateException( + "APK entry " + entryName + " not yet output despite this having been" + + " requested"); + } else if (!actualDataRequest.isDone()) { + throw new IllegalStateException( + "Still waiting to inspect output APK's " + entryName); + } + byte[] actualData = actualDataRequest.getData(); + if (!Arrays.equals(expectedData, actualData)) { + throw new IllegalStateException( + "Output APK entry " + entryName + " data differs from what was requested"); + } + } + mV1SignaturePending = false; + } + + private void checkV2SigningDoneIfEnabled() { + if (!mV2SignaturePending) { + return; + } + if (mAddV2SignatureRequest == null) { + throw new IllegalStateException( + "v2 signature (APK Signature Scheme v2 signature) not yet generated." + + " Skipped outputZipSections()?"); + } + if (!mAddV2SignatureRequest.isDone()) { + throw new IllegalStateException( + "v2 signature (APK Signature Scheme v2 signature) addition requested by" + + " outputZipSections() hasn't been fulfilled yet"); + } + mAddV2SignatureRequest = null; + mV2SignaturePending = false; + } + + /** + * Returns the output policy for the provided input JAR entry. + */ + private InputJarEntryInstructions.OutputPolicy getInputJarEntryOutputPolicy(String entryName) { + if (mSignatureExpectedOutputJarEntryNames.contains(entryName)) { + return InputJarEntryInstructions.OutputPolicy.OUTPUT_BY_ENGINE; + } + if ((mOtherSignersSignaturesPreserved) + || (V1SchemeSigner.isJarEntryDigestNeededInManifest(entryName))) { + return InputJarEntryInstructions.OutputPolicy.OUTPUT; + } + return InputJarEntryInstructions.OutputPolicy.SKIP; + } + + private static class OutputJarSignatureRequestImpl implements OutputJarSignatureRequest { + private final List mAdditionalJarEntries; + private volatile boolean mDone; + + private OutputJarSignatureRequestImpl(List additionalZipEntries) { + mAdditionalJarEntries = + Collections.unmodifiableList(new ArrayList<>(additionalZipEntries)); + } + + @Override + public List getAdditionalJarEntries() { + return mAdditionalJarEntries; + } + + @Override + public void done() { + mDone = true; + } + + private boolean isDone() { + return mDone; + } + } + + private static class OutputApkSigningBlockRequestImpl implements OutputApkSigningBlockRequest { + private final byte[] mApkSigningBlock; + private volatile boolean mDone; + + private OutputApkSigningBlockRequestImpl(byte[] apkSigingBlock) { + mApkSigningBlock = apkSigingBlock.clone(); + } + + @Override + public byte[] getApkSigningBlock() { + return mApkSigningBlock.clone(); + } + + @Override + public void done() { + mDone = true; + } + + private boolean isDone() { + return mDone; + } + } + + /** + * JAR entry inspection request which obtain the entry's uncompressed data. + */ + private static class GetJarEntryDataRequest implements InspectJarEntryRequest { + private final String mEntryName; + private final Object mLock = new Object(); + + private boolean mDone; + private ByteArrayOutputStreamSink mBuf; + + private GetJarEntryDataRequest(String entryName) { + mEntryName = entryName; + } + + @Override + public String getEntryName() { + return mEntryName; + } + + @Override + public DataSink getDataSink() { + synchronized (mLock) { + checkNotDone(); + if (mBuf == null) { + mBuf = new ByteArrayOutputStreamSink(); + } + return mBuf; + } + } + + @Override + public void done() { + synchronized (mLock) { + if (mDone) { + return; + } + mDone = true; + } + } + + private boolean isDone() { + synchronized (mLock) { + return mDone; + } + } + + private void checkNotDone() throws IllegalStateException { + synchronized (mLock) { + if (mDone) { + throw new IllegalStateException("Already done"); + } + } + } + + private byte[] getData() { + synchronized (mLock) { + if (!mDone) { + throw new IllegalStateException("Not yet done"); + } + return (mBuf != null) ? mBuf.getData() : new byte[0]; + } + } + } + + /** + * JAR entry inspection request which obtains the digest of the entry's uncompressed data. + */ + private static class GetJarEntryDataDigestRequest implements InspectJarEntryRequest { + private final String mEntryName; + private final String mJcaDigestAlgorithm; + private final Object mLock = new Object(); + + private boolean mDone; + private DataSink mDataSink; + private MessageDigest mMessageDigest; + private byte[] mDigest; + + private GetJarEntryDataDigestRequest(String entryName, String jcaDigestAlgorithm) { + mEntryName = entryName; + mJcaDigestAlgorithm = jcaDigestAlgorithm; + } + + @Override + public String getEntryName() { + return mEntryName; + } + + @Override + public DataSink getDataSink() { + synchronized (mLock) { + checkNotDone(); + if (mDataSink == null) { + mDataSink = new MessageDigestSink(new MessageDigest[] {getMessageDigest()}); + } + return mDataSink; + } + } + + private MessageDigest getMessageDigest() { + synchronized (mLock) { + if (mMessageDigest == null) { + try { + mMessageDigest = MessageDigest.getInstance(mJcaDigestAlgorithm); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException( + mJcaDigestAlgorithm + " MessageDigest not available", e); + } + } + return mMessageDigest; + } + } + + @Override + public void done() { + synchronized (mLock) { + if (mDone) { + return; + } + mDone = true; + mDigest = getMessageDigest().digest(); + mMessageDigest = null; + mDataSink = null; + } + } + + private boolean isDone() { + synchronized (mLock) { + return mDone; + } + } + + private void checkNotDone() throws IllegalStateException { + synchronized (mLock) { + if (mDone) { + throw new IllegalStateException("Already done"); + } + } + } + + private byte[] getDigest() { + synchronized (mLock) { + if (!mDone) { + throw new IllegalStateException("Not yet done"); + } + return mDigest.clone(); + } + } + } + + /** + * Configuration of a signer. + * + *

Use {@link Builder} to obtain configuration instances. + */ + public static class SignerConfig { + private final String mName; + private final PrivateKey mPrivateKey; + private final List mCertificates; + + private SignerConfig( + String name, + PrivateKey privateKey, + List certificates) { + mName = name; + mPrivateKey = privateKey; + mCertificates = Collections.unmodifiableList(new ArrayList<>(certificates)); + } + + /** + * Returns the name of this signer. + */ + public String getName() { + return mName; + } + + /** + * Returns the signing key of this signer. + */ + public PrivateKey getPrivateKey() { + return mPrivateKey; + } + + /** + * Returns the certificate(s) of this signer. The first certificate's public key corresponds + * to this signer's private key. + */ + public List getCertificates() { + return mCertificates; + } + + /** + * Builder of {@link SignerConfig} instances. + */ + public static class Builder { + private final String mName; + private final PrivateKey mPrivateKey; + private final List mCertificates; + + /** + * Constructs a new {@code Builder}. + * + * @param name signer's name. The name is reflected in the name of files comprising the + * JAR signature of the APK. + * @param privateKey signing key + * @param certificates list of one or more X.509 certificates. The subject public key of + * the first certificate must correspond to the {@code privateKey}. + */ + public Builder( + String name, + PrivateKey privateKey, + List certificates) { + mName = name; + mPrivateKey = privateKey; + mCertificates = new ArrayList<>(certificates); + } + + /** + * Returns a new {@code SignerConfig} instance configured based on the configuration of + * this builder. + */ + public SignerConfig build() { + return new SignerConfig( + mName, + mPrivateKey, + mCertificates); + } + } + } + + /** + * Builder of {@link DefaultApkSignerEngine} instances. + */ + public static class Builder { + private final List mSignerConfigs; + private final int mMinSdkVersion; + + private boolean mV1SigningEnabled = true; + private boolean mV2SigningEnabled = true; + private boolean mOtherSignersSignaturesPreserved; + + /** + * Constructs a new {@code Builder}. + * + * @param signerConfigs information about signers with which the APK will be signed. At + * least one signer configuration must be provided. + * @param minSdkVersion API Level of the oldest Android platform on which the APK is + * supposed to be installed. See {@code minSdkVersion} attribute in the APK's + * {@code AndroidManifest.xml}. The higher the version, the stronger signing features + * will be enabled. + */ + public Builder( + List signerConfigs, + int minSdkVersion) { + if (signerConfigs.isEmpty()) { + throw new IllegalArgumentException("At least one signer config must be provided"); + } + mSignerConfigs = new ArrayList<>(signerConfigs); + mMinSdkVersion = minSdkVersion; + } + + /** + * Returns a new {@code DefaultApkSignerEngine} instance configured based on the + * configuration of this builder. + */ + public DefaultApkSignerEngine build() throws InvalidKeyException { + return new DefaultApkSignerEngine( + mSignerConfigs, + mMinSdkVersion, + mV1SigningEnabled, + mV2SigningEnabled, + mOtherSignersSignaturesPreserved); + } + + /** + * Sets whether the APK should be signed using JAR signing (aka v1 signature scheme). + * + *

By default, the APK will be signed using this scheme. + */ + public Builder setV1SigningEnabled(boolean enabled) { + mV1SigningEnabled = enabled; + return this; + } + + /** + * Sets whether the APK should be signed using APK Signature Scheme v2 (aka v2 signature + * scheme). + * + *

By default, the APK will be signed using this scheme. + */ + public Builder setV2SigningEnabled(boolean enabled) { + mV2SigningEnabled = enabled; + return this; + } + + /** + * Sets whether signatures produced by signers other than the ones configured in this engine + * should be copied from the input APK to the output APK. + * + *

By default, signatures of other signers are omitted from the output APK. + */ + public Builder setOtherSignersSignaturesPreserved(boolean preserved) { + mOtherSignersSignaturesPreserved = preserved; + return this; + } + } +} diff --git a/plugin/src/main/java/com/android/apksigner/core/apk/ApkUtils.java b/plugin/src/main/java/com/android/apksigner/core/apk/ApkUtils.java new file mode 100644 index 0000000..8cc8c90 --- /dev/null +++ b/plugin/src/main/java/com/android/apksigner/core/apk/ApkUtils.java @@ -0,0 +1,158 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksigner.core.apk; + +import com.android.apksigner.core.internal.util.Pair; +import com.android.apksigner.core.internal.zip.ZipUtils; +import com.android.apksigner.core.util.DataSource; +import com.android.apksigner.core.zip.ZipFormatException; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +/** + * APK utilities. + */ +public class ApkUtils { + + private ApkUtils() {} + + /** + * Finds the main ZIP sections of the provided APK. + * + * @throws IOException if an I/O error occurred while reading the APK + * @throws ZipFormatException if the APK is malformed + */ + public static ZipSections findZipSections(DataSource apk) + throws IOException, ZipFormatException { + Pair eocdAndOffsetInFile = + ZipUtils.findZipEndOfCentralDirectoryRecord(apk); + if (eocdAndOffsetInFile == null) { + throw new ZipFormatException("ZIP End of Central Directory record not found"); + } + + ByteBuffer eocdBuf = eocdAndOffsetInFile.getFirst(); + long eocdOffset = eocdAndOffsetInFile.getSecond(); + if (ZipUtils.isZip64EndOfCentralDirectoryLocatorPresent(apk, eocdOffset)) { + throw new ZipFormatException("ZIP64 APK not supported"); + } + eocdBuf.order(ByteOrder.LITTLE_ENDIAN); + long cdStartOffset = ZipUtils.getZipEocdCentralDirectoryOffset(eocdBuf); + if (cdStartOffset >= eocdOffset) { + throw new ZipFormatException( + "ZIP Central Directory start offset out of range: " + cdStartOffset + + ". ZIP End of Central Directory offset: " + eocdOffset); + } + + long cdSizeBytes = ZipUtils.getZipEocdCentralDirectorySizeBytes(eocdBuf); + long cdEndOffset = cdStartOffset + cdSizeBytes; + if (cdEndOffset > eocdOffset) { + throw new ZipFormatException( + "ZIP Central Directory overlaps with End of Central Directory" + + ". CD end: " + cdEndOffset + + ", EoCD start: " + eocdOffset); + } + + int cdRecordCount = ZipUtils.getZipEocdCentralDirectoryTotalRecordCount(eocdBuf); + + return new ZipSections( + cdStartOffset, + cdSizeBytes, + cdRecordCount, + eocdOffset, + eocdBuf); + } + + /** + * Information about the ZIP sections of an APK. + */ + public static class ZipSections { + private final long mCentralDirectoryOffset; + private final long mCentralDirectorySizeBytes; + private final int mCentralDirectoryRecordCount; + private final long mEocdOffset; + private final ByteBuffer mEocd; + + public ZipSections( + long centralDirectoryOffset, + long centralDirectorySizeBytes, + int centralDirectoryRecordCount, + long eocdOffset, + ByteBuffer eocd) { + mCentralDirectoryOffset = centralDirectoryOffset; + mCentralDirectorySizeBytes = centralDirectorySizeBytes; + mCentralDirectoryRecordCount = centralDirectoryRecordCount; + mEocdOffset = eocdOffset; + mEocd = eocd; + } + + /** + * Returns the start offset of the ZIP Central Directory. This value is taken from the + * ZIP End of Central Directory record. + */ + public long getZipCentralDirectoryOffset() { + return mCentralDirectoryOffset; + } + + /** + * Returns the size (in bytes) of the ZIP Central Directory. This value is taken from the + * ZIP End of Central Directory record. + */ + public long getZipCentralDirectorySizeBytes() { + return mCentralDirectorySizeBytes; + } + + /** + * Returns the number of records in the ZIP Central Directory. This value is taken from the + * ZIP End of Central Directory record. + */ + public int getZipCentralDirectoryRecordCount() { + return mCentralDirectoryRecordCount; + } + + /** + * Returns the start offset of the ZIP End of Central Directory record. The record extends + * until the very end of the APK. + */ + public long getZipEndOfCentralDirectoryOffset() { + return mEocdOffset; + } + + /** + * Returns the contents of the ZIP End of Central Directory. + */ + public ByteBuffer getZipEndOfCentralDirectory() { + return mEocd; + } + } + + /** + * Sets the offset of the start of the ZIP Central Directory in the APK's ZIP End of Central + * Directory record. + * + * @param zipEndOfCentralDirectory APK's ZIP End of Central Directory record + * @param offset offset of the ZIP Central Directory relative to the start of the archive. Must + * be between {@code 0} and {@code 2^32 - 1} inclusive. + */ + public static void setZipEocdCentralDirectoryOffset( + ByteBuffer zipEndOfCentralDirectory, long offset) { + ByteBuffer eocd = zipEndOfCentralDirectory.slice(); + eocd.order(ByteOrder.LITTLE_ENDIAN); + ZipUtils.setZipEocdCentralDirectoryOffset(eocd, offset); + } +} diff --git a/plugin/src/main/java/com/android/apksigner/core/internal/apk/v1/DigestAlgorithm.java b/plugin/src/main/java/com/android/apksigner/core/internal/apk/v1/DigestAlgorithm.java new file mode 100644 index 0000000..71e698b --- /dev/null +++ b/plugin/src/main/java/com/android/apksigner/core/internal/apk/v1/DigestAlgorithm.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksigner.core.internal.apk.v1; + +/** + * Digest algorithm used with JAR signing (aka v1 signing scheme). + */ +public enum DigestAlgorithm { + /** SHA-1 */ + SHA1("SHA-1"), + + /** SHA2-256 */ + SHA256("SHA-256"); + + private final String mJcaMessageDigestAlgorithm; + + private DigestAlgorithm(String jcaMessageDigestAlgoritm) { + mJcaMessageDigestAlgorithm = jcaMessageDigestAlgoritm; + } + + /** + * Returns the {@link java.security.MessageDigest} algorithm represented by this digest + * algorithm. + */ + String getJcaMessageDigestAlgorithm() { + return mJcaMessageDigestAlgorithm; + } +} diff --git a/plugin/src/main/java/com/android/apksigner/core/internal/apk/v1/V1SchemeSigner.java b/plugin/src/main/java/com/android/apksigner/core/internal/apk/v1/V1SchemeSigner.java new file mode 100644 index 0000000..9f4ccce --- /dev/null +++ b/plugin/src/main/java/com/android/apksigner/core/internal/apk/v1/V1SchemeSigner.java @@ -0,0 +1,573 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksigner.core.internal.apk.v1; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.security.InvalidKeyException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.SignatureException; +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Base64; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.SortedMap; +import java.util.TreeMap; +import java.util.jar.Attributes; +import java.util.jar.Manifest; + +import org.bouncycastle.asn1.ASN1InputStream; +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.DERNull; +import org.bouncycastle.asn1.DEROutputStream; +import org.bouncycastle.asn1.x509.AlgorithmIdentifier; +import org.bouncycastle.asn1.x9.X9ObjectIdentifiers; +import org.bouncycastle.cert.jcajce.JcaCertStore; +import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder; +import org.bouncycastle.cms.CMSException; +import org.bouncycastle.cms.CMSProcessableByteArray; +import org.bouncycastle.cms.CMSSignatureEncryptionAlgorithmFinder; +import org.bouncycastle.cms.CMSSignedData; +import org.bouncycastle.cms.CMSSignedDataGenerator; +import org.bouncycastle.cms.DefaultCMSSignatureEncryptionAlgorithmFinder; +import org.bouncycastle.cms.SignerInfoGeneratorBuilder; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder; + +import com.android.apksigner.core.internal.jar.ManifestWriter; +import com.android.apksigner.core.internal.jar.SignatureFileWriter; +import com.android.apksigner.core.internal.util.Pair; + +/** + * APK signer which uses JAR signing (aka v1 signing scheme). + * + * @see Signed JAR File + */ +public abstract class V1SchemeSigner { + + public static final String MANIFEST_ENTRY_NAME = "META-INF/MANIFEST.MF"; + + private static final Attributes.Name ATTRIBUTE_NAME_CREATED_BY = + new Attributes.Name("Created-By"); + private static final String ATTRIBUTE_DEFALT_VALUE_CREATED_BY = "1.0 (Android apksigner)"; + private static final String ATTRIBUTE_VALUE_MANIFEST_VERSION = "1.0"; + private static final String ATTRIBUTE_VALUE_SIGNATURE_VERSION = "1.0"; + + private static final Attributes.Name SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME = + new Attributes.Name("X-Android-APK-Signed"); + + /** + * Signer configuration. + */ + public static class SignerConfig { + /** Name. */ + public String name; + + /** Private key. */ + public PrivateKey privateKey; + + /** + * Certificates, with the first certificate containing the public key corresponding to + * {@link #privateKey}. + */ + public List certificates; + + /** + * Digest algorithm used for the signature. + */ + public DigestAlgorithm signatureDigestAlgorithm; + + /** + * Digest algorithm used for digests of JAR entries and MANIFEST.MF. + */ + public DigestAlgorithm contentDigestAlgorithm; + } + + /** Hidden constructor to prevent instantiation. */ + private V1SchemeSigner() {} + + /** + * Gets the JAR signing digest algorithm to be used for signing an APK using the provided key. + * + * @param minSdkVersion minimum API Level of the platform on which the APK may be installed (see + * AndroidManifest.xml minSdkVersion attribute) + * + * @throws InvalidKeyException if the provided key is not suitable for signing APKs using + * JAR signing (aka v1 signature scheme) + */ + public static DigestAlgorithm getSuggestedSignatureDigestAlgorithm( + PublicKey signingKey, int minSdkVersion) throws InvalidKeyException { + String keyAlgorithm = signingKey.getAlgorithm(); + if ("RSA".equalsIgnoreCase(keyAlgorithm)) { + // Prior to API Level 18, only SHA-1 can be used with RSA. + if (minSdkVersion < 18) { + return DigestAlgorithm.SHA1; + } + return DigestAlgorithm.SHA256; + } else if ("DSA".equalsIgnoreCase(keyAlgorithm)) { + // Prior to API Level 21, only SHA-1 can be used with DSA + if (minSdkVersion < 21) { + return DigestAlgorithm.SHA1; + } else { + return DigestAlgorithm.SHA256; + } + } else if ("EC".equalsIgnoreCase(keyAlgorithm)) { + if (minSdkVersion < 18) { + throw new InvalidKeyException( + "ECDSA signatures only supported for minSdkVersion 18 and higher"); + } + // Prior to API Level 21, only SHA-1 can be used with ECDSA + if (minSdkVersion < 21) { + return DigestAlgorithm.SHA1; + } else { + return DigestAlgorithm.SHA256; + } + } else { + throw new InvalidKeyException("Unsupported key algorithm: " + keyAlgorithm); + } + } + + /** + * Returns the JAR signing digest algorithm to be used for JAR entry digests. + * + * @param minSdkVersion minimum API Level of the platform on which the APK may be installed (see + * AndroidManifest.xml minSdkVersion attribute) + */ + public static DigestAlgorithm getSuggestedContentDigestAlgorithm(int minSdkVersion) { + return (minSdkVersion >= 18) ? DigestAlgorithm.SHA256 : DigestAlgorithm.SHA1; + } + + /** + * Returns a new {@link MessageDigest} instance corresponding to the provided digest algorithm. + */ + public static MessageDigest getMessageDigestInstance(DigestAlgorithm digestAlgorithm) { + String jcaAlgorithm = digestAlgorithm.getJcaMessageDigestAlgorithm(); + try { + return MessageDigest.getInstance(jcaAlgorithm); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("Failed to obtain " + jcaAlgorithm + " MessageDigest", e); + } + } + + /** + * Returns the JCA {@link MessageDigest} algorithm corresponding to the provided digest + * algorithm. + */ + public static String getJcaMessageDigestAlgorithm(DigestAlgorithm digestAlgorithm) { + return digestAlgorithm.getJcaMessageDigestAlgorithm(); + } + + /** + * Returns {@code true} if the provided JAR entry must be mentioned in signed JAR archive's + * manifest. + */ + public static boolean isJarEntryDigestNeededInManifest(String entryName) { + // See https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Signed_JAR_File + + // Entries outside of META-INF must be listed in the manifest. + if (!entryName.startsWith("META-INF/")) { + return true; + } + // Entries in subdirectories of META-INF must be listed in the manifest. + if (entryName.indexOf('/', "META-INF/".length()) != -1) { + return true; + } + + // Ignored file names (case-insensitive) in META-INF directory: + // MANIFEST.MF + // *.SF + // *.RSA + // *.DSA + // *.EC + // SIG-* + String fileNameLowerCase = + entryName.substring("META-INF/".length()).toLowerCase(Locale.US); + if (("manifest.mf".equals(fileNameLowerCase)) + || (fileNameLowerCase.endsWith(".sf")) + || (fileNameLowerCase.endsWith(".rsa")) + || (fileNameLowerCase.endsWith(".dsa")) + || (fileNameLowerCase.endsWith(".ec")) + || (fileNameLowerCase.startsWith("sig-"))) { + return false; + } + return true; + } + + /** + * Signs the provided APK using JAR signing (aka v1 signature scheme) and returns the list of + * JAR entries which need to be added to the APK as part of the signature. + * + * @param signerConfigs signer configurations, one for each signer. At least one signer config + * must be provided. + * + * @throws InvalidKeyException if a signing key is not suitable for this signature scheme or + * cannot be used in general + * @throws SignatureException if an error occurs when computing digests of generating + * signatures + */ + public static List> sign( + List signerConfigs, + DigestAlgorithm jarEntryDigestAlgorithm, + Map jarEntryDigests, + List apkSigningSchemeIds, + byte[] sourceManifestBytes) + throws InvalidKeyException, CertificateEncodingException, SignatureException { + if (signerConfigs.isEmpty()) { + throw new IllegalArgumentException("At least one signer config must be provided"); + } + OutputManifestFile manifest = + generateManifestFile(jarEntryDigestAlgorithm, jarEntryDigests, sourceManifestBytes); + + return signManifest(signerConfigs, jarEntryDigestAlgorithm, apkSigningSchemeIds, manifest); + } + + /** + * Signs the provided APK using JAR signing (aka v1 signature scheme) and returns the list of + * JAR entries which need to be added to the APK as part of the signature. + * + * @param signerConfigs signer configurations, one for each signer. At least one signer config + * must be provided. + * + * @throws InvalidKeyException if a signing key is not suitable for this signature scheme or + * cannot be used in general + * @throws SignatureException if an error occurs when computing digests of generating + * signatures + */ + public static List> signManifest( + List signerConfigs, + DigestAlgorithm digestAlgorithm, + List apkSigningSchemeIds, + OutputManifestFile manifest) + throws InvalidKeyException, CertificateEncodingException, SignatureException { + if (signerConfigs.isEmpty()) { + throw new IllegalArgumentException("At least one signer config must be provided"); + } + + // For each signer output .SF and .(RSA|DSA|EC) file, then output MANIFEST.MF. + List> signatureJarEntries = + new ArrayList<>(2 * signerConfigs.size() + 1); + byte[] sfBytes = + generateSignatureFile(apkSigningSchemeIds, digestAlgorithm, manifest); + for (SignerConfig signerConfig : signerConfigs) { + String signerName = signerConfig.name; + byte[] signatureBlock; + try { + signatureBlock = generateSignatureBlock(signerConfig, sfBytes); + } catch (InvalidKeyException e) { + throw new InvalidKeyException( + "Failed to sign using signer \"" + signerName + "\"", e); + } catch (CertificateEncodingException e) { + throw new CertificateEncodingException( + "Failed to sign using signer \"" + signerName + "\"", e); + } catch (SignatureException e) { + throw new SignatureException( + "Failed to sign using signer \"" + signerName + "\"", e); + } + signatureJarEntries.add(Pair.of("META-INF/" + signerName + ".SF", sfBytes)); + PublicKey publicKey = signerConfig.certificates.get(0).getPublicKey(); + String signatureBlockFileName = + "META-INF/" + signerName + "." + + publicKey.getAlgorithm().toUpperCase(Locale.US); + signatureJarEntries.add( + Pair.of(signatureBlockFileName, signatureBlock)); + } + signatureJarEntries.add(Pair.of(MANIFEST_ENTRY_NAME, manifest.contents)); + return signatureJarEntries; + } + + /** + * Returns the names of JAR entries which this signer will produce as part of v1 signature. + */ + public static Set getOutputEntryNames(List signerConfigs) { + Set result = new HashSet<>(2 * signerConfigs.size() + 1); + for (SignerConfig signerConfig : signerConfigs) { + String signerName = signerConfig.name; + result.add("META-INF/" + signerName + ".SF"); + PublicKey publicKey = signerConfig.certificates.get(0).getPublicKey(); + String signatureBlockFileName = + "META-INF/" + signerName + "." + + publicKey.getAlgorithm().toUpperCase(Locale.US); + result.add(signatureBlockFileName); + } + result.add(MANIFEST_ENTRY_NAME); + return result; + } + + /** + * Generated and returns the {@code META-INF/MANIFEST.MF} file based on the provided (optional) + * input {@code MANIFEST.MF} and digests of JAR entries covered by the manifest. + */ + public static OutputManifestFile generateManifestFile( + DigestAlgorithm jarEntryDigestAlgorithm, + Map jarEntryDigests, + byte[] sourceManifestBytes) { + Manifest sourceManifest = null; + if (sourceManifestBytes != null) { + try { + sourceManifest = new Manifest(new ByteArrayInputStream(sourceManifestBytes)); + } catch (IOException e) { + throw new IllegalArgumentException("Failed to parse source MANIFEST.MF", e); + } + } + ByteArrayOutputStream manifestOut = new ByteArrayOutputStream(); + Attributes mainAttrs = new Attributes(); + // Copy the main section from the source manifest (if provided). Otherwise use defaults. + if (sourceManifest != null) { + mainAttrs.putAll(sourceManifest.getMainAttributes()); + } else { + mainAttrs.put(Attributes.Name.MANIFEST_VERSION, ATTRIBUTE_VALUE_MANIFEST_VERSION); + mainAttrs.put(ATTRIBUTE_NAME_CREATED_BY, ATTRIBUTE_DEFALT_VALUE_CREATED_BY); + } + + try { + ManifestWriter.writeMainSection(manifestOut, mainAttrs); + } catch (IOException e) { + throw new RuntimeException("Failed to write in-memory MANIFEST.MF", e); + } + + List sortedEntryNames = new ArrayList<>(jarEntryDigests.keySet()); + Collections.sort(sortedEntryNames); + SortedMap invidualSectionsContents = new TreeMap<>(); + String entryDigestAttributeName = getEntryDigestAttributeName(jarEntryDigestAlgorithm); + for (String entryName : sortedEntryNames) { + byte[] entryDigest = jarEntryDigests.get(entryName); + Attributes entryAttrs = new Attributes(); + entryAttrs.putValue( + entryDigestAttributeName, + Base64.getEncoder().encodeToString(entryDigest)); + ByteArrayOutputStream sectionOut = new ByteArrayOutputStream(); + byte[] sectionBytes; + try { + ManifestWriter.writeIndividualSection(sectionOut, entryName, entryAttrs); + sectionBytes = sectionOut.toByteArray(); + manifestOut.write(sectionBytes); + } catch (IOException e) { + throw new RuntimeException("Failed to write in-memory MANIFEST.MF", e); + } + invidualSectionsContents.put(entryName, sectionBytes); + } + + OutputManifestFile result = new OutputManifestFile(); + result.contents = manifestOut.toByteArray(); + result.mainSectionAttributes = mainAttrs; + result.individualSectionsContents = invidualSectionsContents; + return result; + } + + public static class OutputManifestFile { + public byte[] contents; + public SortedMap individualSectionsContents; + public Attributes mainSectionAttributes; + } + + private static byte[] generateSignatureFile( + List apkSignatureSchemeIds, + DigestAlgorithm manifestDigestAlgorithm, + OutputManifestFile manifest) { + Manifest sf = new Manifest(); + Attributes mainAttrs = sf.getMainAttributes(); + mainAttrs.put(Attributes.Name.SIGNATURE_VERSION, ATTRIBUTE_VALUE_SIGNATURE_VERSION); + mainAttrs.put(ATTRIBUTE_NAME_CREATED_BY, ATTRIBUTE_DEFALT_VALUE_CREATED_BY); + if (!apkSignatureSchemeIds.isEmpty()) { + // Add APK Signature Scheme v2 (and newer) signature stripping protection. + // This attribute indicates that this APK is supposed to have been signed using one or + // more APK-specific signature schemes in addition to the standard JAR signature scheme + // used by this code. APK signature verifier should reject the APK if it does not + // contain a signature for the signature scheme the verifier prefers out of this set. + StringBuilder attrValue = new StringBuilder(); + for (int id : apkSignatureSchemeIds) { + if (attrValue.length() > 0) { + attrValue.append(", "); + } + attrValue.append(String.valueOf(id)); + } + mainAttrs.put( + SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME, + attrValue.toString()); + } + + // Add main attribute containing the digest of MANIFEST.MF. + MessageDigest md = getMessageDigestInstance(manifestDigestAlgorithm); + mainAttrs.putValue( + getManifestDigestAttributeName(manifestDigestAlgorithm), + Base64.getEncoder().encodeToString(md.digest(manifest.contents))); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + try { + SignatureFileWriter.writeMainSection(out, mainAttrs); + } catch (IOException e) { + throw new RuntimeException("Failed to write in-memory .SF file", e); + } + String entryDigestAttributeName = getEntryDigestAttributeName(manifestDigestAlgorithm); + for (Map.Entry manifestSection + : manifest.individualSectionsContents.entrySet()) { + String sectionName = manifestSection.getKey(); + byte[] sectionContents = manifestSection.getValue(); + byte[] sectionDigest = md.digest(sectionContents); + Attributes attrs = new Attributes(); + attrs.putValue( + entryDigestAttributeName, + Base64.getEncoder().encodeToString(sectionDigest)); + + try { + SignatureFileWriter.writeIndividualSection(out, sectionName, attrs); + } catch (IOException e) { + throw new RuntimeException("Failed to write in-memory .SF file", e); + } + } + + // A bug in the java.util.jar implementation of Android platforms up to version 1.6 will + // cause a spurious IOException to be thrown if the length of the signature file is a + // multiple of 1024 bytes. As a workaround, add an extra CRLF in this case. + if ((out.size() > 0) && ((out.size() % 1024) == 0)) { + try { + SignatureFileWriter.writeSectionDelimiter(out); + } catch (IOException e) { + throw new RuntimeException("Failed to write to ByteArrayOutputStream", e); + } + } + + return out.toByteArray(); + } + + private static byte[] generateSignatureBlock( + SignerConfig signerConfig, byte[] signatureFileBytes) + throws InvalidKeyException, CertificateEncodingException, SignatureException { + JcaCertStore certs = new JcaCertStore(signerConfig.certificates); + X509Certificate signerCert = signerConfig.certificates.get(0); + String jcaSignatureAlgorithm = + getJcaSignatureAlgorithm( + signerCert.getPublicKey(), signerConfig.signatureDigestAlgorithm); + try { + ContentSigner signer = + new JcaContentSignerBuilder(jcaSignatureAlgorithm) + .build(signerConfig.privateKey); + CMSSignedDataGenerator gen = new CMSSignedDataGenerator(); + gen.addSignerInfoGenerator( + new SignerInfoGeneratorBuilder( + new JcaDigestCalculatorProviderBuilder().build(), + SignerInfoSignatureAlgorithmFinder.INSTANCE) + .setDirectSignature(true) + .build(signer, new JcaX509CertificateHolder(signerCert))); + gen.addCertificates(certs); + + CMSSignedData sigData = + gen.generate(new CMSProcessableByteArray(signatureFileBytes), false); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + try (ASN1InputStream asn1 = new ASN1InputStream(sigData.getEncoded())) { + DEROutputStream dos = new DEROutputStream(out); + dos.writeObject(asn1.readObject()); + } + return out.toByteArray(); + } catch (OperatorCreationException | CMSException | IOException e) { + throw new SignatureException("Failed to generate signature", e); + } + } + + /** + * Chooser of SignatureAlgorithm for PKCS #7 CMS SignerInfo. + */ + private static class SignerInfoSignatureAlgorithmFinder + implements CMSSignatureEncryptionAlgorithmFinder { + private static final SignerInfoSignatureAlgorithmFinder INSTANCE = + new SignerInfoSignatureAlgorithmFinder(); + + private static final AlgorithmIdentifier DSA = + new AlgorithmIdentifier(X9ObjectIdentifiers.id_dsa, DERNull.INSTANCE); + + private final CMSSignatureEncryptionAlgorithmFinder mDefault = + new DefaultCMSSignatureEncryptionAlgorithmFinder(); + + @Override + public AlgorithmIdentifier findEncryptionAlgorithm(AlgorithmIdentifier id) { + // Use the default chooser, but replace dsaWithSha1 with dsa. This is because "dsa" is + // accepted by any Android platform whereas "dsaWithSha1" is accepted only since + // API Level 9. + id = mDefault.findEncryptionAlgorithm(id); + if (id != null) { + ASN1ObjectIdentifier oid = id.getAlgorithm(); + if (X9ObjectIdentifiers.id_dsa_with_sha1.equals(oid)) { + return DSA; + } + } + + return id; + } + } + + private static String getEntryDigestAttributeName(DigestAlgorithm digestAlgorithm) { + switch (digestAlgorithm) { + case SHA1: + return "SHA1-Digest"; + case SHA256: + return "SHA-256-Digest"; + default: + throw new IllegalArgumentException( + "Unexpected content digest algorithm: " + digestAlgorithm); + } + } + + private static String getManifestDigestAttributeName(DigestAlgorithm digestAlgorithm) { + switch (digestAlgorithm) { + case SHA1: + return "SHA1-Digest-Manifest"; + case SHA256: + return "SHA-256-Digest-Manifest"; + default: + throw new IllegalArgumentException( + "Unexpected content digest algorithm: " + digestAlgorithm); + } + } + + private static String getJcaSignatureAlgorithm( + PublicKey publicKey, DigestAlgorithm digestAlgorithm) throws InvalidKeyException { + String keyAlgorithm = publicKey.getAlgorithm(); + String digestPrefixForSigAlg; + switch (digestAlgorithm) { + case SHA1: + digestPrefixForSigAlg = "SHA1"; + break; + case SHA256: + digestPrefixForSigAlg = "SHA256"; + break; + default: + throw new IllegalArgumentException( + "Unexpected digest algorithm: " + digestAlgorithm); + } + if ("RSA".equalsIgnoreCase(keyAlgorithm)) { + return digestPrefixForSigAlg + "withRSA"; + } else if ("DSA".equalsIgnoreCase(keyAlgorithm)) { + return digestPrefixForSigAlg + "withDSA"; + } else if ("EC".equalsIgnoreCase(keyAlgorithm)) { + return digestPrefixForSigAlg + "withECDSA"; + } else { + throw new InvalidKeyException("Unsupported key algorithm: " + keyAlgorithm); + } + } +} diff --git a/plugin/src/main/java/com/android/apksigner/core/internal/apk/v2/ContentDigestAlgorithm.java b/plugin/src/main/java/com/android/apksigner/core/internal/apk/v2/ContentDigestAlgorithm.java new file mode 100644 index 0000000..7c136be --- /dev/null +++ b/plugin/src/main/java/com/android/apksigner/core/internal/apk/v2/ContentDigestAlgorithm.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksigner.core.internal.apk.v2; + +/** + * APK Signature Scheme v2 content digest algorithm. + */ +public enum ContentDigestAlgorithm { + /** SHA2-256 over 1 MB chunks. */ + CHUNKED_SHA256("SHA-256", 256 / 8), + + /** SHA2-512 over 1 MB chunks. */ + CHUNKED_SHA512("SHA-512", 512 / 8); + + private final String mJcaMessageDigestAlgorithm; + private final int mChunkDigestOutputSizeBytes; + + private ContentDigestAlgorithm( + String jcaMessageDigestAlgorithm, int chunkDigestOutputSizeBytes) { + mJcaMessageDigestAlgorithm = jcaMessageDigestAlgorithm; + mChunkDigestOutputSizeBytes = chunkDigestOutputSizeBytes; + } + + /** + * Returns the {@link java.security.MessageDigest} algorithm used for computing digests of + * chunks by this content digest algorithm. + */ + String getJcaMessageDigestAlgorithm() { + return mJcaMessageDigestAlgorithm; + } + + /** + * Returns the size (in bytes) of the digest of a chunk of content. + */ + int getChunkDigestOutputSizeBytes() { + return mChunkDigestOutputSizeBytes; + } +} \ No newline at end of file diff --git a/plugin/src/main/java/com/android/apksigner/core/internal/apk/v2/SignatureAlgorithm.java b/plugin/src/main/java/com/android/apksigner/core/internal/apk/v2/SignatureAlgorithm.java new file mode 100644 index 0000000..20f890d --- /dev/null +++ b/plugin/src/main/java/com/android/apksigner/core/internal/apk/v2/SignatureAlgorithm.java @@ -0,0 +1,142 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksigner.core.internal.apk.v2; + +import com.android.apksigner.core.internal.util.Pair; + +import java.security.spec.AlgorithmParameterSpec; +import java.security.spec.MGF1ParameterSpec; +import java.security.spec.PSSParameterSpec; + +/** + * APK Signature Scheme v2 signature algorithm. + */ +public enum SignatureAlgorithm { + /** + * RSASSA-PSS with SHA2-256 digest, SHA2-256 MGF1, 32 bytes of salt, trailer: 0xbc, content + * digested using SHA2-256 in 1 MB chunks. + */ + RSA_PSS_WITH_SHA256( + 0x0101, + ContentDigestAlgorithm.CHUNKED_SHA256, + "RSA", + Pair.of("SHA256withRSA/PSS", + new PSSParameterSpec( + "SHA-256", "MGF1", MGF1ParameterSpec.SHA256, 256 / 8, 1))), + + /** + * RSASSA-PSS with SHA2-512 digest, SHA2-512 MGF1, 64 bytes of salt, trailer: 0xbc, content + * digested using SHA2-512 in 1 MB chunks. + */ + RSA_PSS_WITH_SHA512( + 0x0102, + ContentDigestAlgorithm.CHUNKED_SHA512, + "RSA", + Pair.of( + "SHA512withRSA/PSS", + new PSSParameterSpec( + "SHA-512", "MGF1", MGF1ParameterSpec.SHA512, 512 / 8, 1))), + + /** RSASSA-PKCS1-v1_5 with SHA2-256 digest, content digested using SHA2-256 in 1 MB chunks. */ + RSA_PKCS1_V1_5_WITH_SHA256( + 0x0103, + ContentDigestAlgorithm.CHUNKED_SHA256, + "RSA", + Pair.of("SHA256withRSA", null)), + + /** RSASSA-PKCS1-v1_5 with SHA2-512 digest, content digested using SHA2-512 in 1 MB chunks. */ + RSA_PKCS1_V1_5_WITH_SHA512( + 0x0104, + ContentDigestAlgorithm.CHUNKED_SHA512, + "RSA", + Pair.of("SHA512withRSA", null)), + + /** ECDSA with SHA2-256 digest, content digested using SHA2-256 in 1 MB chunks. */ + ECDSA_WITH_SHA256( + 0x0201, + ContentDigestAlgorithm.CHUNKED_SHA256, + "EC", + Pair.of("SHA256withECDSA", null)), + + /** ECDSA with SHA2-512 digest, content digested using SHA2-512 in 1 MB chunks. */ + ECDSA_WITH_SHA512( + 0x0202, + ContentDigestAlgorithm.CHUNKED_SHA512, + "EC", + Pair.of("SHA512withECDSA", null)), + + /** DSA with SHA2-256 digest, content digested using SHA2-256 in 1 MB chunks. */ + DSA_WITH_SHA256( + 0x0301, + ContentDigestAlgorithm.CHUNKED_SHA256, + "DSA", + Pair.of("SHA256withDSA", null)); + + private final int mId; + private final String mJcaKeyAlgorithm; + private final ContentDigestAlgorithm mContentDigestAlgorithm; + private final Pair mJcaSignatureAlgAndParams; + + private SignatureAlgorithm(int id, + ContentDigestAlgorithm contentDigestAlgorithm, + String jcaKeyAlgorithm, + Pair jcaSignatureAlgAndParams) { + mId = id; + mContentDigestAlgorithm = contentDigestAlgorithm; + mJcaKeyAlgorithm = jcaKeyAlgorithm; + mJcaSignatureAlgAndParams = jcaSignatureAlgAndParams; + } + + /** + * Returns the ID of this signature algorithm as used in APK Signature Scheme v2 wire format. + */ + int getId() { + return mId; + } + + /** + * Returns the content digest algorithm associated with this signature algorithm. + */ + ContentDigestAlgorithm getContentDigestAlgorithm() { + return mContentDigestAlgorithm; + } + + /** + * Returns the JCA {@link java.security.Key} algorithm used by this signature scheme. + */ + String getJcaKeyAlgorithm() { + return mJcaKeyAlgorithm; + } + + /** + * Returns the {@link java.security.Signature} algorithm and the {@link AlgorithmParameterSpec} + * (or null if not needed) to parameterize the {@code Signature}. + */ + Pair getJcaSignatureAlgorithmAndParams() { + return mJcaSignatureAlgAndParams; + } + + static SignatureAlgorithm findById(int id) { + for (SignatureAlgorithm alg : SignatureAlgorithm.values()) { + if (alg.getId() == id) { + return alg; + } + } + + return null; + } +} diff --git a/plugin/src/main/java/com/android/apksigner/core/internal/apk/v2/V2SchemeSigner.java b/plugin/src/main/java/com/android/apksigner/core/internal/apk/v2/V2SchemeSigner.java new file mode 100644 index 0000000..aba390b --- /dev/null +++ b/plugin/src/main/java/com/android/apksigner/core/internal/apk/v2/V2SchemeSigner.java @@ -0,0 +1,605 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksigner.core.internal.apk.v2; + +import com.android.apksigner.core.internal.util.MessageDigestSink; +import com.android.apksigner.core.internal.util.Pair; +import com.android.apksigner.core.internal.zip.ZipUtils; +import com.android.apksigner.core.util.DataSource; +import com.android.apksigner.core.util.DataSources; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.security.DigestException; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.KeyFactory; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.Signature; +import java.security.SignatureException; +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; +import java.security.interfaces.ECKey; +import java.security.interfaces.RSAKey; +import java.security.spec.AlgorithmParameterSpec; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.X509EncodedKeySpec; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * APK Signature Scheme v2 signer. + * + *

APK Signature Scheme v2 is a whole-file signature scheme which aims to protect every single + * bit of the APK, as opposed to the JAR Signature Scheme which protects only the names and + * uncompressed contents of ZIP entries. + * + *

TODO: Link to APK Signature Scheme v2 documentation once it's available. + */ +public abstract class V2SchemeSigner { + /* + * The two main goals of APK Signature Scheme v2 are: + * 1. Detect any unauthorized modifications to the APK. This is achieved by making the signature + * cover every byte of the APK being signed. + * 2. Enable much faster signature and integrity verification. This is achieved by requiring + * only a minimal amount of APK parsing before the signature is verified, thus completely + * bypassing ZIP entry decompression and by making integrity verification parallelizable by + * employing a hash tree. + * + * The generated signature block is wrapped into an APK Signing Block and inserted into the + * original APK immediately before the start of ZIP Central Directory. This is to ensure that + * JAR and ZIP parsers continue to work on the signed APK. The APK Signing Block is designed for + * extensibility. For example, a future signature scheme could insert its signatures there as + * well. The contract of the APK Signing Block is that all contents outside of the block must be + * protected by signatures inside the block. + */ + + private static final int CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES = 1024 * 1024; + + private static final byte[] APK_SIGNING_BLOCK_MAGIC = + new byte[] { + 0x41, 0x50, 0x4b, 0x20, 0x53, 0x69, 0x67, 0x20, + 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x20, 0x34, 0x32, + }; + private static final int APK_SIGNATURE_SCHEME_V2_BLOCK_ID = 0x7109871a; + + /** + * Signer configuration. + */ + public static class SignerConfig { + /** Private key. */ + public PrivateKey privateKey; + + /** + * Certificates, with the first certificate containing the public key corresponding to + * {@link #privateKey}. + */ + public List certificates; + + /** + * List of signature algorithms with which to sign. + */ + public List signatureAlgorithms; + } + + /** Hidden constructor to prevent instantiation. */ + private V2SchemeSigner() {} + + /** + * Gets the APK Signature Scheme v2 signature algorithms to be used for signing an APK using the + * provided key. + * + * @param minSdkVersion minimum API Level of the platform on which the APK may be installed (see + * AndroidManifest.xml minSdkVersion attribute). + * + * @throws InvalidKeyException if the provided key is not suitable for signing APKs using + * APK Signature Scheme v2 + */ + public static List getSuggestedSignatureAlgorithms( + PublicKey signingKey, int minSdkVersion) throws InvalidKeyException { + String keyAlgorithm = signingKey.getAlgorithm(); + if ("RSA".equalsIgnoreCase(keyAlgorithm)) { + // Use RSASSA-PKCS1-v1_5 signature scheme instead of RSASSA-PSS to guarantee + // deterministic signatures which make life easier for OTA updates (fewer files + // changed when deterministic signature schemes are used). + + // Pick a digest which is no weaker than the key. + int modulusLengthBits = ((RSAKey) signingKey).getModulus().bitLength(); + if (modulusLengthBits <= 3072) { + // 3072-bit RSA is roughly 128-bit strong, meaning SHA-256 is a good fit. + return Collections.singletonList(SignatureAlgorithm.RSA_PKCS1_V1_5_WITH_SHA256); + } else { + // Keys longer than 3072 bit need to be paired with a stronger digest to avoid the + // digest being the weak link. SHA-512 is the next strongest supported digest. + return Collections.singletonList(SignatureAlgorithm.RSA_PKCS1_V1_5_WITH_SHA512); + } + } else if ("DSA".equalsIgnoreCase(keyAlgorithm)) { + // DSA is supported only with SHA-256. + return Collections.singletonList(SignatureAlgorithm.DSA_WITH_SHA256); + } else if ("EC".equalsIgnoreCase(keyAlgorithm)) { + // Pick a digest which is no weaker than the key. + int keySizeBits = ((ECKey) signingKey).getParams().getOrder().bitLength(); + if (keySizeBits <= 256) { + // 256-bit Elliptic Curve is roughly 128-bit strong, meaning SHA-256 is a good fit. + return Collections.singletonList(SignatureAlgorithm.ECDSA_WITH_SHA256); + } else { + // Keys longer than 256 bit need to be paired with a stronger digest to avoid the + // digest being the weak link. SHA-512 is the next strongest supported digest. + return Collections.singletonList(SignatureAlgorithm.ECDSA_WITH_SHA512); + } + } else { + throw new InvalidKeyException("Unsupported key algorithm: " + keyAlgorithm); + } + } + + /** + * Signs the provided APK using APK Signature Scheme v2 and returns the APK Signing Block + * containing the signature. + * + * @param signerConfigs signer configurations, one for each signer At least one signer config + * must be provided. + * + * @throws IOException if an I/O error occurs + * @throws InvalidKeyException if a signing key is not suitable for this signature scheme or + * cannot be used in general + * @throws SignatureException if an error occurs when computing digests of generating + * signatures + */ + public static byte[] generateApkSigningBlock( + DataSource beforeCentralDir, + DataSource centralDir, + DataSource eocd, + List signerConfigs) + throws IOException, InvalidKeyException, SignatureException { + if (signerConfigs.isEmpty()) { + throw new IllegalArgumentException( + "No signer configs provided. At least one is required"); + } + + // Figure out which digest(s) to use for APK contents. + Set contentDigestAlgorithms = new HashSet<>(1); + for (SignerConfig signerConfig : signerConfigs) { + for (SignatureAlgorithm signatureAlgorithm : signerConfig.signatureAlgorithms) { + contentDigestAlgorithms.add(signatureAlgorithm.getContentDigestAlgorithm()); + } + } + + // Ensure that, when digesting, ZIP End of Central Directory record's Central Directory + // offset field is treated as pointing to the offset at which the APK Signing Block will + // start. + long centralDirOffsetForDigesting = beforeCentralDir.size(); + ByteBuffer eocdBuf = ByteBuffer.allocate((int) eocd.size()); + eocdBuf.order(ByteOrder.LITTLE_ENDIAN); + eocd.copyTo(0, (int) eocd.size(), eocdBuf); + eocdBuf.flip(); + ZipUtils.setZipEocdCentralDirectoryOffset(eocdBuf, centralDirOffsetForDigesting); + + // Compute digests of APK contents. + Map contentDigests; // digest algorithm ID -> digest + try { + contentDigests = + computeContentDigests( + contentDigestAlgorithms, + new DataSource[] { + beforeCentralDir, + centralDir, + DataSources.asDataSource(eocdBuf)}); + } catch (IOException e) { + throw new IOException("Failed to read APK being signed", e); + } catch (DigestException e) { + throw new SignatureException("Failed to compute digests of APK", e); + } + + // Sign the digests and wrap the signatures and signer info into an APK Signing Block. + return generateApkSigningBlock(signerConfigs, contentDigests); + } + + static Map computeContentDigests( + Set digestAlgorithms, + DataSource[] contents) throws IOException, DigestException { + // For each digest algorithm the result is computed as follows: + // 1. Each segment of contents is split into consecutive chunks of 1 MB in size. + // The final chunk will be shorter iff the length of segment is not a multiple of 1 MB. + // No chunks are produced for empty (zero length) segments. + // 2. The digest of each chunk is computed over the concatenation of byte 0xa5, the chunk's + // length in bytes (uint32 little-endian) and the chunk's contents. + // 3. The output digest is computed over the concatenation of the byte 0x5a, the number of + // chunks (uint32 little-endian) and the concatenation of digests of chunks of all + // segments in-order. + + long chunkCountLong = 0; + for (DataSource input : contents) { + chunkCountLong += + getChunkCount(input.size(), CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES); + } + if (chunkCountLong > Integer.MAX_VALUE) { + throw new DigestException("Input too long: " + chunkCountLong + " chunks"); + } + int chunkCount = (int) chunkCountLong; + + ContentDigestAlgorithm[] digestAlgorithmsArray = + digestAlgorithms.toArray(new ContentDigestAlgorithm[digestAlgorithms.size()]); + MessageDigest[] mds = new MessageDigest[digestAlgorithmsArray.length]; + byte[][] digestsOfChunks = new byte[digestAlgorithmsArray.length][]; + int[] digestOutputSizes = new int[digestAlgorithmsArray.length]; + for (int i = 0; i < digestAlgorithmsArray.length; i++) { + ContentDigestAlgorithm digestAlgorithm = digestAlgorithmsArray[i]; + int digestOutputSizeBytes = digestAlgorithm.getChunkDigestOutputSizeBytes(); + digestOutputSizes[i] = digestOutputSizeBytes; + byte[] concatenationOfChunkCountAndChunkDigests = + new byte[5 + chunkCount * digestOutputSizeBytes]; + concatenationOfChunkCountAndChunkDigests[0] = 0x5a; + setUnsignedInt32LittleEndian( + chunkCount, concatenationOfChunkCountAndChunkDigests, 1); + digestsOfChunks[i] = concatenationOfChunkCountAndChunkDigests; + String jcaAlgorithm = digestAlgorithm.getJcaMessageDigestAlgorithm(); + try { + mds[i] = MessageDigest.getInstance(jcaAlgorithm); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(jcaAlgorithm + " MessageDigest not supported", e); + } + } + + MessageDigestSink mdSink = new MessageDigestSink(mds); + byte[] chunkContentPrefix = new byte[5]; + chunkContentPrefix[0] = (byte) 0xa5; + int chunkIndex = 0; + // Optimization opportunity: digests of chunks can be computed in parallel. However, + // determining the number of computations to be performed in parallel is non-trivial. This + // depends on a wide range of factors, such as data source type (e.g., in-memory or fetched + // from file), CPU/memory/disk cache bandwidth and latency, interconnect architecture of CPU + // cores, load on the system from other threads of execution and other processes, size of + // input. + // For now, we compute these digests sequentially and thus have the luxury of improving + // performance by writing the digest of each chunk into a pre-allocated buffer at exactly + // the right position. This avoids unnecessary allocations, copying, and enables the final + // digest to be more efficient because it's presented with all of its input in one go. + for (DataSource input : contents) { + long inputOffset = 0; + long inputRemaining = input.size(); + while (inputRemaining > 0) { + int chunkSize = + (int) Math.min(inputRemaining, CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES); + setUnsignedInt32LittleEndian(chunkSize, chunkContentPrefix, 1); + for (int i = 0; i < mds.length; i++) { + mds[i].update(chunkContentPrefix); + } + try { + input.feed(inputOffset, chunkSize, mdSink); + } catch (IOException e) { + throw new IOException("Failed to read chunk #" + chunkIndex, e); + } + for (int i = 0; i < digestAlgorithmsArray.length; i++) { + MessageDigest md = mds[i]; + byte[] concatenationOfChunkCountAndChunkDigests = digestsOfChunks[i]; + int expectedDigestSizeBytes = digestOutputSizes[i]; + int actualDigestSizeBytes = + md.digest( + concatenationOfChunkCountAndChunkDigests, + 5 + chunkIndex * expectedDigestSizeBytes, + expectedDigestSizeBytes); + if (actualDigestSizeBytes != expectedDigestSizeBytes) { + throw new RuntimeException( + "Unexpected output size of " + md.getAlgorithm() + + " digest: " + actualDigestSizeBytes); + } + } + inputOffset += chunkSize; + inputRemaining -= chunkSize; + chunkIndex++; + } + } + + Map result = new HashMap<>(digestAlgorithmsArray.length); + for (int i = 0; i < digestAlgorithmsArray.length; i++) { + ContentDigestAlgorithm digestAlgorithm = digestAlgorithmsArray[i]; + byte[] concatenationOfChunkCountAndChunkDigests = digestsOfChunks[i]; + MessageDigest md = mds[i]; + byte[] digest = md.digest(concatenationOfChunkCountAndChunkDigests); + result.put(digestAlgorithm, digest); + } + return result; + } + + private static final long getChunkCount(long inputSize, int chunkSize) { + return (inputSize + chunkSize - 1) / chunkSize; + } + + private static void setUnsignedInt32LittleEndian(int value, byte[] result, int offset) { + result[offset] = (byte) (value & 0xff); + result[offset + 1] = (byte) ((value >> 8) & 0xff); + result[offset + 2] = (byte) ((value >> 16) & 0xff); + result[offset + 3] = (byte) ((value >> 24) & 0xff); + } + + private static byte[] generateApkSigningBlock( + List signerConfigs, + Map contentDigests) + throws InvalidKeyException, SignatureException { + byte[] apkSignatureSchemeV2Block = + generateApkSignatureSchemeV2Block(signerConfigs, contentDigests); + return generateApkSigningBlock(apkSignatureSchemeV2Block); + } + + private static byte[] generateApkSigningBlock(byte[] apkSignatureSchemeV2Block) { + // FORMAT: + // uint64: size (excluding this field) + // repeated ID-value pairs: + // uint64: size (excluding this field) + // uint32: ID + // (size - 4) bytes: value + // uint64: size (same as the one above) + // uint128: magic + + int resultSize = + 8 // size + + 8 + 4 + apkSignatureSchemeV2Block.length // v2Block as ID-value pair + + 8 // size + + 16 // magic + ; + ByteBuffer result = ByteBuffer.allocate(resultSize); + result.order(ByteOrder.LITTLE_ENDIAN); + long blockSizeFieldValue = resultSize - 8; + result.putLong(blockSizeFieldValue); + + long pairSizeFieldValue = 4 + apkSignatureSchemeV2Block.length; + result.putLong(pairSizeFieldValue); + result.putInt(APK_SIGNATURE_SCHEME_V2_BLOCK_ID); + result.put(apkSignatureSchemeV2Block); + + result.putLong(blockSizeFieldValue); + result.put(APK_SIGNING_BLOCK_MAGIC); + + return result.array(); + } + + private static byte[] generateApkSignatureSchemeV2Block( + List signerConfigs, + Map contentDigests) + throws InvalidKeyException, SignatureException { + // FORMAT: + // * length-prefixed sequence of length-prefixed signer blocks. + + List signerBlocks = new ArrayList<>(signerConfigs.size()); + int signerNumber = 0; + for (SignerConfig signerConfig : signerConfigs) { + signerNumber++; + byte[] signerBlock; + try { + signerBlock = generateSignerBlock(signerConfig, contentDigests); + } catch (InvalidKeyException e) { + throw new InvalidKeyException("Signer #" + signerNumber + " failed", e); + } catch (SignatureException e) { + throw new SignatureException("Signer #" + signerNumber + " failed", e); + } + signerBlocks.add(signerBlock); + } + + return encodeAsSequenceOfLengthPrefixedElements( + new byte[][] { + encodeAsSequenceOfLengthPrefixedElements(signerBlocks), + }); + } + + private static byte[] generateSignerBlock( + SignerConfig signerConfig, + Map contentDigests) + throws InvalidKeyException, SignatureException { + if (signerConfig.certificates.isEmpty()) { + throw new SignatureException("No certificates configured for signer"); + } + PublicKey publicKey = signerConfig.certificates.get(0).getPublicKey(); + + byte[] encodedPublicKey = encodePublicKey(publicKey); + + V2SignatureSchemeBlock.SignedData signedData = new V2SignatureSchemeBlock.SignedData(); + try { + signedData.certificates = encodeCertificates(signerConfig.certificates); + } catch (CertificateEncodingException e) { + throw new SignatureException("Failed to encode certificates", e); + } + + List> digests = + new ArrayList<>(signerConfig.signatureAlgorithms.size()); + for (SignatureAlgorithm signatureAlgorithm : signerConfig.signatureAlgorithms) { + ContentDigestAlgorithm contentDigestAlgorithm = + signatureAlgorithm.getContentDigestAlgorithm(); + byte[] contentDigest = contentDigests.get(contentDigestAlgorithm); + if (contentDigest == null) { + throw new RuntimeException( + contentDigestAlgorithm + " content digest for " + signatureAlgorithm + + " not computed"); + } + digests.add(Pair.of(signatureAlgorithm.getId(), contentDigest)); + } + signedData.digests = digests; + + V2SignatureSchemeBlock.Signer signer = new V2SignatureSchemeBlock.Signer(); + // FORMAT: + // * length-prefixed sequence of length-prefixed digests: + // * uint32: signature algorithm ID + // * length-prefixed bytes: digest of contents + // * length-prefixed sequence of certificates: + // * length-prefixed bytes: X.509 certificate (ASN.1 DER encoded). + // * length-prefixed sequence of length-prefixed additional attributes: + // * uint32: ID + // * (length - 4) bytes: value + signer.signedData = encodeAsSequenceOfLengthPrefixedElements(new byte[][] { + encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(signedData.digests), + encodeAsSequenceOfLengthPrefixedElements(signedData.certificates), + // additional attributes + new byte[0], + }); + signer.publicKey = encodedPublicKey; + signer.signatures = new ArrayList<>(signerConfig.signatureAlgorithms.size()); + for (SignatureAlgorithm signatureAlgorithm : signerConfig.signatureAlgorithms) { + Pair sigAlgAndParams = + signatureAlgorithm.getJcaSignatureAlgorithmAndParams(); + String jcaSignatureAlgorithm = sigAlgAndParams.getFirst(); + AlgorithmParameterSpec jcaSignatureAlgorithmParams = sigAlgAndParams.getSecond(); + byte[] signatureBytes; + try { + Signature signature = Signature.getInstance(jcaSignatureAlgorithm); + signature.initSign(signerConfig.privateKey); + if (jcaSignatureAlgorithmParams != null) { + signature.setParameter(jcaSignatureAlgorithmParams); + } + signature.update(signer.signedData); + signatureBytes = signature.sign(); + } catch (InvalidKeyException e) { + throw new InvalidKeyException("Failed sign using " + jcaSignatureAlgorithm, e); + } catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException + | SignatureException e) { + throw new SignatureException("Failed sign using " + jcaSignatureAlgorithm, e); + } + + try { + Signature signature = Signature.getInstance(jcaSignatureAlgorithm); + signature.initVerify(publicKey); + if (jcaSignatureAlgorithmParams != null) { + signature.setParameter(jcaSignatureAlgorithmParams); + } + signature.update(signer.signedData); + if (!signature.verify(signatureBytes)) { + throw new SignatureException("Signature did not verify"); + } + } catch (InvalidKeyException e) { + throw new InvalidKeyException("Failed to verify generated " + jcaSignatureAlgorithm + + " signature using public key from certificate", e); + } catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException + | SignatureException e) { + throw new SignatureException("Failed to verify generated " + jcaSignatureAlgorithm + + " signature using public key from certificate", e); + } + + signer.signatures.add(Pair.of(signatureAlgorithm.getId(), signatureBytes)); + } + + // FORMAT: + // * length-prefixed signed data + // * length-prefixed sequence of length-prefixed signatures: + // * uint32: signature algorithm ID + // * length-prefixed bytes: signature of signed data + // * length-prefixed bytes: public key (X.509 SubjectPublicKeyInfo, ASN.1 DER encoded) + return encodeAsSequenceOfLengthPrefixedElements( + new byte[][] { + signer.signedData, + encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes( + signer.signatures), + signer.publicKey, + }); + } + + private static final class V2SignatureSchemeBlock { + private static final class Signer { + public byte[] signedData; + public List> signatures; + public byte[] publicKey; + } + + private static final class SignedData { + public List> digests; + public List certificates; + } + } + + private static byte[] encodePublicKey(PublicKey publicKey) throws InvalidKeyException { + byte[] encodedPublicKey = null; + if ("X.509".equals(publicKey.getFormat())) { + encodedPublicKey = publicKey.getEncoded(); + } + if (encodedPublicKey == null) { + try { + encodedPublicKey = + KeyFactory.getInstance(publicKey.getAlgorithm()) + .getKeySpec(publicKey, X509EncodedKeySpec.class) + .getEncoded(); + } catch (NoSuchAlgorithmException e) { + throw new InvalidKeyException( + "Failed to obtain X.509 encoded form of public key " + publicKey + + " of class " + publicKey.getClass().getName(), + e); + } catch (InvalidKeySpecException e) { + throw new InvalidKeyException( + "Failed to obtain X.509 encoded form of public key " + publicKey + + " of class " + publicKey.getClass().getName(), + e); + } + } + if ((encodedPublicKey == null) || (encodedPublicKey.length == 0)) { + throw new InvalidKeyException( + "Failed to obtain X.509 encoded form of public key " + publicKey + + " of class " + publicKey.getClass().getName()); + } + return encodedPublicKey; + } + + private static List encodeCertificates(List certificates) + throws CertificateEncodingException { + List result = new ArrayList<>(certificates.size()); + for (X509Certificate certificate : certificates) { + result.add(certificate.getEncoded()); + } + return result; + } + + private static byte[] encodeAsSequenceOfLengthPrefixedElements(List sequence) { + return encodeAsSequenceOfLengthPrefixedElements( + sequence.toArray(new byte[sequence.size()][])); + } + + private static byte[] encodeAsSequenceOfLengthPrefixedElements(byte[][] sequence) { + int payloadSize = 0; + for (byte[] element : sequence) { + payloadSize += 4 + element.length; + } + ByteBuffer result = ByteBuffer.allocate(payloadSize); + result.order(ByteOrder.LITTLE_ENDIAN); + for (byte[] element : sequence) { + result.putInt(element.length); + result.put(element); + } + return result.array(); + } + + private static byte[] encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes( + List> sequence) { + int resultSize = 0; + for (Pair element : sequence) { + resultSize += 12 + element.getSecond().length; + } + ByteBuffer result = ByteBuffer.allocate(resultSize); + result.order(ByteOrder.LITTLE_ENDIAN); + for (Pair element : sequence) { + byte[] second = element.getSecond(); + result.putInt(8 + second.length); + result.putInt(element.getFirst()); + result.putInt(second.length); + result.put(second); + } + return result.array(); + } +} diff --git a/plugin/src/main/java/com/android/apksigner/core/internal/apk/v2/V2SchemeVerifier.java b/plugin/src/main/java/com/android/apksigner/core/internal/apk/v2/V2SchemeVerifier.java new file mode 100644 index 0000000..509752e --- /dev/null +++ b/plugin/src/main/java/com/android/apksigner/core/internal/apk/v2/V2SchemeVerifier.java @@ -0,0 +1,939 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksigner.core.internal.apk.v2; + +import com.android.apksigner.core.ApkVerifier.Issue; +import com.android.apksigner.core.ApkVerifier.IssueWithParams; +import com.android.apksigner.core.apk.ApkUtils; +import com.android.apksigner.core.internal.util.ByteBufferDataSource; +import com.android.apksigner.core.internal.util.DelegatingX509Certificate; +import com.android.apksigner.core.internal.util.Pair; +import com.android.apksigner.core.internal.zip.ZipUtils; +import com.android.apksigner.core.util.DataSource; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.security.DigestException; +import java.security.KeyFactory; +import java.security.PublicKey; +import java.security.Signature; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.security.spec.AlgorithmParameterSpec; +import java.security.spec.X509EncodedKeySpec; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * APK Signature Scheme v2 verifier. + * + *

APK Signature Scheme v2 is a whole-file signature scheme which aims to protect every single + * bit of the APK, as opposed to the JAR Signature Scheme which protects only the names and + * uncompressed contents of ZIP entries. + * + *

TODO: Link to APK Signature Scheme v2 documentation once it's available. + */ +public abstract class V2SchemeVerifier { + + public static final long APK_SIG_BLOCK_MAGIC_HI = 0x3234206b636f6c42L; + public static final long APK_SIG_BLOCK_MAGIC_LO = 0x20676953204b5041L; + private static final int APK_SIG_BLOCK_MIN_SIZE = 32; + + private static final int APK_SIGNATURE_SCHEME_V2_BLOCK_ID = 0x7109871a; + + /** Hidden constructor to prevent instantiation. */ + private V2SchemeVerifier() {} + + /** + * Verifies the provided APK's APK Signature Scheme v2 signatures and returns the result of + * verification. APK is considered verified only if {@link Result#verified} is {@code true}. If + * verification fails, the result will contain errors -- see {@link Result#getErrors()}. + * + * @throws SignatureNotFoundException if no APK Signature Scheme v2 signatures are found + * @throws IOException if an I/O error occurs when reading the APK + */ + public static Result verify(DataSource apk, ApkUtils.ZipSections zipSections) + throws IOException, SignatureNotFoundException { + Result result = new Result(); + SignatureInfo signatureInfo = findSignature(apk, zipSections, result); + + DataSource beforeApkSigningBlock = apk.slice(0, signatureInfo.apkSigningBlockOffset); + DataSource centralDir = + apk.slice( + signatureInfo.centralDirOffset, + signatureInfo.eocdOffset - signatureInfo.centralDirOffset); + ByteBuffer eocd = signatureInfo.eocd; + + verify(beforeApkSigningBlock, + signatureInfo.signatureBlock, + centralDir, + eocd, + result); + return result; + } + + /** + * Verifies the provided APK's v2 signatures and outputs the results into the provided + * {@code result}. APK is considered verified only if there are no errors reported in the + * {@code result}. + */ + private static void verify( + DataSource beforeApkSigningBlock, + ByteBuffer apkSignatureSchemeV2Block, + DataSource centralDir, + ByteBuffer eocd, + Result result) throws IOException { + Set contentDigestsToVerify = new HashSet<>(1); + parseSigners(apkSignatureSchemeV2Block, contentDigestsToVerify, result); + if (result.containsErrors()) { + return; + } + verifyIntegrity( + beforeApkSigningBlock, centralDir, eocd, contentDigestsToVerify, result); + if (!result.containsErrors()) { + result.verified = true; + } + } + + /** + * Parses each signer in the provided APK Signature Scheme v2 block and populates + * {@code signerInfos} of the provided {@code result}. + * + *

This verifies signatures over {@code signed-data} block contained in each signer block. + * However, this does not verify the integrity of the rest of the APK but rather simply reports + * the expected digests of the rest of the APK (see {@code contentDigestsToVerify}). + */ + private static void parseSigners( + ByteBuffer apkSignatureSchemeV2Block, + Set contentDigestsToVerify, + Result result) { + ByteBuffer signers; + try { + signers = getLengthPrefixedSlice(apkSignatureSchemeV2Block); + } catch (IOException e) { + result.addError(Issue.V2_SIG_MALFORMED_SIGNERS); + return; + } + if (!signers.hasRemaining()) { + result.addError(Issue.V2_SIG_NO_SIGNERS); + return; + } + + CertificateFactory certFactory; + try { + certFactory = CertificateFactory.getInstance("X.509"); + } catch (CertificateException e) { + throw new RuntimeException("Failed to obtain X.509 CertificateFactory", e); + } + int signerCount = 0; + while (signers.hasRemaining()) { + int signerIndex = signerCount; + signerCount++; + Result.SignerInfo signerInfo = new Result.SignerInfo(); + signerInfo.index = signerIndex; + result.signers.add(signerInfo); + try { + ByteBuffer signer = getLengthPrefixedSlice(signers); + parseSigner(signer, certFactory, signerInfo, contentDigestsToVerify); + } catch (IOException | BufferUnderflowException e) { + signerInfo.addError(Issue.V2_SIG_MALFORMED_SIGNER); + return; + } + } + } + + /** + * Parses the provided signer block and populates the {@code result}. + * + *

This verifies signatures over {@code signed-data} contained in this block but does not + * verify the integrity of the rest of the APK. Rather, this method adds to the + * {@code contentDigestsToVerify}. + */ + private static void parseSigner( + ByteBuffer signerBlock, + CertificateFactory certFactory, + Result.SignerInfo result, + Set contentDigestsToVerify) throws IOException { + ByteBuffer signedData = getLengthPrefixedSlice(signerBlock); + byte[] signedDataBytes = new byte[signedData.remaining()]; + signedData.get(signedDataBytes); + signedData.flip(); + result.signedData = signedDataBytes; + + ByteBuffer signatures = getLengthPrefixedSlice(signerBlock); + byte[] publicKeyBytes = readLengthPrefixedByteArray(signerBlock); + + // Parse the signatures block and identify supported signatures + int signatureCount = 0; + List supportedSignatures = new ArrayList<>(1); + while (signatures.hasRemaining()) { + signatureCount++; + try { + ByteBuffer signature = getLengthPrefixedSlice(signatures); + int sigAlgorithmId = signature.getInt(); + byte[] sigBytes = readLengthPrefixedByteArray(signature); + result.signatures.add( + new Result.SignerInfo.Signature(sigAlgorithmId, sigBytes)); + SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.findById(sigAlgorithmId); + if (signatureAlgorithm == null) { + result.addWarning(Issue.V2_SIG_UNKNOWN_SIG_ALGORITHM, sigAlgorithmId); + continue; + } + supportedSignatures.add(new SupportedSignature(signatureAlgorithm, sigBytes)); + } catch (IOException | BufferUnderflowException e) { + result.addError(Issue.V2_SIG_MALFORMED_SIGNATURE, signatureCount); + return; + } + } + if (result.signatures.isEmpty()) { + result.addError(Issue.V2_SIG_NO_SIGNATURES); + return; + } + + // Verify signatures over signed-data block using the public key + List signaturesToVerify = getSignaturesToVerify(supportedSignatures); + if (signaturesToVerify.isEmpty()) { + result.addError(Issue.V2_SIG_NO_SUPPORTED_SIGNATURES); + return; + } + for (SupportedSignature signature : signaturesToVerify) { + SignatureAlgorithm signatureAlgorithm = signature.algorithm; + String jcaSignatureAlgorithm = + signatureAlgorithm.getJcaSignatureAlgorithmAndParams().getFirst(); + AlgorithmParameterSpec jcaSignatureAlgorithmParams = + signatureAlgorithm.getJcaSignatureAlgorithmAndParams().getSecond(); + String keyAlgorithm = signatureAlgorithm.getJcaKeyAlgorithm(); + PublicKey publicKey; + try { + publicKey = + KeyFactory.getInstance(keyAlgorithm).generatePublic( + new X509EncodedKeySpec(publicKeyBytes)); + } catch (Exception e) { + result.addError(Issue.V2_SIG_MALFORMED_PUBLIC_KEY, e); + return; + } + try { + Signature sig = Signature.getInstance(jcaSignatureAlgorithm); + sig.initVerify(publicKey); + if (jcaSignatureAlgorithmParams != null) { + sig.setParameter(jcaSignatureAlgorithmParams); + } + signedData.position(0); + sig.update(signedData); + byte[] sigBytes = signature.signature; + if (!sig.verify(sigBytes)) { + result.addError(Issue.V2_SIG_DID_NOT_VERIFY, signatureAlgorithm); + return; + } + result.verifiedSignatures.put(signatureAlgorithm, sigBytes); + contentDigestsToVerify.add(signatureAlgorithm.getContentDigestAlgorithm()); + } catch (Exception e) { + result.addError(Issue.V2_SIG_VERIFY_EXCEPTION, signatureAlgorithm, e); + return; + } + } + + // At least one signature over signedData has verified. We can now parse signed-data. + signedData.position(0); + ByteBuffer digests = getLengthPrefixedSlice(signedData); + ByteBuffer certificates = getLengthPrefixedSlice(signedData); + ByteBuffer additionalAttributes = getLengthPrefixedSlice(signedData); + + // Parse the certificates block + int certificateIndex = -1; + while (certificates.hasRemaining()) { + certificateIndex++; + byte[] encodedCert = readLengthPrefixedByteArray(certificates); + X509Certificate certificate; + try { + certificate = + (X509Certificate) + certFactory.generateCertificate( + new ByteArrayInputStream(encodedCert)); + } catch (CertificateException e) { + result.addError( + Issue.V2_SIG_MALFORMED_CERTIFICATE, + certificateIndex, + certificateIndex + 1, + e); + return; + } + // Wrap the cert so that the result's getEncoded returns exactly the original encoded + // form. Without this, getEncoded may return a different form from what was stored in + // the signature. This is becase some X509Certificate(Factory) implementations re-encode + // certificates. + certificate = new GuaranteedEncodedFormX509Certificate(certificate, encodedCert); + result.certs.add(certificate); + } + + if (result.certs.isEmpty()) { + result.addError(Issue.V2_SIG_NO_CERTIFICATES); + return; + } + X509Certificate mainCertificate = result.certs.get(0); + byte[] certificatePublicKeyBytes = mainCertificate.getPublicKey().getEncoded(); + if (!Arrays.equals(publicKeyBytes, certificatePublicKeyBytes)) { + result.addError( + Issue.V2_SIG_PUBLIC_KEY_MISMATCH_BETWEEN_CERTIFICATE_AND_SIGNATURES_RECORD, + toHex(certificatePublicKeyBytes), + toHex(publicKeyBytes)); + return; + } + + // Parse the digests block + int digestCount = 0; + while (digests.hasRemaining()) { + digestCount++; + try { + ByteBuffer digest = getLengthPrefixedSlice(digests); + int sigAlgorithmId = digest.getInt(); + byte[] digestBytes = readLengthPrefixedByteArray(digest); + result.contentDigests.add( + new Result.SignerInfo.ContentDigest(sigAlgorithmId, digestBytes)); + } catch (IOException | BufferUnderflowException e) { + result.addError(Issue.V2_SIG_MALFORMED_DIGEST, digestCount); + return; + } + } + + List sigAlgsFromSignaturesRecord = new ArrayList<>(result.signatures.size()); + for (Result.SignerInfo.Signature signature : result.signatures) { + sigAlgsFromSignaturesRecord.add(signature.getAlgorithmId()); + } + List sigAlgsFromDigestsRecord = new ArrayList<>(result.contentDigests.size()); + for (Result.SignerInfo.ContentDigest digest : result.contentDigests) { + sigAlgsFromDigestsRecord.add(digest.getSignatureAlgorithmId()); + } + + if (!sigAlgsFromSignaturesRecord.equals(sigAlgsFromDigestsRecord)) { + result.addError( + Issue.V2_SIG_SIG_ALG_MISMATCH_BETWEEN_SIGNATURES_AND_DIGESTS_RECORDS, + sigAlgsFromSignaturesRecord, + sigAlgsFromDigestsRecord); + return; + } + + // Parse the additional attributes block. + int additionalAttributeCount = 0; + while (additionalAttributes.hasRemaining()) { + additionalAttributeCount++; + try { + ByteBuffer attribute = getLengthPrefixedSlice(additionalAttributes); + int id = attribute.getInt(); + byte[] value = readLengthPrefixedByteArray(attribute); + result.additionalAttributes.add( + new Result.SignerInfo.AdditionalAttribute(id, value)); + result.addWarning(Issue.V2_SIG_UNKNOWN_ADDITIONAL_ATTRIBUTE, id); + } catch (IOException | BufferUnderflowException e) { + result.addError( + Issue.V2_SIG_MALFORMED_ADDITIONAL_ATTRIBUTE, additionalAttributeCount); + return; + } + } + } + + private static List getSignaturesToVerify( + List signatures) { + // Pick the signature with the strongest algorithm, to mimic Android's behavior. + SignatureAlgorithm bestSigAlgorithm = null; + byte[] bestSigAlgorithmSignatureBytes = null; + for (SupportedSignature sig : signatures) { + SignatureAlgorithm sigAlgorithm = sig.algorithm; + if ((bestSigAlgorithm == null) + || (compareSignatureAlgorithm(sigAlgorithm, bestSigAlgorithm) > 0)) { + bestSigAlgorithm = sigAlgorithm; + bestSigAlgorithmSignatureBytes = sig.signature; + } + } + + if (bestSigAlgorithm == null) { + return Collections.emptyList(); + } else { + return Collections.singletonList( + new SupportedSignature(bestSigAlgorithm, bestSigAlgorithmSignatureBytes)); + } + } + + private static class SupportedSignature { + private final SignatureAlgorithm algorithm; + private final byte[] signature; + + private SupportedSignature(SignatureAlgorithm algorithm, byte[] signature) { + this.algorithm = algorithm; + this.signature = signature; + } + } + + /** + * Returns positive number if {@code alg1} is preferred over {@code alg2}, {@code -1} if + * {@code alg2} is preferred over {@code alg1}, and {@code 0} if there is no preference. + */ + private static int compareSignatureAlgorithm(SignatureAlgorithm alg1, SignatureAlgorithm alg2) { + ContentDigestAlgorithm digestAlg1 = alg1.getContentDigestAlgorithm(); + ContentDigestAlgorithm digestAlg2 = alg2.getContentDigestAlgorithm(); + return compareContentDigestAlgorithm(digestAlg1, digestAlg2); + } + + /** + * Returns positive number if {@code alg1} is preferred over {@code alg2}, {@code -1} if + * {@code alg2} is preferred over {@code alg1}, and {@code 0} if there is no preference. + */ + private static int compareContentDigestAlgorithm( + ContentDigestAlgorithm alg1, + ContentDigestAlgorithm alg2) { + switch (alg1) { + case CHUNKED_SHA256: + switch (alg2) { + case CHUNKED_SHA256: + return 0; + case CHUNKED_SHA512: + return -1; + default: + throw new IllegalArgumentException("Unknown alg2: " + alg2); + } + case CHUNKED_SHA512: + switch (alg2) { + case CHUNKED_SHA256: + return 1; + case CHUNKED_SHA512: + return 0; + default: + throw new IllegalArgumentException("Unknown alg2: " + alg2); + } + default: + throw new IllegalArgumentException("Unknown alg1: " + alg1); + } + } + + /** + * Verifies integrity of the APK outside of the APK Signing Block by computing digests of the + * APK and comparing them against the digests listed in APK Signing Block. The expected digests + * taken from {@code v2SchemeSignerInfos} of the provided {@code result}. + */ + private static void verifyIntegrity( + DataSource beforeApkSigningBlock, + DataSource centralDir, + ByteBuffer eocd, + Set contentDigestAlgorithms, + Result result) throws IOException { + if (contentDigestAlgorithms.isEmpty()) { + // This should never occur because this method is invoked once at least one signature + // is verified, meaning at least one content digest is known. + throw new RuntimeException("No content digests found"); + } + + // For the purposes of verifying integrity, ZIP End of Central Directory (EoCD) must be + // treated as though its Central Directory offset points to the start of APK Signing Block. + // We thus modify the EoCD accordingly. + ByteBuffer modifiedEocd = ByteBuffer.allocate(eocd.remaining()); + modifiedEocd.order(ByteOrder.LITTLE_ENDIAN); + modifiedEocd.put(eocd); + modifiedEocd.flip(); + ZipUtils.setZipEocdCentralDirectoryOffset(modifiedEocd, beforeApkSigningBlock.size()); + Map actualContentDigests; + try { + actualContentDigests = + V2SchemeSigner.computeContentDigests( + contentDigestAlgorithms, + new DataSource[] { + beforeApkSigningBlock, + centralDir, + new ByteBufferDataSource(modifiedEocd) + }); + } catch (DigestException e) { + throw new RuntimeException("Failed to compute content digests", e); + } + if (!contentDigestAlgorithms.equals(actualContentDigests.keySet())) { + throw new RuntimeException( + "Mismatch between sets of requested and computed content digests" + + " . Requested: " + contentDigestAlgorithms + + ", computed: " + actualContentDigests.keySet()); + } + + // Compare digests computed over the rest of APK against the corresponding expected digests + // in signer blocks. + for (Result.SignerInfo signerInfo : result.signers) { + for (Result.SignerInfo.ContentDigest expected : signerInfo.contentDigests) { + SignatureAlgorithm signatureAlgorithm = + SignatureAlgorithm.findById(expected.getSignatureAlgorithmId()); + if (signatureAlgorithm == null) { + continue; + } + ContentDigestAlgorithm contentDigestAlgorithm = + signatureAlgorithm.getContentDigestAlgorithm(); + byte[] expectedDigest = expected.getValue(); + byte[] actualDigest = actualContentDigests.get(contentDigestAlgorithm); + if (!Arrays.equals(expectedDigest, actualDigest)) { + signerInfo.addError( + Issue.V2_SIG_APK_DIGEST_DID_NOT_VERIFY, + contentDigestAlgorithm, + toHex(expectedDigest), + toHex(actualDigest)); + continue; + } + signerInfo.verifiedContentDigests.put(contentDigestAlgorithm, actualDigest); + } + } + } + + /** + * APK Signature Scheme v2 block and additional information relevant to verifying the signatures + * contained in the block against the file. + */ + private static class SignatureInfo { + /** Contents of APK Signature Scheme v2 block. */ + private final ByteBuffer signatureBlock; + + /** Position of the APK Signing Block in the file. */ + private final long apkSigningBlockOffset; + + /** Position of the ZIP Central Directory in the file. */ + private final long centralDirOffset; + + /** Position of the ZIP End of Central Directory (EoCD) in the file. */ + private final long eocdOffset; + + /** Contents of ZIP End of Central Directory (EoCD) of the file. */ + private final ByteBuffer eocd; + + private SignatureInfo( + ByteBuffer signatureBlock, + long apkSigningBlockOffset, + long centralDirOffset, + long eocdOffset, + ByteBuffer eocd) { + this.signatureBlock = signatureBlock; + this.apkSigningBlockOffset = apkSigningBlockOffset; + this.centralDirOffset = centralDirOffset; + this.eocdOffset = eocdOffset; + this.eocd = eocd; + } + } + + /** + * Returns the APK Signature Scheme v2 block contained in the provided APK file and the + * additional information relevant for verifying the block against the file. + * + * @throws SignatureNotFoundException if the APK is not signed using APK Signature Scheme v2 + * @throws IOException if an I/O error occurs while reading the APK + */ + private static SignatureInfo findSignature( + DataSource apk, ApkUtils.ZipSections zipSections, Result result) + throws IOException, SignatureNotFoundException { + long centralDirStartOffset = zipSections.getZipCentralDirectoryOffset(); + long centralDirEndOffset = + centralDirStartOffset + zipSections.getZipCentralDirectorySizeBytes(); + long eocdStartOffset = zipSections.getZipEndOfCentralDirectoryOffset(); + if (centralDirEndOffset != eocdStartOffset) { + throw new SignatureNotFoundException( + "ZIP Central Directory is not immediately followed by End of Central Directory" + + ". CD end: " + centralDirEndOffset + + ", EoCD start: " + eocdStartOffset); + } + + // Find the APK Signing Block. The block immediately precedes the Central Directory. + ByteBuffer eocd = zipSections.getZipEndOfCentralDirectory(); + Pair apkSigningBlockAndOffset = + findApkSigningBlock(apk, centralDirStartOffset); + ByteBuffer apkSigningBlock = apkSigningBlockAndOffset.getFirst(); + long apkSigningBlockOffset = apkSigningBlockAndOffset.getSecond(); + + // Find the APK Signature Scheme v2 Block inside the APK Signing Block. + ByteBuffer apkSignatureSchemeV2Block = + findApkSignatureSchemeV2Block(apkSigningBlock, result); + + return new SignatureInfo( + apkSignatureSchemeV2Block, + apkSigningBlockOffset, + centralDirStartOffset, + eocdStartOffset, + eocd); + } + + public static Pair findApkSigningBlock( + DataSource apk, long centralDirOffset) throws IOException, SignatureNotFoundException { + // FORMAT: + // OFFSET DATA TYPE DESCRIPTION + // * @+0 bytes uint64: size in bytes (excluding this field) + // * @+8 bytes payload + // * @-24 bytes uint64: size in bytes (same as the one above) + // * @-16 bytes uint128: magic + + if (centralDirOffset < APK_SIG_BLOCK_MIN_SIZE) { + throw new SignatureNotFoundException( + "APK too small for APK Signing Block. ZIP Central Directory offset: " + + centralDirOffset); + } + // Read the magic and offset in file from the footer section of the block: + // * uint64: size of block + // * 16 bytes: magic + ByteBuffer footer = apk.getByteBuffer(centralDirOffset - 24, 24); + footer.order(ByteOrder.LITTLE_ENDIAN); + if ((footer.getLong(8) != APK_SIG_BLOCK_MAGIC_LO) + || (footer.getLong(16) != APK_SIG_BLOCK_MAGIC_HI)) { + throw new SignatureNotFoundException( + "No APK Signing Block before ZIP Central Directory"); + } + // Read and compare size fields + long apkSigBlockSizeInFooter = footer.getLong(0); + if ((apkSigBlockSizeInFooter < footer.capacity()) + || (apkSigBlockSizeInFooter > Integer.MAX_VALUE - 8)) { + throw new SignatureNotFoundException( + "APK Signing Block size out of range: " + apkSigBlockSizeInFooter); + } + int totalSize = (int) (apkSigBlockSizeInFooter + 8); + long apkSigBlockOffset = centralDirOffset - totalSize; + if (apkSigBlockOffset < 0) { + throw new SignatureNotFoundException( + "APK Signing Block offset out of range: " + apkSigBlockOffset); + } + ByteBuffer apkSigBlock = apk.getByteBuffer(apkSigBlockOffset, totalSize); + apkSigBlock.order(ByteOrder.LITTLE_ENDIAN); + long apkSigBlockSizeInHeader = apkSigBlock.getLong(0); + if (apkSigBlockSizeInHeader != apkSigBlockSizeInFooter) { + throw new SignatureNotFoundException( + "APK Signing Block sizes in header and footer do not match: " + + apkSigBlockSizeInHeader + " vs " + apkSigBlockSizeInFooter); + } + return Pair.of(apkSigBlock, apkSigBlockOffset); + } + + public static ByteBuffer findApkSignatureSchemeV2Block( + ByteBuffer apkSigningBlock, + Result result) throws SignatureNotFoundException { + checkByteOrderLittleEndian(apkSigningBlock); + // FORMAT: + // OFFSET DATA TYPE DESCRIPTION + // * @+0 bytes uint64: size in bytes (excluding this field) + // * @+8 bytes pairs + // * @-24 bytes uint64: size in bytes (same as the one above) + // * @-16 bytes uint128: magic + ByteBuffer pairs = sliceFromTo(apkSigningBlock, 8, apkSigningBlock.capacity() - 24); + + int entryCount = 0; + while (pairs.hasRemaining()) { + entryCount++; + if (pairs.remaining() < 8) { + throw new SignatureNotFoundException( + "Insufficient data to read size of APK Signing Block entry #" + entryCount); + } + long lenLong = pairs.getLong(); + if ((lenLong < 4) || (lenLong > Integer.MAX_VALUE)) { + throw new SignatureNotFoundException( + "APK Signing Block entry #" + entryCount + + " size out of range: " + lenLong); + } + int len = (int) lenLong; + int nextEntryPos = pairs.position() + len; + if (len > pairs.remaining()) { + throw new SignatureNotFoundException( + "APK Signing Block entry #" + entryCount + " size out of range: " + len + + ", available: " + pairs.remaining()); + } + int id = pairs.getInt(); + if (id == APK_SIGNATURE_SCHEME_V2_BLOCK_ID) { + return getByteBuffer(pairs, len - 4); + } + result.addWarning(Issue.APK_SIG_BLOCK_UNKNOWN_ENTRY_ID, id); + pairs.position(nextEntryPos); + } + + throw new SignatureNotFoundException( + "No APK Signature Scheme v2 block in APK Signing Block"); + } + + private static void checkByteOrderLittleEndian(ByteBuffer buffer) { + if (buffer.order() != ByteOrder.LITTLE_ENDIAN) { + throw new IllegalArgumentException("ByteBuffer byte order must be little endian"); + } + } + + public static class SignatureNotFoundException extends Exception { + private static final long serialVersionUID = 1L; + + public SignatureNotFoundException(String message) { + super(message); + } + + public SignatureNotFoundException(String message, Throwable cause) { + super(message, cause); + } + } + + /** + * Returns new byte buffer whose content is a shared subsequence of this buffer's content + * between the specified start (inclusive) and end (exclusive) positions. As opposed to + * {@link ByteBuffer#slice()}, the returned buffer's byte order is the same as the source + * buffer's byte order. + */ + private static ByteBuffer sliceFromTo(ByteBuffer source, int start, int end) { + if (start < 0) { + throw new IllegalArgumentException("start: " + start); + } + if (end < start) { + throw new IllegalArgumentException("end < start: " + end + " < " + start); + } + int capacity = source.capacity(); + if (end > source.capacity()) { + throw new IllegalArgumentException("end > capacity: " + end + " > " + capacity); + } + int originalLimit = source.limit(); + int originalPosition = source.position(); + try { + source.position(0); + source.limit(end); + source.position(start); + ByteBuffer result = source.slice(); + result.order(source.order()); + return result; + } finally { + source.position(0); + source.limit(originalLimit); + source.position(originalPosition); + } + } + + /** + * Relative get method for reading {@code size} number of bytes from the current + * position of this buffer. + * + *

This method reads the next {@code size} bytes at this buffer's current position, + * returning them as a {@code ByteBuffer} with start set to 0, limit and capacity set to + * {@code size}, byte order set to this buffer's byte order; and then increments the position by + * {@code size}. + */ + public static ByteBuffer getByteBuffer(ByteBuffer source, int size) + throws BufferUnderflowException { + if (size < 0) { + throw new IllegalArgumentException("size: " + size); + } + int originalLimit = source.limit(); + int position = source.position(); + int limit = position + size; + if ((limit < position) || (limit > originalLimit)) { + throw new BufferUnderflowException(); + } + source.limit(limit); + try { + ByteBuffer result = source.slice(); + result.order(source.order()); + source.position(limit); + return result; + } finally { + source.limit(originalLimit); + } + } + + public static ByteBuffer getLengthPrefixedSlice(ByteBuffer source) throws IOException { + if (source.remaining() < 4) { + throw new IOException( + "Remaining buffer too short to contain length of length-prefixed field." + + " Remaining: " + source.remaining()); + } + int len = source.getInt(); + if (len < 0) { + throw new IllegalArgumentException("Negative length"); + } else if (len > source.remaining()) { + throw new IOException("Length-prefixed field longer than remaining buffer." + + " Field length: " + len + ", remaining: " + source.remaining()); + } + return getByteBuffer(source, len); + } + + public static byte[] readLengthPrefixedByteArray(ByteBuffer buf) throws IOException { + int len = buf.getInt(); + if (len < 0) { + throw new IOException("Negative length"); + } else if (len > buf.remaining()) { + throw new IOException("Underflow while reading length-prefixed value. Length: " + len + + ", available: " + buf.remaining()); + } + byte[] result = new byte[len]; + buf.get(result); + return result; + } + + /** + * {@link X509Certificate} whose {@link #getEncoded()} returns the data provided at construction + * time. + */ + public static class GuaranteedEncodedFormX509Certificate extends DelegatingX509Certificate { + private byte[] mEncodedForm; + + public GuaranteedEncodedFormX509Certificate(X509Certificate wrapped, byte[] encodedForm) { + super(wrapped); + this.mEncodedForm = (encodedForm != null) ? encodedForm.clone() : null; + } + + @Override + public byte[] getEncoded() throws CertificateEncodingException { + return (mEncodedForm != null) ? mEncodedForm.clone() : null; + } + } + + private static final char[] HEX_DIGITS = "01234567890abcdef".toCharArray(); + + private static String toHex(byte[] value) { + StringBuilder sb = new StringBuilder(value.length * 2); + int len = value.length; + for (int i = 0; i < len; i++) { + int hi = (value[i] & 0xff) >>> 4; + int lo = value[i] & 0x0f; + sb.append(HEX_DIGITS[hi]).append(HEX_DIGITS[lo]); + } + return sb.toString(); + } + + public static class Result { + + /** Whether the APK's APK Signature Scheme v2 signature verifies. */ + public boolean verified; + + public final List signers = new ArrayList<>(); + private final List mWarnings = new ArrayList<>(); + private final List mErrors = new ArrayList<>(); + + public boolean containsErrors() { + if (!mErrors.isEmpty()) { + return true; + } + if (!signers.isEmpty()) { + for (SignerInfo signer : signers) { + if (signer.containsErrors()) { + return true; + } + } + } + return false; + } + + public void addError(Issue msg, Object... parameters) { + mErrors.add(new IssueWithParams(msg, parameters)); + } + + public void addWarning(Issue msg, Object... parameters) { + mWarnings.add(new IssueWithParams(msg, parameters)); + } + + public List getErrors() { + return mErrors; + } + + public List getWarnings() { + return mWarnings; + } + + public static class SignerInfo { + public int index; + public List certs = new ArrayList<>(); + public List contentDigests = new ArrayList<>(); + public Map verifiedContentDigests = new HashMap<>(); + public List signatures = new ArrayList<>(); + public Map verifiedSignatures = new HashMap<>(); + public List additionalAttributes = new ArrayList<>(); + public byte[] signedData; + + private final List mWarnings = new ArrayList<>(); + private final List mErrors = new ArrayList<>(); + + public void addError(Issue msg, Object... parameters) { + mErrors.add(new IssueWithParams(msg, parameters)); + } + + public void addWarning(Issue msg, Object... parameters) { + mWarnings.add(new IssueWithParams(msg, parameters)); + } + + public boolean containsErrors() { + return !mErrors.isEmpty(); + } + + public List getErrors() { + return mErrors; + } + + public List getWarnings() { + return mWarnings; + } + + public static class ContentDigest { + private final int mSignatureAlgorithmId; + private final byte[] mValue; + + public ContentDigest(int signatureAlgorithmId, byte[] value) { + mSignatureAlgorithmId = signatureAlgorithmId; + mValue = value; + } + + public int getSignatureAlgorithmId() { + return mSignatureAlgorithmId; + } + + public byte[] getValue() { + return mValue; + } + } + + public static class Signature { + private final int mAlgorithmId; + private final byte[] mValue; + + public Signature(int algorithmId, byte[] value) { + mAlgorithmId = algorithmId; + mValue = value; + } + + public int getAlgorithmId() { + return mAlgorithmId; + } + + public byte[] getValue() { + return mValue; + } + } + + public static class AdditionalAttribute { + private final int mId; + private final byte[] mValue; + + public AdditionalAttribute(int id, byte[] value) { + mId = id; + mValue = value.clone(); + } + + public int getId() { + return mId; + } + + public byte[] getValue() { + return mValue.clone(); + } + } + } + } +} diff --git a/plugin/src/main/java/com/android/apksigner/core/internal/jar/ManifestWriter.java b/plugin/src/main/java/com/android/apksigner/core/internal/jar/ManifestWriter.java new file mode 100644 index 0000000..449953a --- /dev/null +++ b/plugin/src/main/java/com/android/apksigner/core/internal/jar/ManifestWriter.java @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksigner.core.internal.jar; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.Map; +import java.util.Set; +import java.util.SortedMap; +import java.util.TreeMap; +import java.util.jar.Attributes; + +/** + * Producer of {@code META-INF/MANIFEST.MF} file. + */ +public abstract class ManifestWriter { + + private static final byte[] CRLF = new byte[] {'\r', '\n'}; + private static final int MAX_LINE_LENGTH = 70; + + private ManifestWriter() {} + + public static void writeMainSection(OutputStream out, Attributes attributes) + throws IOException { + + // Main section must start with the Manifest-Version attribute. + // See https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Signed_JAR_File. + String manifestVersion = attributes.getValue(Attributes.Name.MANIFEST_VERSION); + if (manifestVersion == null) { + throw new IllegalArgumentException( + "Mandatory " + Attributes.Name.MANIFEST_VERSION + " attribute missing"); + } + writeAttribute(out, Attributes.Name.MANIFEST_VERSION, manifestVersion); + + if (attributes.size() > 1) { + SortedMap namedAttributes = getAttributesSortedByName(attributes); + namedAttributes.remove(Attributes.Name.MANIFEST_VERSION.toString()); + writeAttributes(out, namedAttributes); + } + writeSectionDelimiter(out); + } + + public static void writeIndividualSection(OutputStream out, String name, Attributes attributes) + throws IOException { + writeAttribute(out, "Name", name); + + if (!attributes.isEmpty()) { + writeAttributes(out, getAttributesSortedByName(attributes)); + } + writeSectionDelimiter(out); + } + + static void writeSectionDelimiter(OutputStream out) throws IOException { + out.write(CRLF); + } + + static void writeAttribute(OutputStream out, Attributes.Name name, String value) + throws IOException { + writeAttribute(out, name.toString(), value); + } + + private static void writeAttribute(OutputStream out, String name, String value) + throws IOException { + writeLine(out, name + ": " + value); + } + + private static void writeLine(OutputStream out, String line) throws IOException { + byte[] lineBytes = line.getBytes("UTF-8"); + int offset = 0; + int remaining = lineBytes.length; + boolean firstLine = true; + while (remaining > 0) { + int chunkLength; + if (firstLine) { + // First line + chunkLength = Math.min(remaining, MAX_LINE_LENGTH); + } else { + // Continuation line + out.write(CRLF); + out.write(' '); + chunkLength = Math.min(remaining, MAX_LINE_LENGTH - 1); + } + out.write(lineBytes, offset, chunkLength); + offset += chunkLength; + remaining -= chunkLength; + firstLine = false; + } + out.write(CRLF); + } + + static SortedMap getAttributesSortedByName(Attributes attributes) { + Set> attributesEntries = attributes.entrySet(); + SortedMap namedAttributes = new TreeMap(); + for (Map.Entry attribute : attributesEntries) { + String attrName = attribute.getKey().toString(); + String attrValue = attribute.getValue().toString(); + namedAttributes.put(attrName, attrValue); + } + return namedAttributes; + } + + static void writeAttributes( + OutputStream out, SortedMap attributesSortedByName) throws IOException { + for (Map.Entry attribute : attributesSortedByName.entrySet()) { + String attrName = attribute.getKey(); + String attrValue = attribute.getValue(); + writeAttribute(out, attrName, attrValue); + } + } +} diff --git a/plugin/src/main/java/com/android/apksigner/core/internal/jar/SignatureFileWriter.java b/plugin/src/main/java/com/android/apksigner/core/internal/jar/SignatureFileWriter.java new file mode 100644 index 0000000..9cd25f3 --- /dev/null +++ b/plugin/src/main/java/com/android/apksigner/core/internal/jar/SignatureFileWriter.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksigner.core.internal.jar; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.SortedMap; +import java.util.jar.Attributes; + +/** + * Producer of JAR signature file ({@code *.SF}). + */ +public abstract class SignatureFileWriter { + private SignatureFileWriter() {} + + public static void writeMainSection(OutputStream out, Attributes attributes) + throws IOException { + + // Main section must start with the Signature-Version attribute. + // See https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Signed_JAR_File. + String signatureVersion = attributes.getValue(Attributes.Name.SIGNATURE_VERSION); + if (signatureVersion == null) { + throw new IllegalArgumentException( + "Mandatory " + Attributes.Name.SIGNATURE_VERSION + " attribute missing"); + } + ManifestWriter.writeAttribute(out, Attributes.Name.SIGNATURE_VERSION, signatureVersion); + + if (attributes.size() > 1) { + SortedMap namedAttributes = + ManifestWriter.getAttributesSortedByName(attributes); + namedAttributes.remove(Attributes.Name.SIGNATURE_VERSION.toString()); + ManifestWriter.writeAttributes(out, namedAttributes); + } + writeSectionDelimiter(out); + } + + public static void writeIndividualSection(OutputStream out, String name, Attributes attributes) + throws IOException { + ManifestWriter.writeIndividualSection(out, name, attributes); + } + + public static void writeSectionDelimiter(OutputStream out) throws IOException { + ManifestWriter.writeSectionDelimiter(out); + } +} diff --git a/plugin/src/main/java/com/android/apksigner/core/internal/util/ByteArrayOutputStreamSink.java b/plugin/src/main/java/com/android/apksigner/core/internal/util/ByteArrayOutputStreamSink.java new file mode 100644 index 0000000..ca79df7 --- /dev/null +++ b/plugin/src/main/java/com/android/apksigner/core/internal/util/ByteArrayOutputStreamSink.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksigner.core.internal.util; + +import com.android.apksigner.core.util.DataSink; + +import java.io.ByteArrayOutputStream; +import java.nio.ByteBuffer; + +/** + * Data sink which stores all input data into an internal {@link ByteArrayOutputStream}, thus + * accepting an arbitrary amount of data. + */ +public class ByteArrayOutputStreamSink implements DataSink { + + private final ByteArrayOutputStream mBuf = new ByteArrayOutputStream(); + + @Override + public void consume(byte[] buf, int offset, int length) { + mBuf.write(buf, offset, length); + } + + @Override + public void consume(ByteBuffer buf) { + if (!buf.hasRemaining()) { + return; + } + + if (buf.hasArray()) { + mBuf.write( + buf.array(), + buf.arrayOffset() + buf.position(), + buf.remaining()); + buf.position(buf.limit()); + } else { + byte[] tmp = new byte[buf.remaining()]; + buf.get(tmp); + mBuf.write(tmp, 0, tmp.length); + } + } + + /** + * Returns the data received so far. + */ + public byte[] getData() { + return mBuf.toByteArray(); + } +} diff --git a/plugin/src/main/java/com/android/apksigner/core/internal/util/ByteBufferDataSource.java b/plugin/src/main/java/com/android/apksigner/core/internal/util/ByteBufferDataSource.java new file mode 100644 index 0000000..b2d9ca1 --- /dev/null +++ b/plugin/src/main/java/com/android/apksigner/core/internal/util/ByteBufferDataSource.java @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksigner.core.internal.util; + +import com.android.apksigner.core.util.DataSink; +import com.android.apksigner.core.util.DataSource; + +import java.io.IOException; +import java.nio.ByteBuffer; + +/** + * {@link DataSource} backed by a {@link ByteBuffer}. + */ +public class ByteBufferDataSource implements DataSource { + + private final ByteBuffer mBuffer; + private final int mSize; + + /** + * Constructs a new {@code ByteBufferDigestSource} based on the data contained in the provided + * buffer between the buffer's position and limit. + */ + public ByteBufferDataSource(ByteBuffer buffer) { + this(buffer, true); + } + + /** + * Constructs a new {@code ByteBufferDigestSource} based on the data contained in the provided + * buffer between the buffer's position and limit. + */ + private ByteBufferDataSource(ByteBuffer buffer, boolean sliceRequired) { + mBuffer = (sliceRequired) ? buffer.slice() : buffer; + mSize = buffer.remaining(); + } + + @Override + public long size() { + return mSize; + } + + @Override + public ByteBuffer getByteBuffer(long offset, int size) { + checkChunkValid(offset, size); + + // checkChunkValid ensures that it's OK to cast offset to int. + int chunkPosition = (int) offset; + int chunkLimit = chunkPosition + size; + // Creating a slice of ByteBuffer modifies the state of the source ByteBuffer (position + // and limit fields, to be more specific). We thus use synchronization around these + // state-changing operations to make instances of this class thread-safe. + synchronized (mBuffer) { + // ByteBuffer.limit(int) and .position(int) check that that the position >= limit + // invariant is not broken. Thus, the only way to safely change position and limit + // without caring about their current values is to first set position to 0 or set the + // limit to capacity. + mBuffer.position(0); + + mBuffer.limit(chunkLimit); + mBuffer.position(chunkPosition); + return mBuffer.slice(); + } + } + + @Override + public void copyTo(long offset, int size, ByteBuffer dest) { + dest.put(getByteBuffer(offset, size)); + } + + @Override + public void feed(long offset, long size, DataSink sink) throws IOException { + if ((size < 0) || (size > mSize)) { + throw new IllegalArgumentException("size: " + size + ", source size: " + mSize); + } + sink.consume(getByteBuffer(offset, (int) size)); + } + + @Override + public ByteBufferDataSource slice(long offset, long size) { + if ((offset == 0) && (size == mSize)) { + return this; + } + if ((size < 0) || (size > mSize)) { + throw new IllegalArgumentException("size: " + size + ", source size: " + mSize); + } + return new ByteBufferDataSource( + getByteBuffer(offset, (int) size), + false // no need to slice -- it's already a slice + ); + } + + private void checkChunkValid(long offset, long size) { + if (offset < 0) { + throw new IllegalArgumentException("offset: " + offset); + } + if (size < 0) { + throw new IllegalArgumentException("size: " + size); + } + if (offset > mSize) { + throw new IllegalArgumentException( + "offset (" + offset + ") > source size (" + mSize + ")"); + } + long endOffset = offset + size; + if (endOffset < offset) { + throw new IllegalArgumentException( + "offset (" + offset + ") + size (" + size + ") overflow"); + } + if (endOffset > mSize) { + throw new IllegalArgumentException( + "offset (" + offset + ") + size (" + size + ") > source size (" + mSize +")"); + } + } +} diff --git a/plugin/src/main/java/com/android/apksigner/core/internal/util/ByteBufferSink.java b/plugin/src/main/java/com/android/apksigner/core/internal/util/ByteBufferSink.java new file mode 100644 index 0000000..8c57905 --- /dev/null +++ b/plugin/src/main/java/com/android/apksigner/core/internal/util/ByteBufferSink.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksigner.core.internal.util; + +import com.android.apksigner.core.util.DataSink; + +import java.io.IOException; +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; + +/** + * Data sink which stores all received data into the associated {@link ByteBuffer}. + */ +public class ByteBufferSink implements DataSink { + + private final ByteBuffer mBuffer; + + public ByteBufferSink(ByteBuffer buffer) { + mBuffer = buffer; + } + + @Override + public void consume(byte[] buf, int offset, int length) throws IOException { + try { + mBuffer.put(buf, offset, length); + } catch (BufferOverflowException e) { + throw new IOException( + "Insufficient space in output buffer for " + length + " bytes", e); + } + } + + @Override + public void consume(ByteBuffer buf) throws IOException { + int length = buf.remaining(); + try { + mBuffer.put(buf); + } catch (BufferOverflowException e) { + throw new IOException( + "Insufficient space in output buffer for " + length + " bytes", e); + } + } +} diff --git a/plugin/src/main/java/com/android/apksigner/core/internal/util/DelegatingX509Certificate.java b/plugin/src/main/java/com/android/apksigner/core/internal/util/DelegatingX509Certificate.java new file mode 100644 index 0000000..936cfa9 --- /dev/null +++ b/plugin/src/main/java/com/android/apksigner/core/internal/util/DelegatingX509Certificate.java @@ -0,0 +1,179 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksigner.core.internal.util; + +import java.math.BigInteger; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.Principal; +import java.security.PublicKey; +import java.security.SignatureException; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateExpiredException; +import java.security.cert.CertificateNotYetValidException; +import java.security.cert.X509Certificate; +import java.util.Date; +import java.util.Set; + +/** + * {@link X509Certificate} which delegates all method invocations to the provided delegate + * {@code X509Certificate}. + */ +public class DelegatingX509Certificate extends X509Certificate { + private final X509Certificate mDelegate; + + public DelegatingX509Certificate(X509Certificate delegate) { + this.mDelegate = delegate; + } + + @Override + public Set getCriticalExtensionOIDs() { + return mDelegate.getCriticalExtensionOIDs(); + } + + @Override + public byte[] getExtensionValue(String oid) { + return mDelegate.getExtensionValue(oid); + } + + @Override + public Set getNonCriticalExtensionOIDs() { + return mDelegate.getNonCriticalExtensionOIDs(); + } + + @Override + public boolean hasUnsupportedCriticalExtension() { + return mDelegate.hasUnsupportedCriticalExtension(); + } + + @Override + public void checkValidity() + throws CertificateExpiredException, CertificateNotYetValidException { + mDelegate.checkValidity(); + } + + @Override + public void checkValidity(Date date) + throws CertificateExpiredException, CertificateNotYetValidException { + mDelegate.checkValidity(date); + } + + @Override + public int getVersion() { + return mDelegate.getVersion(); + } + + @Override + public BigInteger getSerialNumber() { + return mDelegate.getSerialNumber(); + } + + @Override + public Principal getIssuerDN() { + return mDelegate.getIssuerDN(); + } + + @Override + public Principal getSubjectDN() { + return mDelegate.getSubjectDN(); + } + + @Override + public Date getNotBefore() { + return mDelegate.getNotBefore(); + } + + @Override + public Date getNotAfter() { + return mDelegate.getNotAfter(); + } + + @Override + public byte[] getTBSCertificate() throws CertificateEncodingException { + return mDelegate.getTBSCertificate(); + } + + @Override + public byte[] getSignature() { + return mDelegate.getSignature(); + } + + @Override + public String getSigAlgName() { + return mDelegate.getSigAlgName(); + } + + @Override + public String getSigAlgOID() { + return mDelegate.getSigAlgOID(); + } + + @Override + public byte[] getSigAlgParams() { + return mDelegate.getSigAlgParams(); + } + + @Override + public boolean[] getIssuerUniqueID() { + return mDelegate.getIssuerUniqueID(); + } + + @Override + public boolean[] getSubjectUniqueID() { + return mDelegate.getSubjectUniqueID(); + } + + @Override + public boolean[] getKeyUsage() { + return mDelegate.getKeyUsage(); + } + + @Override + public int getBasicConstraints() { + return mDelegate.getBasicConstraints(); + } + + @Override + public byte[] getEncoded() throws CertificateEncodingException { + return mDelegate.getEncoded(); + } + + @Override + public void verify(PublicKey key) throws CertificateException, NoSuchAlgorithmException, + InvalidKeyException, NoSuchProviderException, SignatureException { + mDelegate.verify(key); + } + + @Override + public void verify(PublicKey key, String sigProvider) + throws CertificateException, NoSuchAlgorithmException, InvalidKeyException, + NoSuchProviderException, SignatureException { + mDelegate.verify(key, sigProvider); + } + + @Override + public String toString() { + return mDelegate.toString(); + } + + @Override + public PublicKey getPublicKey() { + return mDelegate.getPublicKey(); + } +} diff --git a/plugin/src/main/java/com/android/apksigner/core/internal/util/MessageDigestSink.java b/plugin/src/main/java/com/android/apksigner/core/internal/util/MessageDigestSink.java new file mode 100644 index 0000000..45bb30e --- /dev/null +++ b/plugin/src/main/java/com/android/apksigner/core/internal/util/MessageDigestSink.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.apksigner.core.internal.util; + +import com.android.apksigner.core.util.DataSink; + +import java.nio.ByteBuffer; +import java.security.MessageDigest; + +/** + * Data sink which feeds all received data into the associated {@link MessageDigest} instances. Each + * {@code MessageDigest} instance receives the same data. + */ +public class MessageDigestSink implements DataSink { + + private final MessageDigest[] mMessageDigests; + + public MessageDigestSink(MessageDigest[] digests) { + mMessageDigests = digests; + } + + @Override + public void consume(byte[] buf, int offset, int length) { + for (MessageDigest md : mMessageDigests) { + md.update(buf, offset, length); + } + } + + @Override + public void consume(ByteBuffer buf) { + int originalPosition = buf.position(); + for (MessageDigest md : mMessageDigests) { + // Reset the position back to the original because the previous iteration's + // MessageDigest.update set the buffer's position to the buffer's limit. + buf.position(originalPosition); + md.update(buf); + } + } +} diff --git a/plugin/src/main/java/com/android/apksigner/core/internal/util/Pair.java b/plugin/src/main/java/com/android/apksigner/core/internal/util/Pair.java new file mode 100644 index 0000000..d59af41 --- /dev/null +++ b/plugin/src/main/java/com/android/apksigner/core/internal/util/Pair.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksigner.core.internal.util; + +/** + * Pair of two elements. + */ +public final class Pair { + private final A mFirst; + private final B mSecond; + + private Pair(A first, B second) { + mFirst = first; + mSecond = second; + } + + public static Pair of(A first, B second) { + return new Pair(first, second); + } + + public A getFirst() { + return mFirst; + } + + public B getSecond() { + return mSecond; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((mFirst == null) ? 0 : mFirst.hashCode()); + result = prime * result + ((mSecond == null) ? 0 : mSecond.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + @SuppressWarnings("rawtypes") + Pair other = (Pair) obj; + if (mFirst == null) { + if (other.mFirst != null) { + return false; + } + } else if (!mFirst.equals(other.mFirst)) { + return false; + } + if (mSecond == null) { + if (other.mSecond != null) { + return false; + } + } else if (!mSecond.equals(other.mSecond)) { + return false; + } + return true; + } +} diff --git a/plugin/src/main/java/com/android/apksigner/core/internal/zip/ZipUtils.java b/plugin/src/main/java/com/android/apksigner/core/internal/zip/ZipUtils.java new file mode 100644 index 0000000..5e724a2 --- /dev/null +++ b/plugin/src/main/java/com/android/apksigner/core/internal/zip/ZipUtils.java @@ -0,0 +1,278 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksigner.core.internal.zip; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +import com.android.apksigner.core.internal.util.Pair; +import com.android.apksigner.core.util.DataSource; + +/** + * Assorted ZIP format helpers. + * + *

NOTE: Most helper methods operating on {@code ByteBuffer} instances expect that the byte + * order of these buffers is little-endian. + */ +public abstract class ZipUtils { + private ZipUtils() {} + + public static final short COMPRESSION_METHOD_STORED = 0; + public static final short COMPRESSION_METHOD_DEFLATED = 8; + + private static final int ZIP_EOCD_REC_MIN_SIZE = 22; + private static final int ZIP_EOCD_REC_SIG = 0x06054b50; + private static final int ZIP_EOCD_CENTRAL_DIR_TOTAL_RECORD_COUNT_OFFSET = 10; + private static final int ZIP_EOCD_CENTRAL_DIR_SIZE_FIELD_OFFSET = 12; + private static final int ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET = 16; + private static final int ZIP_EOCD_COMMENT_LENGTH_FIELD_OFFSET = 20; + + private static final int ZIP64_EOCD_LOCATOR_SIZE = 20; + private static final int ZIP64_EOCD_LOCATOR_SIG = 0x07064b50; + + private static final int UINT16_MAX_VALUE = 0xffff; + + /** + * Sets the offset of the start of the ZIP Central Directory in the archive. + * + *

NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian. + */ + public static void setZipEocdCentralDirectoryOffset( + ByteBuffer zipEndOfCentralDirectory, long offset) { + assertByteOrderLittleEndian(zipEndOfCentralDirectory); + setUnsignedInt32( + zipEndOfCentralDirectory, + zipEndOfCentralDirectory.position() + ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET, + offset); + } + + /** + * Returns the offset of the start of the ZIP Central Directory in the archive. + * + *

NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian. + */ + public static long getZipEocdCentralDirectoryOffset(ByteBuffer zipEndOfCentralDirectory) { + assertByteOrderLittleEndian(zipEndOfCentralDirectory); + return getUnsignedInt32( + zipEndOfCentralDirectory, + zipEndOfCentralDirectory.position() + ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET); + } + + /** + * Returns the size (in bytes) of the ZIP Central Directory. + * + *

NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian. + */ + public static long getZipEocdCentralDirectorySizeBytes(ByteBuffer zipEndOfCentralDirectory) { + assertByteOrderLittleEndian(zipEndOfCentralDirectory); + return getUnsignedInt32( + zipEndOfCentralDirectory, + zipEndOfCentralDirectory.position() + ZIP_EOCD_CENTRAL_DIR_SIZE_FIELD_OFFSET); + } + + /** + * Returns the total number of records in ZIP Central Directory. + * + *

NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian. + */ + public static int getZipEocdCentralDirectoryTotalRecordCount( + ByteBuffer zipEndOfCentralDirectory) { + assertByteOrderLittleEndian(zipEndOfCentralDirectory); + return getUnsignedInt16( + zipEndOfCentralDirectory, + zipEndOfCentralDirectory.position() + + ZIP_EOCD_CENTRAL_DIR_TOTAL_RECORD_COUNT_OFFSET); + } + + /** + * Returns the ZIP End of Central Directory record of the provided ZIP file. + * + * @return contents of the ZIP End of Central Directory record and the record's offset in the + * file or {@code null} if the file does not contain the record. + * + * @throws IOException if an I/O error occurs while reading the file. + */ + public static Pair findZipEndOfCentralDirectoryRecord(DataSource zip) + throws IOException { + // ZIP End of Central Directory (EOCD) record is located at the very end of the ZIP archive. + // The record can be identified by its 4-byte signature/magic which is located at the very + // beginning of the record. A complication is that the record is variable-length because of + // the comment field. + // The algorithm for locating the ZIP EOCD record is as follows. We search backwards from + // end of the buffer for the EOCD record signature. Whenever we find a signature, we check + // the candidate record's comment length is such that the remainder of the record takes up + // exactly the remaining bytes in the buffer. The search is bounded because the maximum + // size of the comment field is 65535 bytes because the field is an unsigned 16-bit number. + + long fileSize = zip.size(); + if (fileSize < ZIP_EOCD_REC_MIN_SIZE) { + return null; + } + + // Optimization: 99.99% of APKs have a zero-length comment field in the EoCD record and thus + // the EoCD record offset is known in advance. Try that offset first to avoid unnecessarily + // reading more data. + Pair result = findZipEndOfCentralDirectoryRecord(zip, 0); + if (result != null) { + return result; + } + + // EoCD does not start where we expected it to. Perhaps it contains a non-empty comment + // field. Expand the search. The maximum size of the comment field in EoCD is 65535 because + // the comment length field is an unsigned 16-bit number. + return findZipEndOfCentralDirectoryRecord(zip, UINT16_MAX_VALUE); + } + + /** + * Returns the ZIP End of Central Directory record of the provided ZIP file. + * + * @param maxCommentSize maximum accepted size (in bytes) of EoCD comment field. The permitted + * value is from 0 to 65535 inclusive. The smaller the value, the faster this method + * locates the record, provided its comment field is no longer than this value. + * + * @return contents of the ZIP End of Central Directory record and the record's offset in the + * file or {@code null} if the file does not contain the record. + * + * @throws IOException if an I/O error occurs while reading the file. + */ + private static Pair findZipEndOfCentralDirectoryRecord( + DataSource zip, int maxCommentSize) throws IOException { + // ZIP End of Central Directory (EOCD) record is located at the very end of the ZIP archive. + // The record can be identified by its 4-byte signature/magic which is located at the very + // beginning of the record. A complication is that the record is variable-length because of + // the comment field. + // The algorithm for locating the ZIP EOCD record is as follows. We search backwards from + // end of the buffer for the EOCD record signature. Whenever we find a signature, we check + // the candidate record's comment length is such that the remainder of the record takes up + // exactly the remaining bytes in the buffer. The search is bounded because the maximum + // size of the comment field is 65535 bytes because the field is an unsigned 16-bit number. + + if ((maxCommentSize < 0) || (maxCommentSize > UINT16_MAX_VALUE)) { + throw new IllegalArgumentException("maxCommentSize: " + maxCommentSize); + } + + long fileSize = zip.size(); + if (fileSize < ZIP_EOCD_REC_MIN_SIZE) { + // No space for EoCD record in the file. + return null; + } + // Lower maxCommentSize if the file is too small. + maxCommentSize = (int) Math.min(maxCommentSize, fileSize - ZIP_EOCD_REC_MIN_SIZE); + + int maxEocdSize = ZIP_EOCD_REC_MIN_SIZE + maxCommentSize; + long bufOffsetInFile = fileSize - maxEocdSize; + ByteBuffer buf = zip.getByteBuffer(bufOffsetInFile, maxEocdSize); + buf.order(ByteOrder.LITTLE_ENDIAN); + int eocdOffsetInBuf = findZipEndOfCentralDirectoryRecord(buf); + if (eocdOffsetInBuf == -1) { + // No EoCD record found in the buffer + return null; + } + // EoCD found + buf.position(eocdOffsetInBuf); + ByteBuffer eocd = buf.slice(); + eocd.order(ByteOrder.LITTLE_ENDIAN); + return Pair.of(eocd, bufOffsetInFile + eocdOffsetInBuf); + } + + /** + * Returns the position at which ZIP End of Central Directory record starts in the provided + * buffer or {@code -1} if the record is not present. + * + *

NOTE: Byte order of {@code zipContents} must be little-endian. + */ + private static int findZipEndOfCentralDirectoryRecord(ByteBuffer zipContents) { + assertByteOrderLittleEndian(zipContents); + + // ZIP End of Central Directory (EOCD) record is located at the very end of the ZIP archive. + // The record can be identified by its 4-byte signature/magic which is located at the very + // beginning of the record. A complication is that the record is variable-length because of + // the comment field. + // The algorithm for locating the ZIP EOCD record is as follows. We search backwards from + // end of the buffer for the EOCD record signature. Whenever we find a signature, we check + // the candidate record's comment length is such that the remainder of the record takes up + // exactly the remaining bytes in the buffer. The search is bounded because the maximum + // size of the comment field is 65535 bytes because the field is an unsigned 16-bit number. + + int archiveSize = zipContents.capacity(); + if (archiveSize < ZIP_EOCD_REC_MIN_SIZE) { + return -1; + } + int maxCommentLength = Math.min(archiveSize - ZIP_EOCD_REC_MIN_SIZE, UINT16_MAX_VALUE); + int eocdWithEmptyCommentStartPosition = archiveSize - ZIP_EOCD_REC_MIN_SIZE; + for (int expectedCommentLength = 0; expectedCommentLength < maxCommentLength; + expectedCommentLength++) { + int eocdStartPos = eocdWithEmptyCommentStartPosition - expectedCommentLength; + if (zipContents.getInt(eocdStartPos) == ZIP_EOCD_REC_SIG) { + int actualCommentLength = + getUnsignedInt16( + zipContents, eocdStartPos + ZIP_EOCD_COMMENT_LENGTH_FIELD_OFFSET); + if (actualCommentLength == expectedCommentLength) { + return eocdStartPos; + } + } + } + + return -1; + } + + /** + * Returns {@code true} if the provided file contains a ZIP64 End of Central Directory + * Locator. + * + * @param zipEndOfCentralDirectoryPosition offset of the ZIP End of Central Directory record + * in the file. + * + * @throws IOException if an I/O error occurs while reading the data source + */ + public static final boolean isZip64EndOfCentralDirectoryLocatorPresent( + DataSource zip, long zipEndOfCentralDirectoryPosition) throws IOException { + + // ZIP64 End of Central Directory Locator immediately precedes the ZIP End of Central + // Directory Record. + long locatorPosition = zipEndOfCentralDirectoryPosition - ZIP64_EOCD_LOCATOR_SIZE; + if (locatorPosition < 0) { + return false; + } + + ByteBuffer sig = zip.getByteBuffer(locatorPosition, 4); + sig.order(ByteOrder.LITTLE_ENDIAN); + return sig.getInt(0) == ZIP64_EOCD_LOCATOR_SIG; + } + + private static void assertByteOrderLittleEndian(ByteBuffer buffer) { + if (buffer.order() != ByteOrder.LITTLE_ENDIAN) { + throw new IllegalArgumentException("ByteBuffer byte order must be little endian"); + } + } + + private static int getUnsignedInt16(ByteBuffer buffer, int offset) { + return buffer.getShort(offset) & 0xffff; + } + + private static void setUnsignedInt32(ByteBuffer buffer, int offset, long value) { + if ((value < 0) || (value > 0xffffffffL)) { + throw new IllegalArgumentException("uint32 value of out range: " + value); + } + buffer.putInt(offset, (int) value); + } + + private static long getUnsignedInt32(ByteBuffer buffer, int offset) { + return buffer.getInt(offset) & 0xffffffffL; + } +} \ No newline at end of file diff --git a/plugin/src/main/java/com/android/apksigner/core/util/DataSink.java b/plugin/src/main/java/com/android/apksigner/core/util/DataSink.java new file mode 100644 index 0000000..35a61fc --- /dev/null +++ b/plugin/src/main/java/com/android/apksigner/core/util/DataSink.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksigner.core.util; + +import java.io.IOException; +import java.nio.ByteBuffer; + +/** + * Consumer of input data which may be provided in one go or in chunks. + */ +public interface DataSink { + + /** + * Consumes the provided chunk of data. + * + *

This data sink guarantees to not hold references to the provided buffer after this method + * terminates. + */ + void consume(byte[] buf, int offset, int length) throws IOException; + + /** + * Consumes all remaining data in the provided buffer and advances the buffer's position + * to the buffer's limit. + * + *

This data sink guarantees to not hold references to the provided buffer after this method + * terminates. + */ + void consume(ByteBuffer buf) throws IOException; +} diff --git a/plugin/src/main/java/com/android/apksigner/core/util/DataSource.java b/plugin/src/main/java/com/android/apksigner/core/util/DataSource.java new file mode 100644 index 0000000..e268dd2 --- /dev/null +++ b/plugin/src/main/java/com/android/apksigner/core/util/DataSource.java @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksigner.core.util; + +import java.io.IOException; +import java.nio.ByteBuffer; + +/** + * Abstract representation of a source of data. + * + *

This abstraction serves three purposes: + *

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

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

    + *
  • Stream the chunk's data into a {@link DataSink} using + * {@link #feed(long, long, DataSink) feed}. This is best suited for scenarios where there is no + * need to have the chunk's data accessible at the same time, for example, when computing the + * digest of the chunk. If you need to keep the chunk's data around after {@code feed} + * completes, you must create a copy during {@code feed}. However, in that case the following + * methods of obtaining the chunk's data may be more appropriate.
  • + *
  • Obtain a {@link ByteBuffer} containing the chunk's data using + * {@link #getByteBuffer(long, int) getByteBuffer}. Depending on the data source, the chunk's + * data may or may not be copied by this operation. This is best suited for scenarios where + * you need to access the chunk's data in arbitrary order, but don't need to modify the data and + * thus don't require a copy of the data.
  • + *
  • Copy the chunk's data to a {@link ByteBuffer} using + * {@link #copyTo(long, int, ByteBuffer) copyTo}. This is best suited for scenarios where + * you require a copy of the chunk's data, such as to when you need to modify the data. + *
  • + *
+ */ +public interface DataSource { + + /** + * Returns the amount of data (in bytes) contained in this data source. + */ + long size(); + + /** + * Feeds the specified chunk from this data source into the provided sink. + * + * @param offset index (in bytes) at which the chunk starts inside data source + * @param size size (in bytes) of the chunk + */ + void feed(long offset, long size, DataSink sink) throws IOException; + + /** + * Returns a buffer holding the contents of the specified chunk of data from this data source. + * Changes to the data source are not guaranteed to be reflected in the returned buffer. + * Similarly, changes in the buffer are not guaranteed to be reflected in the data source. + * + *

The returned buffer's position is {@code 0}, and the buffer's limit and capacity is + * {@code size}. + * + * @param offset index (in bytes) at which the chunk starts inside data source + * @param size size (in bytes) of the chunk + */ + ByteBuffer getByteBuffer(long offset, int size) throws IOException; + + /** + * Copies the specified chunk from this data source into the provided destination buffer, + * advancing the destination buffer's position by {@code size}. + * + * @param offset index (in bytes) at which the chunk starts inside data source + * @param size size (in bytes) of the chunk + */ + void copyTo(long offset, int size, ByteBuffer dest) throws IOException; + + /** + * Returns a data source representing the specified region of data of this data source. Changes + * to data represented by this data source will also be visible in the returned data source. + * + * @param offset index (in bytes) at which the region starts inside data source + * @param size size (in bytes) of the region + */ + DataSource slice(long offset, long size); +} diff --git a/plugin/src/main/java/com/android/apksigner/core/util/DataSources.java b/plugin/src/main/java/com/android/apksigner/core/util/DataSources.java new file mode 100644 index 0000000..6ce0ac8 --- /dev/null +++ b/plugin/src/main/java/com/android/apksigner/core/util/DataSources.java @@ -0,0 +1,24 @@ +package com.android.apksigner.core.util; + +import com.android.apksigner.core.internal.util.ByteBufferDataSource; + +import java.nio.ByteBuffer; + +/** + * Utility methods for working with {@link DataSource} abstraction. + */ +public abstract class DataSources { + private DataSources() {} + + /** + * Returns a {@link DataSource} backed by the provided {@link ByteBuffer}. The data source + * represents the data contained between the position and limit of the buffer. Changes to the + * buffer's contents will be visible in the data source. + */ + public static DataSource asDataSource(ByteBuffer buffer) { + if (buffer == null) { + throw new NullPointerException(); + } + return new ByteBufferDataSource(buffer); + } +} diff --git a/plugin/src/main/java/com/android/apksigner/core/zip/ZipFormatException.java b/plugin/src/main/java/com/android/apksigner/core/zip/ZipFormatException.java new file mode 100644 index 0000000..7da57d9 --- /dev/null +++ b/plugin/src/main/java/com/android/apksigner/core/zip/ZipFormatException.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksigner.core.zip; + +/** + * Indicates that a ZIP archive is not well-formed. + */ +public class ZipFormatException extends Exception { + private static final long serialVersionUID = 1L; + + public ZipFormatException(String message) { + super(message); + } + + public ZipFormatException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/plugin/src/main/java/com/android/signapk/SignApk.java b/plugin/src/main/java/com/android/signapk/SignApk.java new file mode 100644 index 0000000..b8f4cb9 --- /dev/null +++ b/plugin/src/main/java/com/android/signapk/SignApk.java @@ -0,0 +1,1094 @@ +///* +// * Copyright (C) 2008 The Android Open Source Project +// * +// * Licensed under the Apache License, Version 2.0 (the "License"); +// * you may not use this file except in compliance with the License. +// * You may obtain a copy of the License at +// * +// * http://www.apache.org/licenses/LICENSE-2.0 +// * +// * Unless required by applicable law or agreed to in writing, software +// * distributed under the License is distributed on an "AS IS" BASIS, +// * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// * See the License for the specific language governing permissions and +// * limitations under the License. +// */ +// +//package com.android.signapk; +// +//import org.bouncycastle.asn1.ASN1InputStream; +//import org.bouncycastle.asn1.ASN1ObjectIdentifier; +//import org.bouncycastle.asn1.DEROutputStream; +//import org.bouncycastle.asn1.cms.CMSObjectIdentifiers; +//import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; +//import org.bouncycastle.cert.jcajce.JcaCertStore; +//import org.bouncycastle.cms.CMSException; +//import org.bouncycastle.cms.CMSSignedData; +//import org.bouncycastle.cms.CMSSignedDataGenerator; +//import org.bouncycastle.cms.CMSTypedData; +//import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder; +//import org.bouncycastle.jce.provider.BouncyCastleProvider; +//import org.bouncycastle.operator.ContentSigner; +//import org.bouncycastle.operator.OperatorCreationException; +//import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +//import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder; +//import org.conscrypt.OpenSSLProvider; +// +//import com.android.apksigner.core.ApkSignerEngine; +//import com.android.apksigner.core.DefaultApkSignerEngine; +//import com.android.apksigner.core.apk.ApkUtils; +//import com.android.apksigner.core.util.DataSink; +//import com.android.apksigner.core.util.DataSources; +//import com.android.apksigner.core.zip.ZipFormatException; +// +//import java.io.Console; +//import java.io.BufferedReader; +//import java.io.ByteArrayInputStream; +//import java.io.ByteArrayOutputStream; +//import java.io.DataInputStream; +//import java.io.File; +//import java.io.FileInputStream; +//import java.io.FileOutputStream; +//import java.io.FilterOutputStream; +//import java.io.IOException; +//import java.io.InputStream; +//import java.io.InputStreamReader; +//import java.io.OutputStream; +//import java.lang.reflect.Constructor; +//import java.nio.ByteBuffer; +//import java.nio.ByteOrder; +//import java.security.GeneralSecurityException; +//import java.security.Key; +//import java.security.KeyFactory; +//import java.security.PrivateKey; +//import java.security.Provider; +//import java.security.Security; +//import java.security.cert.CertificateEncodingException; +//import java.security.cert.CertificateFactory; +//import java.security.cert.X509Certificate; +//import java.security.spec.InvalidKeySpecException; +//import java.security.spec.PKCS8EncodedKeySpec; +//import java.util.ArrayList; +//import java.util.Collections; +//import java.util.Enumeration; +//import java.util.List; +//import java.util.Locale; +//import java.util.TimeZone; +//import java.util.jar.JarEntry; +//import java.util.jar.JarFile; +//import java.util.jar.JarOutputStream; +//import java.util.regex.Pattern; +// +//import javax.crypto.Cipher; +//import javax.crypto.EncryptedPrivateKeyInfo; +//import javax.crypto.SecretKeyFactory; +//import javax.crypto.spec.PBEKeySpec; +// +///** +// * HISTORICAL NOTE: +// * +// * Prior to the keylimepie release, SignApk ignored the signature +// * algorithm specified in the certificate and always used SHA1withRSA. +// * +// * Starting with JB-MR2, the platform supports SHA256withRSA, so we use +// * the signature algorithm in the certificate to select which to use +// * (SHA256withRSA or SHA1withRSA). Also in JB-MR2, EC keys are supported. +// * +// * Because there are old keys still in use whose certificate actually +// * says "MD5withRSA", we treat these as though they say "SHA1withRSA" +// * for compatibility with older releases. This can be changed by +// * altering the getAlgorithm() function below. +// */ +// +// +///** +// * Command line tool to sign JAR files (including APKs and OTA updates) in a way +// * compatible with the mincrypt verifier, using EC or RSA keys and SHA1 or +// * SHA-256 (see historical note). The tool can additionally sign APKs using +// * APK Signature Scheme v2. +// */ +//class SignApk { +// private static final String OTACERT_NAME = "META-INF/com/android/otacert"; +// +// /** +// * Extensible data block/field header ID used for storing information about alignment of +// * uncompressed entries as well as for aligning the entries's data. See ZIP appnote.txt section +// * 4.5 Extensible data fields. +// */ +// private static final short ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID = (short) 0xd935; +// +// /** +// * Minimum size (in bytes) of the extensible data block/field used for alignment of uncompressed +// * entries. +// */ +// private static final short ALIGNMENT_ZIP_EXTRA_DATA_FIELD_MIN_SIZE_BYTES = 6; +// +// // bitmasks for which hash algorithms we need the manifest to include. +// private static final int USE_SHA1 = 1; +// private static final int USE_SHA256 = 2; +// +// /** +// * Returns the digest algorithm ID (one of {@code USE_SHA1} or {@code USE_SHA256}) to be used +// * for signing an OTA update package using the private key corresponding to the provided +// * certificate. +// */ +// private static int getDigestAlgorithmForOta(X509Certificate cert) { +// String sigAlg = cert.getSigAlgName().toUpperCase(Locale.US); +// if ("SHA1WITHRSA".equals(sigAlg) || "MD5WITHRSA".equals(sigAlg)) { +// // see "HISTORICAL NOTE" above. +// return USE_SHA1; +// } else if (sigAlg.startsWith("SHA256WITH")) { +// return USE_SHA256; +// } else { +// throw new IllegalArgumentException("unsupported signature algorithm \"" + sigAlg + +// "\" in cert [" + cert.getSubjectDN()); +// } +// } +// +// /** +// * Returns the JCA {@link java.security.Signature} algorithm to be used for signing and OTA +// * update package using the private key corresponding to the provided certificate and the +// * provided digest algorithm (see {@code USE_SHA1} and {@code USE_SHA256} constants). +// */ +// private static String getJcaSignatureAlgorithmForOta( +// X509Certificate cert, int hash) { +// String sigAlgDigestPrefix; +// switch (hash) { +// case USE_SHA1: +// sigAlgDigestPrefix = "SHA1"; +// break; +// case USE_SHA256: +// sigAlgDigestPrefix = "SHA256"; +// break; +// default: +// throw new IllegalArgumentException("Unknown hash ID: " + hash); +// } +// +// String keyAlgorithm = cert.getPublicKey().getAlgorithm(); +// if ("RSA".equalsIgnoreCase(keyAlgorithm)) { +// return sigAlgDigestPrefix + "withRSA"; +// } else if ("EC".equalsIgnoreCase(keyAlgorithm)) { +// return sigAlgDigestPrefix + "withECDSA"; +// } else { +// throw new IllegalArgumentException("Unsupported key algorithm: " + keyAlgorithm); +// } +// } +// +// private static X509Certificate readPublicKey(File file) +// throws IOException, GeneralSecurityException { +// FileInputStream input = new FileInputStream(file); +// try { +// CertificateFactory cf = CertificateFactory.getInstance("X.509"); +// return (X509Certificate) cf.generateCertificate(input); +// } finally { +// input.close(); +// } +// } +// +// /** +// * If a console doesn't exist, reads the password from stdin +// * If a console exists, reads the password from console and returns it as a string. +// * +// * @param keyFile The file containing the private key. Used to prompt the user. +// */ +// private static String readPassword(File keyFile) { +// Console console; +// char[] pwd; +// if ((console = System.console()) == null) { +// System.out.print("Enter password for " + keyFile + " (password will not be hidden): "); +// System.out.flush(); +// BufferedReader stdin = new BufferedReader(new InputStreamReader(System.in)); +// try { +// return stdin.readLine(); +// } catch (IOException ex) { +// return null; +// } +// } else { +// if ((pwd = console.readPassword("[%s]", "Enter password for " + keyFile)) != null) { +// return String.valueOf(pwd); +// } else { +// return null; +// } +// } +// } +// +// /** +// * Decrypt an encrypted PKCS#8 format private key. +// * +// * Based on ghstark's post on Aug 6, 2006 at +// * http://forums.sun.com/thread.jspa?threadID=758133&messageID=4330949 +// * +// * @param encryptedPrivateKey The raw data of the private key +// * @param keyFile The file containing the private key +// */ +// private static PKCS8EncodedKeySpec decryptPrivateKey(byte[] encryptedPrivateKey, File keyFile) +// throws GeneralSecurityException { +// EncryptedPrivateKeyInfo epkInfo; +// try { +// epkInfo = new EncryptedPrivateKeyInfo(encryptedPrivateKey); +// } catch (IOException ex) { +// // Probably not an encrypted key. +// return null; +// } +// +// char[] password = readPassword(keyFile).toCharArray(); +// +// SecretKeyFactory skFactory = SecretKeyFactory.getInstance(epkInfo.getAlgName()); +// Key key = skFactory.generateSecret(new PBEKeySpec(password)); +// +// Cipher cipher = Cipher.getInstance(epkInfo.getAlgName()); +// cipher.init(Cipher.DECRYPT_MODE, key, epkInfo.getAlgParameters()); +// +// try { +// return epkInfo.getKeySpec(cipher); +// } catch (InvalidKeySpecException ex) { +// System.err.println("signapk: Password for " + keyFile + " may be bad."); +// throw ex; +// } +// } +// +// /** Read a PKCS#8 format private key. */ +// private static PrivateKey readPrivateKey(File file) +// throws IOException, GeneralSecurityException { +// DataInputStream input = new DataInputStream(new FileInputStream(file)); +// try { +// byte[] bytes = new byte[(int) file.length()]; +// input.read(bytes); +// +// /* Check to see if this is in an EncryptedPrivateKeyInfo structure. */ +// PKCS8EncodedKeySpec spec = decryptPrivateKey(bytes, file); +// if (spec == null) { +// spec = new PKCS8EncodedKeySpec(bytes); +// } +// +// /* +// * Now it's in a PKCS#8 PrivateKeyInfo structure. Read its Algorithm +// * OID and use that to construct a KeyFactory. +// */ +// PrivateKeyInfo pki; +// try (ASN1InputStream bIn = +// new ASN1InputStream(new ByteArrayInputStream(spec.getEncoded()))) { +// pki = PrivateKeyInfo.getInstance(bIn.readObject()); +// } +// String algOid = pki.getPrivateKeyAlgorithm().getAlgorithm().getId(); +// +// return KeyFactory.getInstance(algOid).generatePrivate(spec); +// } finally { +// input.close(); +// } +// } +// +// /** +// * Add a copy of the public key to the archive; this should +// * exactly match one of the files in +// * /system/etc/security/otacerts.zip on the device. (The same +// * cert can be extracted from the OTA update package's signature +// * block but this is much easier to get at.) +// */ +// private static void addOtacert(JarOutputStream outputJar, +// File publicKeyFile, +// long timestamp) +// throws IOException { +// +// JarEntry je = new JarEntry(OTACERT_NAME); +// je.setTime(timestamp); +// outputJar.putNextEntry(je); +// FileInputStream input = new FileInputStream(publicKeyFile); +// byte[] b = new byte[4096]; +// int read; +// while ((read = input.read(b)) != -1) { +// outputJar.write(b, 0, read); +// } +// input.close(); +// } +// +// +// /** Sign data and write the digital signature to 'out'. */ +// private static void writeSignatureBlock( +// CMSTypedData data, X509Certificate publicKey, PrivateKey privateKey, int hash, +// OutputStream out) +// throws IOException, +// CertificateEncodingException, +// OperatorCreationException, +// CMSException { +// ArrayList certList = new ArrayList(1); +// certList.add(publicKey); +// JcaCertStore certs = new JcaCertStore(certList); +// +// CMSSignedDataGenerator gen = new CMSSignedDataGenerator(); +// ContentSigner signer = +// new JcaContentSignerBuilder( +// getJcaSignatureAlgorithmForOta(publicKey, hash)) +// .build(privateKey); +// gen.addSignerInfoGenerator( +// new JcaSignerInfoGeneratorBuilder( +// new JcaDigestCalculatorProviderBuilder() +// .build()) +// .setDirectSignature(true) +// .build(signer, publicKey)); +// gen.addCertificates(certs); +// CMSSignedData sigData = gen.generate(data, false); +// +// try (ASN1InputStream asn1 = new ASN1InputStream(sigData.getEncoded())) { +// DEROutputStream dos = new DEROutputStream(out); +// dos.writeObject(asn1.readObject()); +// } +// } +// +// /** +// * Adds ZIP entries which represent the v1 signature (JAR signature scheme). +// */ +// private static void addV1Signature( +// ApkSignerEngine apkSigner, +// ApkSignerEngine.OutputJarSignatureRequest v1Signature, +// JarOutputStream out, +// long timestamp) throws IOException { +// for (ApkSignerEngine.OutputJarSignatureRequest.JarEntry entry +// : v1Signature.getAdditionalJarEntries()) { +// String entryName = entry.getName(); +// JarEntry outEntry = new JarEntry(entryName); +// outEntry.setTime(timestamp); +// out.putNextEntry(outEntry); +// byte[] entryData = entry.getData(); +// out.write(entryData); +// ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest = +// apkSigner.outputJarEntry(entryName); +// if (inspectEntryRequest != null) { +// inspectEntryRequest.getDataSink().consume(entryData, 0, entryData.length); +// inspectEntryRequest.done(); +// } +// } +// } +// +// /** +// * Copy all JAR entries from input to output. We set the modification times in the output to a +// * fixed time, so as to reduce variation in the output file and make incremental OTAs more +// * efficient. +// */ +// private static void copyFiles( +// JarFile in, +// Pattern ignoredFilenamePattern, +// ApkSignerEngine apkSigner, +// JarOutputStream out, +// long timestamp, +// int defaultAlignment) throws IOException { +// byte[] buffer = new byte[4096]; +// int num; +// +// ArrayList names = new ArrayList(); +// for (Enumeration e = in.entries(); e.hasMoreElements();) { +// JarEntry entry = e.nextElement(); +// if (entry.isDirectory()) { +// continue; +// } +// String entryName = entry.getName(); +// if ((ignoredFilenamePattern != null) +// && (ignoredFilenamePattern.matcher(entryName).matches())) { +// continue; +// } +// names.add(entryName); +// } +// Collections.sort(names); +// +// boolean firstEntry = true; +// long offset = 0L; +// +// // We do the copy in two passes -- first copying all the +// // entries that are STORED, then copying all the entries that +// // have any other compression flag (which in practice means +// // DEFLATED). This groups all the stored entries together at +// // the start of the file and makes it easier to do alignment +// // on them (since only stored entries are aligned). +// +// List remainingNames = new ArrayList<>(names.size()); +// for (String name : names) { +// JarEntry inEntry = in.getJarEntry(name); +// if (inEntry.getMethod() != JarEntry.STORED) { +// // Defer outputting this entry until we're ready to output compressed entries. +// remainingNames.add(name); +// continue; +// } +// +// if (!shouldOutputApkEntry(apkSigner, in, inEntry, buffer)) { +// continue; +// } +// +// // Preserve the STORED method of the input entry. +// JarEntry outEntry = new JarEntry(inEntry); +// outEntry.setTime(timestamp); +// // Discard comment and extra fields of this entry to +// // simplify alignment logic below and for consistency with +// // how compressed entries are handled later. +// outEntry.setComment(null); +// outEntry.setExtra(null); +// +// int alignment = getStoredEntryDataAlignment(name, defaultAlignment); +// // Alignment of the entry's data is achieved by adding a data block to the entry's Local +// // File Header extra field. The data block contains information about the alignment +// // value and the necessary padding bytes (0x00) to achieve the alignment. This works +// // because the entry's data will be located immediately after the extra field. +// // See ZIP APPNOTE.txt section "4.5 Extensible data fields" for details about the format +// // of the extra field. +// +// // 'offset' is the offset into the file at which we expect the entry's data to begin. +// // This is the value we need to make a multiple of 'alignment'. +// offset += JarFile.LOCHDR + outEntry.getName().length(); +// if (firstEntry) { +// // The first entry in a jar file has an extra field of four bytes that you can't get +// // rid of; any extra data you specify in the JarEntry is appended to these forced +// // four bytes. This is JAR_MAGIC in JarOutputStream; the bytes are 0xfeca0000. +// // See http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6808540 +// // and http://bugs.java.com/bugdatabase/view_bug.do?bug_id=4138619. +// offset += 4; +// firstEntry = false; +// } +// int extraPaddingSizeBytes = 0; +// if (alignment > 0) { +// long paddingStartOffset = offset + ALIGNMENT_ZIP_EXTRA_DATA_FIELD_MIN_SIZE_BYTES; +// extraPaddingSizeBytes = alignment - (int) (paddingStartOffset % alignment); +// } +// byte[] extra = +// new byte[ALIGNMENT_ZIP_EXTRA_DATA_FIELD_MIN_SIZE_BYTES + extraPaddingSizeBytes]; +// ByteBuffer extraBuf = ByteBuffer.wrap(extra); +// extraBuf.order(ByteOrder.LITTLE_ENDIAN); +// extraBuf.putShort(ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID); // Header ID +// extraBuf.putShort((short) (2 + extraPaddingSizeBytes)); // Data Size +// extraBuf.putShort((short) alignment); +// outEntry.setExtra(extra); +// offset += extra.length; +// +// out.putNextEntry(outEntry); +// ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest = +// (apkSigner != null) ? apkSigner.outputJarEntry(name) : null; +// DataSink entryDataSink = +// (inspectEntryRequest != null) ? inspectEntryRequest.getDataSink() : null; +// +// try (InputStream data = in.getInputStream(inEntry)) { +// while ((num = data.read(buffer)) > 0) { +// out.write(buffer, 0, num); +// if (entryDataSink != null) { +// entryDataSink.consume(buffer, 0, num); +// } +// offset += num; +// } +// } +// out.flush(); +// if (inspectEntryRequest != null) { +// inspectEntryRequest.done(); +// } +// } +// +// // Copy all the non-STORED entries. We don't attempt to +// // maintain the 'offset' variable past this point; we don't do +// // alignment on these entries. +// +// for (String name : remainingNames) { +// JarEntry inEntry = in.getJarEntry(name); +// if (!shouldOutputApkEntry(apkSigner, in, inEntry, buffer)) { +// continue; +// } +// +// // Create a new entry so that the compressed len is recomputed. +// JarEntry outEntry = new JarEntry(name); +// outEntry.setTime(timestamp); +// out.putNextEntry(outEntry); +// ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest = +// (apkSigner != null) ? apkSigner.outputJarEntry(name) : null; +// DataSink entryDataSink = +// (inspectEntryRequest != null) ? inspectEntryRequest.getDataSink() : null; +// +// InputStream data = in.getInputStream(inEntry); +// while ((num = data.read(buffer)) > 0) { +// out.write(buffer, 0, num); +// if (entryDataSink != null) { +// entryDataSink.consume(buffer, 0, num); +// } +// } +// out.flush(); +// if (inspectEntryRequest != null) { +// inspectEntryRequest.done(); +// } +// } +// } +// +// private static boolean shouldOutputApkEntry( +// ApkSignerEngine apkSigner, JarFile inFile, JarEntry inEntry, byte[] tmpbuf) +// throws IOException { +// if (apkSigner == null) { +// return true; +// } +// +// ApkSignerEngine.InputJarEntryInstructions instructions = +// apkSigner.inputJarEntry(inEntry.getName()); +// ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest = +// instructions.getInspectJarEntryRequest(); +// if (inspectEntryRequest != null) { +// provideJarEntry(inFile, inEntry, inspectEntryRequest, tmpbuf); +// } +// switch (instructions.getOutputPolicy()) { +// case OUTPUT: +// return true; +// case SKIP: +// case OUTPUT_BY_ENGINE: +// return false; +// default: +// throw new RuntimeException( +// "Unsupported output policy: " + instructions.getOutputPolicy()); +// } +// } +// +// private static void provideJarEntry( +// JarFile jarFile, +// JarEntry jarEntry, +// ApkSignerEngine.InspectJarEntryRequest request, +// byte[] tmpbuf) throws IOException { +// DataSink dataSink = request.getDataSink(); +// try (InputStream in = jarFile.getInputStream(jarEntry)) { +// int chunkSize; +// while ((chunkSize = in.read(tmpbuf)) > 0) { +// dataSink.consume(tmpbuf, 0, chunkSize); +// } +// request.done(); +// } +// } +// +// /** +// * Returns the multiple (in bytes) at which the provided {@code STORED} entry's data must start +// * relative to start of file or {@code 0} if alignment of this entry's data is not important. +// */ +// private static int getStoredEntryDataAlignment(String entryName, int defaultAlignment) { +// if (defaultAlignment <= 0) { +// return 0; +// } +// +// if (entryName.endsWith(".so")) { +// // Align .so contents to memory page boundary to enable memory-mapped +// // execution. +// return 4096; +// } else { +// return defaultAlignment; +// } +// } +// +// private static class WholeFileSignerOutputStream extends FilterOutputStream { +// private boolean closing = false; +// private ByteArrayOutputStream footer = new ByteArrayOutputStream(); +// private OutputStream tee; +// +// public WholeFileSignerOutputStream(OutputStream out, OutputStream tee) { +// super(out); +// this.tee = tee; +// } +// +// public void notifyClosing() { +// closing = true; +// } +// +// public void finish() throws IOException { +// closing = false; +// +// byte[] data = footer.toByteArray(); +// if (data.length < 2) +// throw new IOException("Less than two bytes written to footer"); +// write(data, 0, data.length - 2); +// } +// +// public byte[] getTail() { +// return footer.toByteArray(); +// } +// +// @Override +// public void write(byte[] b) throws IOException { +// write(b, 0, b.length); +// } +// +// @Override +// public void write(byte[] b, int off, int len) throws IOException { +// if (closing) { +// // if the jar is about to close, save the footer that will be written +// footer.write(b, off, len); +// } +// else { +// // write to both output streams. out is the CMSTypedData signer and tee is the file. +// out.write(b, off, len); +// tee.write(b, off, len); +// } +// } +// +// @Override +// public void write(int b) throws IOException { +// if (closing) { +// // if the jar is about to close, save the footer that will be written +// footer.write(b); +// } +// else { +// // write to both output streams. out is the CMSTypedData signer and tee is the file. +// out.write(b); +// tee.write(b); +// } +// } +// } +// +// private static class CMSSigner implements CMSTypedData { +// private final JarFile inputJar; +// private final File publicKeyFile; +// private final X509Certificate publicKey; +// private final PrivateKey privateKey; +// private final int hash; +// private final long timestamp; +// private final OutputStream outputStream; +// private final ASN1ObjectIdentifier type; +// private WholeFileSignerOutputStream signer; +// +// // Files matching this pattern are not copied to the output. +// private static final Pattern STRIP_PATTERN = +// Pattern.compile("^(META-INF/((.*)[.](SF|RSA|DSA|EC)|com/android/otacert))|(" +// + Pattern.quote(JarFile.MANIFEST_NAME) + ")$"); +// +// public CMSSigner(JarFile inputJar, File publicKeyFile, +// X509Certificate publicKey, PrivateKey privateKey, int hash, +// long timestamp, OutputStream outputStream) { +// this.inputJar = inputJar; +// this.publicKeyFile = publicKeyFile; +// this.publicKey = publicKey; +// this.privateKey = privateKey; +// this.hash = hash; +// this.timestamp = timestamp; +// this.outputStream = outputStream; +// this.type = new ASN1ObjectIdentifier(CMSObjectIdentifiers.data.getId()); +// } +// +// /** +// * This should actually return byte[] or something similar, but nothing +// * actually checks it currently. +// */ +// @Override +// public Object getContent() { +// return this; +// } +// +// @Override +// public ASN1ObjectIdentifier getContentType() { +// return type; +// } +// +// @Override +// public void write(OutputStream out) throws IOException { +// try { +// signer = new WholeFileSignerOutputStream(out, outputStream); +// JarOutputStream outputJar = new JarOutputStream(signer); +// +// copyFiles(inputJar, STRIP_PATTERN, null, outputJar, timestamp, 0); +// addOtacert(outputJar, publicKeyFile, timestamp); +// +// signer.notifyClosing(); +// outputJar.close(); +// signer.finish(); +// } +// catch (Exception e) { +// throw new IOException(e); +// } +// } +// +// public void writeSignatureBlock(ByteArrayOutputStream temp) +// throws IOException, +// CertificateEncodingException, +// OperatorCreationException, +// CMSException { +// SignApk.writeSignatureBlock(this, publicKey, privateKey, hash, temp); +// } +// +// public WholeFileSignerOutputStream getSigner() { +// return signer; +// } +// } +// +// private static void signWholeFile(JarFile inputJar, File publicKeyFile, +// X509Certificate publicKey, PrivateKey privateKey, +// int hash, long timestamp, +// OutputStream outputStream) throws Exception { +// CMSSigner cmsOut = new CMSSigner(inputJar, publicKeyFile, +// publicKey, privateKey, hash, timestamp, outputStream); +// +// ByteArrayOutputStream temp = new ByteArrayOutputStream(); +// +// // put a readable message and a null char at the start of the +// // archive comment, so that tools that display the comment +// // (hopefully) show something sensible. +// // TODO: anything more useful we can put in this message? +// byte[] message = "signed by SignApk".getBytes("UTF-8"); +// temp.write(message); +// temp.write(0); +// +// cmsOut.writeSignatureBlock(temp); +// +// byte[] zipData = cmsOut.getSigner().getTail(); +// +// // For a zip with no archive comment, the +// // end-of-central-directory record will be 22 bytes long, so +// // we expect to find the EOCD marker 22 bytes from the end. +// if (zipData[zipData.length-22] != 0x50 || +// zipData[zipData.length-21] != 0x4b || +// zipData[zipData.length-20] != 0x05 || +// zipData[zipData.length-19] != 0x06) { +// throw new IllegalArgumentException("zip data already has an archive comment"); +// } +// +// int total_size = temp.size() + 6; +// if (total_size > 0xffff) { +// throw new IllegalArgumentException("signature is too big for ZIP file comment"); +// } +// // signature starts this many bytes from the end of the file +// int signature_start = total_size - message.length - 1; +// temp.write(signature_start & 0xff); +// temp.write((signature_start >> 8) & 0xff); +// // Why the 0xff bytes? In a zip file with no archive comment, +// // bytes [-6:-2] of the file are the little-endian offset from +// // the start of the file to the central directory. So for the +// // two high bytes to be 0xff 0xff, the archive would have to +// // be nearly 4GB in size. So it's unlikely that a real +// // commentless archive would have 0xffs here, and lets us tell +// // an old signed archive from a new one. +// temp.write(0xff); +// temp.write(0xff); +// temp.write(total_size & 0xff); +// temp.write((total_size >> 8) & 0xff); +// temp.flush(); +// +// // Signature verification checks that the EOCD header is the +// // last such sequence in the file (to avoid minzip finding a +// // fake EOCD appended after the signature in its scan). The +// // odds of producing this sequence by chance are very low, but +// // let's catch it here if it does. +// byte[] b = temp.toByteArray(); +// for (int i = 0; i < b.length-3; ++i) { +// if (b[i] == 0x50 && b[i+1] == 0x4b && b[i+2] == 0x05 && b[i+3] == 0x06) { +// throw new IllegalArgumentException("found spurious EOCD header at " + i); +// } +// } +// +// outputStream.write(total_size & 0xff); +// outputStream.write((total_size >> 8) & 0xff); +// temp.writeTo(outputStream); +// } +// +// /** +// * Tries to load a JSE Provider by class name. This is for custom PrivateKey +// * types that might be stored in PKCS#11-like storage. +// */ +// private static void loadProviderIfNecessary(String providerClassName) { +// if (providerClassName == null) { +// return; +// } +// +// final Class klass; +// try { +// final ClassLoader sysLoader = ClassLoader.getSystemClassLoader(); +// if (sysLoader != null) { +// klass = sysLoader.loadClass(providerClassName); +// } else { +// klass = Class.forName(providerClassName); +// } +// } catch (ClassNotFoundException e) { +// e.printStackTrace(); +// System.exit(1); +// return; +// } +// +// Constructor constructor = null; +// for (Constructor c : klass.getConstructors()) { +// if (c.getParameterTypes().length == 0) { +// constructor = c; +// break; +// } +// } +// if (constructor == null) { +// System.err.println("No zero-arg constructor found for " + providerClassName); +// System.exit(1); +// return; +// } +// +// final Object o; +// try { +// o = constructor.newInstance(); +// } catch (Exception e) { +// e.printStackTrace(); +// System.exit(1); +// return; +// } +// if (!(o instanceof Provider)) { +// System.err.println("Not a Provider class: " + providerClassName); +// System.exit(1); +// } +// +// Security.insertProviderAt((Provider) o, 1); +// } +// +// private static List createSignerConfigs( +// PrivateKey[] privateKeys, X509Certificate[] certificates) { +// if (privateKeys.length != certificates.length) { +// throw new IllegalArgumentException( +// "The number of private keys must match the number of certificates: " +// + privateKeys.length + " vs" + certificates.length); +// } +// List signerConfigs = new ArrayList<>(); +// String signerNameFormat = (privateKeys.length == 1) ? "CERT" : "CERT%s"; +// for (int i = 0; i < privateKeys.length; i++) { +// String signerName = String.format(Locale.US, signerNameFormat, (i + 1)); +// DefaultApkSignerEngine.SignerConfig signerConfig = +// new DefaultApkSignerEngine.SignerConfig.Builder( +// signerName, +// privateKeys[i], +// Collections.singletonList(certificates[i])) +// .build(); +// signerConfigs.add(signerConfig); +// } +// return signerConfigs; +// } +// +// private static class ZipSections { +// ByteBuffer beforeCentralDir; +// ByteBuffer centralDir; +// ByteBuffer eocd; +// } +// +// private static ZipSections findMainZipSections(ByteBuffer apk) +// throws IOException, ZipFormatException { +// apk.slice(); +// ApkUtils.ZipSections sections = ApkUtils.findZipSections(DataSources.asDataSource(apk)); +// long centralDirStartOffset = sections.getZipCentralDirectoryOffset(); +// long centralDirSizeBytes = sections.getZipCentralDirectorySizeBytes(); +// long centralDirEndOffset = centralDirStartOffset + centralDirSizeBytes; +// long eocdStartOffset = sections.getZipEndOfCentralDirectoryOffset(); +// if (centralDirEndOffset != eocdStartOffset) { +// throw new ZipFormatException( +// "ZIP Central Directory is not immediately followed by End of Central Directory" +// + ". CD end: " + centralDirEndOffset +// + ", EoCD start: " + eocdStartOffset); +// } +// apk.position(0); +// apk.limit((int) centralDirStartOffset); +// ByteBuffer beforeCentralDir = apk.slice(); +// +// apk.position((int) centralDirStartOffset); +// apk.limit((int) centralDirEndOffset); +// ByteBuffer centralDir = apk.slice(); +// +// apk.position((int) eocdStartOffset); +// apk.limit(apk.capacity()); +// ByteBuffer eocd = apk.slice(); +// +// apk.position(0); +// apk.limit(apk.capacity()); +// +// ZipSections result = new ZipSections(); +// result.beforeCentralDir = beforeCentralDir; +// result.centralDir = centralDir; +// result.eocd = eocd; +// return result; +// } +// +// private static void usage() { +// System.err.println("Usage: signapk [-w] " + +// "[-a ] " + +// "[-providerClass ] " + +// "[--min-sdk-version ] " + +// "[--disable-v2] " + +// "publickey.x509[.pem] privatekey.pk8 " + +// "[publickey2.x509[.pem] privatekey2.pk8 ...] " + +// "input.jar output.jar"); +// System.exit(2); +// } +// +// public static void main(String[] args) { +// if (args.length < 4) usage(); +// +// // Install Conscrypt as the highest-priority provider. Its crypto primitives are faster than +// // the standard or Bouncy Castle ones. +// Security.insertProviderAt(new OpenSSLProvider(), 1); +// // Install Bouncy Castle (as the lowest-priority provider) because Conscrypt does not offer +// // DSA which may still be needed. +// // TODO: Stop installing Bouncy Castle provider once DSA is no longer needed. +// Security.addProvider(new BouncyCastleProvider()); +// +// boolean signWholeFile = false; +// String providerClass = null; +// int alignment = 4; +// int minSdkVersion = 0; +// boolean signUsingApkSignatureSchemeV2 = true; +// +// int argstart = 0; +// while (argstart < args.length && args[argstart].startsWith("-")) { +// if ("-w".equals(args[argstart])) { +// signWholeFile = true; +// ++argstart; +// } else if ("-providerClass".equals(args[argstart])) { +// if (argstart + 1 >= args.length) { +// usage(); +// } +// providerClass = args[++argstart]; +// ++argstart; +// } else if ("-a".equals(args[argstart])) { +// alignment = Integer.parseInt(args[++argstart]); +// ++argstart; +// } else if ("--min-sdk-version".equals(args[argstart])) { +// String minSdkVersionString = args[++argstart]; +// try { +// minSdkVersion = Integer.parseInt(minSdkVersionString); +// } catch (NumberFormatException e) { +// throw new IllegalArgumentException( +// "--min-sdk-version must be a decimal number: " + minSdkVersionString); +// } +// ++argstart; +// } else if ("--disable-v2".equals(args[argstart])) { +// signUsingApkSignatureSchemeV2 = false; +// ++argstart; +// } else { +// usage(); +// } +// } +// +// if ((args.length - argstart) % 2 == 1) usage(); +// int numKeys = ((args.length - argstart) / 2) - 1; +// if (signWholeFile && numKeys > 1) { +// System.err.println("Only one key may be used with -w."); +// System.exit(2); +// } +// +// loadProviderIfNecessary(providerClass); +// +// String inputFilename = args[args.length-2]; +// String outputFilename = args[args.length-1]; +// +// JarFile inputJar = null; +// FileOutputStream outputFile = null; +// +// try { +// File firstPublicKeyFile = new File(args[argstart+0]); +// +// X509Certificate[] publicKey = new X509Certificate[numKeys]; +// try { +// for (int i = 0; i < numKeys; ++i) { +// int argNum = argstart + i*2; +// publicKey[i] = readPublicKey(new File(args[argNum])); +// } +// } catch (IllegalArgumentException e) { +// System.err.println(e); +// System.exit(1); +// } +// +// // Set all ZIP file timestamps to Jan 1 2009 00:00:00. +// long timestamp = 1230768000000L; +// // The Java ZipEntry API we're using converts milliseconds since epoch into MS-DOS +// // timestamp using the current timezone. We thus adjust the milliseconds since epoch +// // value to end up with MS-DOS timestamp of Jan 1 2009 00:00:00. +// timestamp -= TimeZone.getDefault().getOffset(timestamp); +// +// PrivateKey[] privateKey = new PrivateKey[numKeys]; +// for (int i = 0; i < numKeys; ++i) { +// int argNum = argstart + i*2 + 1; +// privateKey[i] = readPrivateKey(new File(args[argNum])); +// } +// inputJar = new JarFile(new File(inputFilename), false); // Don't verify. +// +// outputFile = new FileOutputStream(outputFilename); +// +// // NOTE: Signing currently recompresses any compressed entries using Deflate (default +// // compression level for OTA update files and maximum compession level for APKs). +// if (signWholeFile) { +// int digestAlgorithm = getDigestAlgorithmForOta(publicKey[0]); +// signWholeFile(inputJar, firstPublicKeyFile, +// publicKey[0], privateKey[0], digestAlgorithm, +// timestamp, +// outputFile); +// } else { +// try (ApkSignerEngine apkSigner = +// new DefaultApkSignerEngine.Builder( +// createSignerConfigs(privateKey, publicKey), minSdkVersion) +// .setV1SigningEnabled(true) +// .setV2SigningEnabled(signUsingApkSignatureSchemeV2) +// .setOtherSignersSignaturesPreserved(false) +// .build()) { +// // We don't preserve the input APK's APK Signing Block (which contains v2 +// // signatures) +// apkSigner.inputApkSigningBlock(null); +// +// // Build the output APK in memory, by copying input APK's ZIP entries across +// // and then signing the output APK. +// ByteArrayOutputStream v1SignedApkBuf = new ByteArrayOutputStream(); +// JarOutputStream outputJar = new JarOutputStream(v1SignedApkBuf); +// // Use maximum compression for compressed entries because the APK lives forever +// // on the system partition. +// outputJar.setLevel(9); +// copyFiles(inputJar, null, apkSigner, outputJar, timestamp, alignment); +// ApkSignerEngine.OutputJarSignatureRequest addV1SignatureRequest = +// apkSigner.outputJarEntries(); +// if (addV1SignatureRequest != null) { +// addV1Signature(apkSigner, addV1SignatureRequest, outputJar, timestamp); +// addV1SignatureRequest.done(); +// } +// outputJar.close(); +// ByteBuffer v1SignedApk = ByteBuffer.wrap(v1SignedApkBuf.toByteArray()); +// v1SignedApkBuf.reset(); +// ByteBuffer[] outputChunks = new ByteBuffer[] {v1SignedApk}; +// +// ZipSections zipSections = findMainZipSections(v1SignedApk); +// ApkSignerEngine.OutputApkSigningBlockRequest addV2SignatureRequest = +// apkSigner.outputZipSections( +// DataSources.asDataSource(zipSections.beforeCentralDir), +// DataSources.asDataSource(zipSections.centralDir), +// DataSources.asDataSource(zipSections.eocd)); +// if (addV2SignatureRequest != null) { +// // Need to insert the returned APK Signing Block before ZIP Central +// // Directory. +// byte[] apkSigningBlock = addV2SignatureRequest.getApkSigningBlock(); +// // Because the APK Signing Block is inserted before the Central Directory, +// // we need to adjust accordingly the offset of Central Directory inside the +// // ZIP End of Central Directory (EoCD) record. +// ByteBuffer modifiedEocd = ByteBuffer.allocate(zipSections.eocd.remaining()); +// modifiedEocd.put(zipSections.eocd); +// modifiedEocd.flip(); +// modifiedEocd.order(ByteOrder.LITTLE_ENDIAN); +// ApkUtils.setZipEocdCentralDirectoryOffset( +// modifiedEocd, +// zipSections.beforeCentralDir.remaining() + apkSigningBlock.length); +// outputChunks = +// new ByteBuffer[] { +// zipSections.beforeCentralDir, +// ByteBuffer.wrap(apkSigningBlock), +// zipSections.centralDir, +// modifiedEocd}; +// addV2SignatureRequest.done(); +// } +// +// // This assumes outputChunks are array-backed. To avoid this assumption, the +// // code could be rewritten to use FileChannel. +// for (ByteBuffer outputChunk : outputChunks) { +// outputFile.write( +// outputChunk.array(), +// outputChunk.arrayOffset() + outputChunk.position(), +// outputChunk.remaining()); +// outputChunk.position(outputChunk.limit()); +// } +// +// outputFile.close(); +// outputFile = null; +// apkSigner.outputDone(); +// } +// +// return; +// } +// } catch (Exception e) { +// e.printStackTrace(); +// System.exit(1); +// } finally { +// try { +// if (inputJar != null) inputJar.close(); +// if (outputFile != null) outputFile.close(); +// } catch (IOException e) { +// e.printStackTrace(); +// System.exit(1); +// } +// } +// } +//} diff --git a/plugin/src/main/resources/META-INF/gradle-plugins/walle.properties b/plugin/src/main/resources/META-INF/gradle-plugins/walle.properties new file mode 100644 index 0000000..97647ca --- /dev/null +++ b/plugin/src/main/resources/META-INF/gradle-plugins/walle.properties @@ -0,0 +1 @@ +implementation-class=com.meituan.android.walle.GradlePlugin \ No newline at end of file diff --git a/sample/app/.gitignore b/sample/app/.gitignore new file mode 100644 index 0000000..c125ebd --- /dev/null +++ b/sample/app/.gitignore @@ -0,0 +1,3 @@ +/build + +/version.properties diff --git a/sample/app/build.gradle b/sample/app/build.gradle new file mode 100644 index 0000000..24e7b03 --- /dev/null +++ b/sample/app/build.gradle @@ -0,0 +1,53 @@ +apply plugin: 'com.android.application' +apply plugin: 'walle' + +android { + + compileSdkVersion 24 + buildToolsVersion '24.0.1' + + defaultConfig { + minSdkVersion 16 + targetSdkVersion 24 + versionCode 1 + versionName "0.0.1" + applicationId "com.meituan.android.walle.sample" + } + + signingConfigs { + sankuai { + storeFile file("keystore/meituan-debug.keystore") + storePassword "1234567" + keyAlias "meituan" + keyPassword "12345678" + } + } + + buildTypes { + release { + minifyEnabled true + shrinkResources true + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + + signingConfig signingConfigs.sankuai + } + + debug { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + + signingConfig signingConfigs.sankuai + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_7 + targetCompatibility JavaVersion.VERSION_1_7 + } +} + +dependencies { + compile fileTree(dir: 'libs', include: ['*.jar']) + compile 'com.android.support:support-v4:24.1.1' + compile 'com.android.support:appcompat-v7:24.1.1' +} diff --git a/sample/app/keystore/meituan-debug.keystore b/sample/app/keystore/meituan-debug.keystore new file mode 100644 index 0000000..4d52fb7 Binary files /dev/null and b/sample/app/keystore/meituan-debug.keystore differ diff --git a/sample/app/proguard-rules.pro b/sample/app/proguard-rules.pro new file mode 100644 index 0000000..36186f4 --- /dev/null +++ b/sample/app/proguard-rules.pro @@ -0,0 +1,19 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /Users/zhangshaowen/Library/Android/sdk/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} +-keepattributes SourceFile,LineNumberTable + diff --git a/sample/app/src/main/AndroidManifest.xml b/sample/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..a3dcdde --- /dev/null +++ b/sample/app/src/main/AndroidManifest.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + diff --git a/sample/app/src/main/java/com/meituan/android/walle/sample/MainActivity.java b/sample/app/src/main/java/com/meituan/android/walle/sample/MainActivity.java new file mode 100644 index 0000000..fe57d04 --- /dev/null +++ b/sample/app/src/main/java/com/meituan/android/walle/sample/MainActivity.java @@ -0,0 +1,12 @@ +package com.meituan.android.walle.sample; + +import android.os.Bundle; +import android.support.v7.app.AppCompatActivity; + +public class MainActivity extends AppCompatActivity { + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + } +} diff --git a/sample/app/src/main/java/com/meituan/android/walle/sample/MyApplication.java b/sample/app/src/main/java/com/meituan/android/walle/sample/MyApplication.java new file mode 100644 index 0000000..1922f0d --- /dev/null +++ b/sample/app/src/main/java/com/meituan/android/walle/sample/MyApplication.java @@ -0,0 +1,8 @@ + +package com.meituan.android.walle.sample; + +import android.app.Application; + +public class MyApplication extends Application { + +} diff --git a/sample/app/src/main/res/layout/activity_main.xml b/sample/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..f9a033c --- /dev/null +++ b/sample/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,32 @@ + + + + + + + + + diff --git a/sample/app/src/main/res/mipmap-hdpi/ic_launcher.png b/sample/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..cde69bc Binary files /dev/null and b/sample/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/sample/app/src/main/res/mipmap-mdpi/ic_launcher.png b/sample/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..c133a0c Binary files /dev/null and b/sample/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/sample/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/sample/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..bfa42f0 Binary files /dev/null and b/sample/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/sample/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/sample/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..324e72c Binary files /dev/null and b/sample/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/sample/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/sample/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..aee44e1 Binary files /dev/null and b/sample/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/sample/app/src/main/res/values-w820dp/dimens.xml b/sample/app/src/main/res/values-w820dp/dimens.xml new file mode 100644 index 0000000..63fc816 --- /dev/null +++ b/sample/app/src/main/res/values-w820dp/dimens.xml @@ -0,0 +1,6 @@ + + + 64dp + diff --git a/sample/app/src/main/res/values/colors.xml b/sample/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..3ab3e9c --- /dev/null +++ b/sample/app/src/main/res/values/colors.xml @@ -0,0 +1,6 @@ + + + #3F51B5 + #303F9F + #FF4081 + diff --git a/sample/app/src/main/res/values/dimens.xml b/sample/app/src/main/res/values/dimens.xml new file mode 100644 index 0000000..47c8224 --- /dev/null +++ b/sample/app/src/main/res/values/dimens.xml @@ -0,0 +1,5 @@ + + + 16dp + 16dp + diff --git a/sample/app/src/main/res/values/strings.xml b/sample/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..facfe8a --- /dev/null +++ b/sample/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + Walle-Sample + diff --git a/sample/app/src/main/res/values/styles.xml b/sample/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..391ec9a --- /dev/null +++ b/sample/app/src/main/res/values/styles.xml @@ -0,0 +1,10 @@ + + + + + diff --git a/sample/build.gradle b/sample/build.gradle new file mode 100644 index 0000000..11130f4 --- /dev/null +++ b/sample/build.gradle @@ -0,0 +1,19 @@ + +buildscript { + repositories { + mavenLocal() + jcenter() + } + dependencies { + classpath 'com.android.tools.build:gradle:2.2.0' + classpath "com.meituan.android.walle:plugin:${WALLE_PLUGIN_VERSION}" + } +} + +allprojects { + repositories { + mavenLocal() + jcenter() + } +} + diff --git a/sample/gradle.properties b/sample/gradle.properties new file mode 100644 index 0000000..429e196 --- /dev/null +++ b/sample/gradle.properties @@ -0,0 +1,21 @@ +# Project-wide Gradle settings. + +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. + +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html + +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +# Default value: -Xmx10248m -XX:MaxPermSize=256m + org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=1024m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 + +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true + +WALLE_PLUGIN_VERSION=0.0.1 + diff --git a/sample/gradle/wrapper/gradle-wrapper.jar b/sample/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..05ef575 Binary files /dev/null and b/sample/gradle/wrapper/gradle-wrapper.jar differ diff --git a/sample/gradle/wrapper/gradle-wrapper.properties b/sample/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..32352bc --- /dev/null +++ b/sample/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,4 @@ +#Wed Oct 21 11:34:03 PDT 2015 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip diff --git a/sample/gradlew b/sample/gradlew new file mode 100755 index 0000000..9d82f78 --- /dev/null +++ b/sample/gradlew @@ -0,0 +1,160 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/sample/gradlew.bat b/sample/gradlew.bat new file mode 100644 index 0000000..8a0b282 --- /dev/null +++ b/sample/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/sample/settings.gradle b/sample/settings.gradle new file mode 100644 index 0000000..e7b4def --- /dev/null +++ b/sample/settings.gradle @@ -0,0 +1 @@ +include ':app' diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..d796d23 --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +include ':plugin', ':library'