diff --git a/.gitignore b/.gitignore index a219f7aaa..d692faa0a 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ obj/ libs/ *.zip *.jks +*.apk -# Copied binaries +# Built binaries ziptools/zipadjust diff --git a/.gitmodules b/.gitmodules index c12cb1cb2..d87eda592 100644 --- a/.gitmodules +++ b/.gitmodules @@ -11,7 +11,7 @@ path = jni/magiskpolicy url = https://github.com/topjohnwu/magiskpolicy.git [submodule "MagiskManager"] - path = MagiskManager + path = java url = https://github.com/topjohnwu/MagiskManager.git [submodule "jni/busybox"] path = jni/external/busybox diff --git a/MagiskManager b/MagiskManager deleted file mode 160000 index 773c24b7f..000000000 --- a/MagiskManager +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 773c24b7fc7e6931c0932cd53943813eb4ebd027 diff --git a/README.MD b/README.MD index 7b587ab15..e7c460cf5 100644 --- a/README.MD +++ b/README.MD @@ -45,7 +45,7 @@ along with this program. If not, see . ## Credits -**MagiskManager** (`MagiskManager`) +**MagiskManager** (`java`) * Copyright 2016-2017, John Wu (@topjohnwu) * All contributors and translators diff --git a/build.py b/build.py index 826c804dd..9a0d13644 100755 --- a/build.py +++ b/build.py @@ -51,6 +51,10 @@ def zip_with_msg(zipfile, source, target): print('zip: {} -> {}'.format(source, target)) zipfile.write(source, target) +def cp(source, target): + print('cp: {} -> {}'.format(source, target)) + shutil.copyfile(source, target) + def build_all(args): build_binary(args) build_apk(args) @@ -75,26 +79,23 @@ def build_apk(args): for key in ['public.certificate.x509.pem', 'private.key.pk8']: source = os.path.join('ziptools', key) - target = os.path.join('MagiskManager', 'app', 'src', 'main', 'assets', key) - print('cp: {} -> {}'.format(source, target)) - shutil.copyfile(source, target) + target = os.path.join('java', 'app', 'src', 'main', 'assets', key) + cp(source, target) for script in ['magisk_uninstaller.sh', 'util_functions.sh']: source = os.path.join('scripts', script) - target = os.path.join('MagiskManager', 'app', 'src', 'main', 'assets', script) - print('cp: {} -> {}'.format(source, target)) - shutil.copyfile(source, target) + target = os.path.join('java', 'app', 'src', 'main', 'assets', script) + cp(source, target) - os.chdir('MagiskManager') + os.chdir('java') # Build unhide app and place in assets - proc = subprocess.run('{} unhide::assembleRelease'.format(os.path.join('.', 'gradlew')), shell=True) + proc = subprocess.run('{} unhide:assembleRelease'.format(os.path.join('.', 'gradlew')), shell=True) if proc.returncode != 0: error('Build Magisk Manager failed!') source = os.path.join('unhide', 'build', 'outputs', 'apk', 'release', 'unhide-release-unsigned.apk') target = os.path.join('app', 'src', 'main', 'assets', 'unhide.apk') - print('cp: {} -> {}'.format(source, target)) - shutil.copyfile(source, target) + cp(source, target) print('') @@ -102,7 +103,7 @@ def build_apk(args): if not os.path.exists(os.path.join('..', 'release_signature.jks')): error('Please generate a java keystore and place it in \'release_signature.jks\'') - proc = subprocess.run('{} app::assembleRelease'.format(os.path.join('.', 'gradlew')), shell=True) + proc = subprocess.run('{} app:assembleRelease'.format(os.path.join('.', 'gradlew')), shell=True) if proc.returncode != 0: error('Build Magisk Manager failed!') @@ -141,16 +142,26 @@ def build_apk(args): silentremove(unsigned) silentremove(aligned) else: - proc = subprocess.run('{} app::assembleDebug'.format(os.path.join('.', 'gradlew')), shell=True) + proc = subprocess.run('{} app:assembleDebug'.format(os.path.join('.', 'gradlew')), shell=True) if proc.returncode != 0: error('Build Magisk Manager failed!') # Return to upper directory os.chdir('..') -def sign_adjust_zip(unsigned, output): +def build_snet(args): + os.chdir('java') + proc = subprocess.run('{} snet:assembleRelease'.format(os.path.join('.', 'gradlew')), shell=True) + if proc.returncode != 0: + error('Build snet extention failed!') + source = os.path.join('snet', 'build', 'outputs', 'apk', 'release', 'snet-release-unsigned.apk') + target = os.path.join('..', 'snet.apk') + print('') + cp(source, target) + os.chdir('..') - zipsigner = os.path.join('ziptools', 'zipsigner', 'build', 'libs', 'zipsigner.jar') +def sign_adjust_zip(unsigned, output): + jarsigner = os.path.join('java', 'jarsigner', 'build', 'libs', 'jarsigner-fat.jar') if os.name != 'nt' and not os.path.exists(os.path.join('ziptools', 'zipadjust')): header('* Building zipadjust') @@ -158,13 +169,13 @@ def sign_adjust_zip(unsigned, output): proc = subprocess.run('gcc -o ziptools/zipadjust ziptools/zipadjust_src/*.c -lz', shell=True) if proc.returncode != 0: error('Build zipadjust failed!') - if not os.path.exists(zipsigner): - header('* Building zipsigner.jar') - os.chdir(os.path.join('ziptools', 'zipsigner')) - proc = subprocess.run('{} shadowJar'.format(os.path.join('.', 'gradlew')), shell=True) + if not os.path.exists(jarsigner): + header('* Building jarsigner-fat.jar') + os.chdir('java') + proc = subprocess.run('{} jarsigner:shadowJar'.format(os.path.join('.', 'gradlew')), shell=True) if proc.returncode != 0: - error('Build zipsigner.jar failed!') - os.chdir(os.path.join('..', '..')) + error('Build jarsigner-fat.jar failed!') + os.chdir('..') header('* Signing / Adjusting Zip') @@ -172,7 +183,7 @@ def sign_adjust_zip(unsigned, output): privateKey = os.path.join('ziptools', 'private.key.pk8') # Unsigned->signed - proc = subprocess.run(['java', '-jar', zipsigner, + proc = subprocess.run(['java', '-jar', jarsigner, publicKey, privateKey, unsigned, 'tmp_signed.zip']) if proc.returncode != 0: error('First sign flashable zip failed!') @@ -183,7 +194,7 @@ def sign_adjust_zip(unsigned, output): error('Adjust flashable zip failed!') # Adjusted -> output - proc = subprocess.run(['java', '-jar', zipsigner, + proc = subprocess.run(['java', '-jar', jarsigner, "-m", publicKey, privateKey, 'tmp_adjusted.zip', output]) if proc.returncode != 0: error('Second sign flashable zip failed!') @@ -243,7 +254,7 @@ def zip_main(args): zip_with_msg(zipf, source, target) # APK - source = os.path.join('MagiskManager', 'app', 'build', 'outputs', 'apk', + source = os.path.join('java', 'app', 'build', 'outputs', 'apk', 'release' if args.release else 'debug', 'app-release.apk' if args.release else 'app-debug.apk') target = os.path.join('common', 'magisk.apk') zip_with_msg(zipf, source, target) @@ -328,20 +339,20 @@ def zip_uninstaller(args): def cleanup(args): if len(args.target) == 0: - args.target = ['binary', 'apk', 'zip'] + args.target = ['binary', 'java', 'zip'] if 'binary' in args.target: - header('* Cleaning Magisk binaries') + header('* Cleaning binaries') subprocess.run(os.path.join(os.environ['ANDROID_HOME'], 'ndk-bundle', 'ndk-build') + ' clean', shell=True) - if 'apk' in args.target: - header('* Cleaning Magisk Manager') - os.chdir('MagiskManager') + if 'java' in args.target: + header('* Cleaning java') + os.chdir('java') subprocess.run('{} clean'.format(os.path.join('.', 'gradlew')), shell=True) os.chdir('..') if 'zip' in args.target: - header('* Cleaning created zip files') + header('* Cleaning zip files') for f in os.listdir('.'): if '.zip' in f: print('rm {}'.format(f)) @@ -364,6 +375,9 @@ binary_parser.set_defaults(func=build_binary) apk_parser = subparsers.add_parser('apk', help='build Magisk Manager APK') apk_parser.set_defaults(func=build_apk) +snet_parser = subparsers.add_parser('snet', help='build snet extention for Magisk Manager') +snet_parser.set_defaults(func=build_snet) + zip_parser = subparsers.add_parser('zip', help='zip and sign Magisk into a flashable zip') zip_parser.add_argument('versionString') zip_parser.add_argument('versionCode', type=int) @@ -372,7 +386,7 @@ zip_parser.set_defaults(func=zip_main) uninstaller_parser = subparsers.add_parser('uninstaller', help='create flashable uninstaller') uninstaller_parser.set_defaults(func=zip_uninstaller) -clean_parser = subparsers.add_parser('clean', help='clean [target...] targets: binary apk zip') +clean_parser = subparsers.add_parser('clean', help='clean [target...] targets: binary java zip') clean_parser.add_argument('target', nargs='*') clean_parser.set_defaults(func=cleanup) diff --git a/java b/java new file mode 160000 index 000000000..39b6df27b --- /dev/null +++ b/java @@ -0,0 +1 @@ +Subproject commit 39b6df27b3d72c706010e6773d4bb50a202a1543 diff --git a/ziptools/zipsigner/.gitignore b/ziptools/zipsigner/.gitignore deleted file mode 100644 index 87dc02017..000000000 --- a/ziptools/zipsigner/.gitignore +++ /dev/null @@ -1,7 +0,0 @@ -*.iml -.gradle -/local.properties -.idea/ -/build -*.hprof -app/.externalNativeBuild/ diff --git a/ziptools/zipsigner/build.gradle b/ziptools/zipsigner/build.gradle deleted file mode 100644 index 60879d23a..000000000 --- a/ziptools/zipsigner/build.gradle +++ /dev/null @@ -1,36 +0,0 @@ -group 'com.topjohnwu' -version '1.0.0' - -apply plugin: 'java' -apply plugin: 'com.github.johnrengelman.shadow' - -sourceCompatibility = 1.8 - -jar { - manifest { - attributes 'Main-Class': 'com.topjohnwu.ZipSigner' - } -} - -shadowJar { - classifier = null - version = null -} - -buildscript { - repositories { - jcenter() - } - dependencies { - classpath 'com.github.jengelman.gradle.plugins:shadow:2.0.1' - } -} - -repositories { - mavenCentral() -} - -dependencies { - compile 'org.bouncycastle:bcprov-jdk15on:1.57' - compile 'org.bouncycastle:bcpkix-jdk15on:1.57' -} diff --git a/ziptools/zipsigner/gradle/wrapper/gradle-wrapper.jar b/ziptools/zipsigner/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index ccfe89381..000000000 Binary files a/ziptools/zipsigner/gradle/wrapper/gradle-wrapper.jar and /dev/null differ diff --git a/ziptools/zipsigner/gradle/wrapper/gradle-wrapper.properties b/ziptools/zipsigner/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 1d882d3e6..000000000 --- a/ziptools/zipsigner/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,6 +0,0 @@ -#Thu Aug 24 10:35:40 CST 2017 -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-3.5-rc-2-bin.zip diff --git a/ziptools/zipsigner/gradlew b/ziptools/zipsigner/gradlew deleted file mode 100755 index 4453ccea3..000000000 --- a/ziptools/zipsigner/gradlew +++ /dev/null @@ -1,172 +0,0 @@ -#!/usr/bin/env sh - -############################################################################## -## -## Gradle start up script for UN*X -## -############################################################################## - -# 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 - -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" - -# 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 -nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; -esac - -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" -a "$nonstop" = "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 - -# Escape application args -save ( ) { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=$(save "$@") - -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" - -# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong -if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then - cd "$(dirname "$0")" -fi - -exec "$JAVACMD" "$@" diff --git a/ziptools/zipsigner/gradlew.bat b/ziptools/zipsigner/gradlew.bat deleted file mode 100644 index f9553162f..000000000 --- a/ziptools/zipsigner/gradlew.bat +++ /dev/null @@ -1,84 +0,0 @@ -@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 - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@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= - -@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 Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_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=%* - -: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/ziptools/zipsigner/settings.gradle b/ziptools/zipsigner/settings.gradle deleted file mode 100644 index a42023557..000000000 --- a/ziptools/zipsigner/settings.gradle +++ /dev/null @@ -1,4 +0,0 @@ -rootProject.name = 'zipsigner' -include 'apksigner' -rootProject.name = 'zipsigner' - diff --git a/ziptools/zipsigner/src/main/java/com/topjohnwu/ZipSigner.java b/ziptools/zipsigner/src/main/java/com/topjohnwu/ZipSigner.java deleted file mode 100644 index bf7245314..000000000 --- a/ziptools/zipsigner/src/main/java/com/topjohnwu/ZipSigner.java +++ /dev/null @@ -1,567 +0,0 @@ -package com.topjohnwu; - -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.CMSProcessableByteArray; -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.bouncycastle.util.encoders.Base64; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -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.OutputStream; -import java.io.PrintStream; -import java.security.DigestOutputStream; -import java.security.GeneralSecurityException; -import java.security.KeyFactory; -import java.security.MessageDigest; -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.PKCS8EncodedKeySpec; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Enumeration; -import java.util.Locale; -import java.util.Map; -import java.util.TreeMap; -import java.util.jar.Attributes; -import java.util.jar.JarEntry; -import java.util.jar.JarFile; -import java.util.jar.JarOutputStream; -import java.util.jar.Manifest; -import java.util.regex.Pattern; - -/* -* Modified from from AOSP(Marshmallow) SignAPK.java -* */ - -public class ZipSigner { - private static final String CERT_SF_NAME = "META-INF/CERT.SF"; - private static final String CERT_SIG_NAME = "META-INF/CERT.%s"; - - private static Provider sBouncyCastleProvider; - - // 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; - - /** - * Return one of USE_SHA1 or USE_SHA256 according to the signature - * algorithm specified in the cert. - */ - private static int getDigestAlgorithm(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 expected signature algorithm for this key type. */ - private static String getSignatureAlgorithm(X509Certificate cert) { - String sigAlg = cert.getSigAlgName().toUpperCase(Locale.US); - String keyType = cert.getPublicKey().getAlgorithm().toUpperCase(Locale.US); - if ("RSA".equalsIgnoreCase(keyType)) { - if (getDigestAlgorithm(cert) == USE_SHA256) { - return "SHA256withRSA"; - } else { - return "SHA1withRSA"; - } - } else if ("EC".equalsIgnoreCase(keyType)) { - return "SHA256withECDSA"; - } else { - throw new IllegalArgumentException("unsupported key type: " + keyType); - } - } - // Files matching this pattern are not copied to the output. - private static Pattern stripPattern = - Pattern.compile("^(META-INF/((.*)[.](SF|RSA|DSA|EC)|com/android/otacert))|(" + - Pattern.quote(JarFile.MANIFEST_NAME) + ")$"); - private static X509Certificate readPublicKey(InputStream input) - throws IOException, GeneralSecurityException { - try { - CertificateFactory cf = CertificateFactory.getInstance("X.509"); - return (X509Certificate) cf.generateCertificate(input); - } finally { - input.close(); - } - } - - /** Read a PKCS#8 format private key. */ - private static PrivateKey readPrivateKey(InputStream input) - throws IOException, GeneralSecurityException { - try { - byte[] buffer = new byte[4096]; - int size = input.read(buffer); - byte[] bytes = Arrays.copyOf(buffer, size); - /* Check to see if this is in an EncryptedPrivateKeyInfo structure. */ - PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(bytes); - /* - * Now it's in a PKCS#8 PrivateKeyInfo structure. Read its Algorithm - * OID and use that to construct a KeyFactory. - */ - ASN1InputStream bIn = new ASN1InputStream(new ByteArrayInputStream(spec.getEncoded())); - PrivateKeyInfo pki = PrivateKeyInfo.getInstance(bIn.readObject()); - String algOid = pki.getPrivateKeyAlgorithm().getAlgorithm().getId(); - return KeyFactory.getInstance(algOid).generatePrivate(spec); - } finally { - input.close(); - } - } - /** - * Add the hash(es) of every file to the manifest, creating it if - * necessary. - */ - private static Manifest addDigestsToManifest(JarFile jar, int hashes) - throws IOException, GeneralSecurityException { - Manifest input = jar.getManifest(); - Manifest output = new Manifest(); - Attributes main = output.getMainAttributes(); - if (input != null) { - main.putAll(input.getMainAttributes()); - } else { - main.putValue("Manifest-Version", "1.0"); - main.putValue("Created-By", "1.0 (Android SignApk)"); - } - MessageDigest md_sha1 = null; - MessageDigest md_sha256 = null; - if ((hashes & USE_SHA1) != 0) { - md_sha1 = MessageDigest.getInstance("SHA1"); - } - if ((hashes & USE_SHA256) != 0) { - md_sha256 = MessageDigest.getInstance("SHA256"); - } - byte[] buffer = new byte[4096]; - int num; - // We sort the input entries by name, and add them to the - // output manifest in sorted order. We expect that the output - // map will be deterministic. - TreeMap byName = new TreeMap(); - for (Enumeration e = jar.entries(); e.hasMoreElements(); ) { - JarEntry entry = e.nextElement(); - byName.put(entry.getName(), entry); - } - for (JarEntry entry: byName.values()) { - String name = entry.getName(); - if (!entry.isDirectory() && - (stripPattern == null || !stripPattern.matcher(name).matches())) { - InputStream data = jar.getInputStream(entry); - while ((num = data.read(buffer)) > 0) { - if (md_sha1 != null) md_sha1.update(buffer, 0, num); - if (md_sha256 != null) md_sha256.update(buffer, 0, num); - } - Attributes attr = null; - if (input != null) attr = input.getAttributes(name); - attr = attr != null ? new Attributes(attr) : new Attributes(); - if (md_sha1 != null) { - attr.putValue("SHA1-Digest", - new String(Base64.encode(md_sha1.digest()), "ASCII")); - } - if (md_sha256 != null) { - attr.putValue("SHA-256-Digest", - new String(Base64.encode(md_sha256.digest()), "ASCII")); - } - output.getEntries().put(name, attr); - } - } - return output; - } - - /** Write to another stream and track how many bytes have been - * written. - */ - private static class CountOutputStream extends FilterOutputStream { - private int mCount; - public CountOutputStream(OutputStream out) { - super(out); - mCount = 0; - } - @Override - public void write(int b) throws IOException { - super.write(b); - mCount++; - } - @Override - public void write(byte[] b, int off, int len) throws IOException { - super.write(b, off, len); - mCount += len; - } - public int size() { - return mCount; - } - } - /** Write a .SF file with a digest of the specified manifest. */ - private static void writeSignatureFile(Manifest manifest, OutputStream out, - int hash) - throws IOException, GeneralSecurityException { - Manifest sf = new Manifest(); - Attributes main = sf.getMainAttributes(); - main.putValue("Signature-Version", "1.0"); - main.putValue("Created-By", "1.0 (Android SignApk)"); - MessageDigest md = MessageDigest.getInstance( - hash == USE_SHA256 ? "SHA256" : "SHA1"); - PrintStream print = new PrintStream( - new DigestOutputStream(new ByteArrayOutputStream(), md), - true, "UTF-8"); - // Digest of the entire manifest - manifest.write(print); - print.flush(); - main.putValue(hash == USE_SHA256 ? "SHA-256-Digest-Manifest" : "SHA1-Digest-Manifest", - new String(Base64.encode(md.digest()), "ASCII")); - Map entries = manifest.getEntries(); - for (Map.Entry entry : entries.entrySet()) { - // Digest of the manifest stanza for this entry. - print.print("Name: " + entry.getKey() + "\r\n"); - for (Map.Entry att : entry.getValue().entrySet()) { - print.print(att.getKey() + ": " + att.getValue() + "\r\n"); - } - print.print("\r\n"); - print.flush(); - Attributes sfAttr = new Attributes(); - sfAttr.putValue(hash == USE_SHA256 ? "SHA-256-Digest" : "SHA1-Digest-Manifest", - new String(Base64.encode(md.digest()), "ASCII")); - sf.getEntries().put(entry.getKey(), sfAttr); - } - CountOutputStream cout = new CountOutputStream(out); - sf.write(cout); - // 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 ((cout.size() % 1024) == 0) { - cout.write('\r'); - cout.write('\n'); - } - } - /** Sign data and write the digital signature to 'out'. */ - private static void writeSignatureBlock( - CMSTypedData data, X509Certificate publicKey, PrivateKey privateKey, - 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(getSignatureAlgorithm(publicKey)) - .setProvider(sBouncyCastleProvider) - .build(privateKey); - gen.addSignerInfoGenerator( - new JcaSignerInfoGeneratorBuilder( - new JcaDigestCalculatorProviderBuilder() - .setProvider(sBouncyCastleProvider) - .build()) - .setDirectSignature(true) - .build(signer, publicKey)); - gen.addCertificates(certs); - CMSSignedData sigData = gen.generate(data, false); - ASN1InputStream asn1 = new ASN1InputStream(sigData.getEncoded()); - DEROutputStream dos = new DEROutputStream(out); - dos.writeObject(asn1.readObject()); - } - /** - * Copy all the files in a manifest 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(Manifest manifest, JarFile in, JarOutputStream out, - long timestamp, int alignment) throws IOException { - byte[] buffer = new byte[4096]; - int num; - Map entries = manifest.getEntries(); - ArrayList names = new ArrayList(entries.keySet()); - 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). - for (String name : names) { - JarEntry inEntry = in.getJarEntry(name); - JarEntry outEntry = null; - if (inEntry.getMethod() != JarEntry.STORED) continue; - // Preserve the STORED method of the input entry. - outEntry = new JarEntry(inEntry); - outEntry.setTime(timestamp); - // 'offset' is the offset into the file at which we expect - // the file data to begin. This is the value we need to - // make a multiple of 'alignement'. - 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. - offset += 4; - firstEntry = false; - } - if (alignment > 0 && (offset % alignment != 0)) { - // Set the "extra data" of the entry to between 1 and - // alignment-1 bytes, to make the file data begin at - // an aligned offset. - int needed = alignment - (int)(offset % alignment); - outEntry.setExtra(new byte[needed]); - offset += needed; - } - out.putNextEntry(outEntry); - InputStream data = in.getInputStream(inEntry); - while ((num = data.read(buffer)) > 0) { - out.write(buffer, 0, num); - offset += num; - } - out.flush(); - } - // 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 : names) { - JarEntry inEntry = in.getJarEntry(name); - JarEntry outEntry = null; - if (inEntry.getMethod() == JarEntry.STORED) continue; - // Create a new entry so that the compressed len is recomputed. - outEntry = new JarEntry(name); - outEntry.setTime(timestamp); - out.putNextEntry(outEntry); - InputStream data = in.getInputStream(inEntry); - while ((num = data.read(buffer)) > 0) { - out.write(buffer, 0, num); - } - out.flush(); - } - } - - // This class is to provide a file's content, but trimming out the last two bytes - // Used for signWholeFile - private static class CMSProcessableFile implements CMSTypedData { - - private File file; - private ASN1ObjectIdentifier type; - private byte[] buffer; - int bufferSize = 0; - - CMSProcessableFile(File file) { - this.file = file; - type = new ASN1ObjectIdentifier(CMSObjectIdentifiers.data.getId()); - buffer = new byte[4096]; - } - - @Override - public ASN1ObjectIdentifier getContentType() { - return type; - } - - @Override - public void write(OutputStream out) throws IOException, CMSException { - FileInputStream input = new FileInputStream(file); - long len = file.length() - 2; - while ((bufferSize = input.read(buffer)) > 0) { - if (len <= bufferSize) { - out.write(buffer, 0, (int) len); - break; - } else { - out.write(buffer, 0, bufferSize); - } - len -= bufferSize; - } - } - - @Override - public Object getContent() { - return file; - } - - byte[] getTail() { - return Arrays.copyOfRange(buffer, 0, bufferSize); - } - } - - private static void signWholeFile(File input, X509Certificate publicKey, - PrivateKey privateKey, OutputStream outputStream) - throws Exception { - 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); - - CMSProcessableFile cmsFile = new CMSProcessableFile(input); - writeSignatureBlock(cmsFile, publicKey, privateKey, temp); - - // 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. - byte[] zipData = cmsFile.getTail(); - 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); - } - } - cmsFile.write(outputStream); - outputStream.write(total_size & 0xff); - outputStream.write((total_size >> 8) & 0xff); - temp.writeTo(outputStream); - } - private static void signFile(Manifest manifest, JarFile inputJar, - X509Certificate publicKey, PrivateKey privateKey, - JarOutputStream outputJar) - throws Exception { - // Assume the certificate is valid for at least an hour. - long timestamp = publicKey.getNotBefore().getTime() + 3600L * 1000; - // MANIFEST.MF - JarEntry je = new JarEntry(JarFile.MANIFEST_NAME); - je.setTime(timestamp); - outputJar.putNextEntry(je); - manifest.write(outputJar); - je = new JarEntry(CERT_SF_NAME); - je.setTime(timestamp); - outputJar.putNextEntry(je); - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - writeSignatureFile(manifest, baos, getDigestAlgorithm(publicKey)); - byte[] signedData = baos.toByteArray(); - outputJar.write(signedData); - // CERT.{EC,RSA} / CERT#.{EC,RSA} - final String keyType = publicKey.getPublicKey().getAlgorithm(); - je = new JarEntry(String.format(CERT_SIG_NAME, keyType)); - je.setTime(timestamp); - outputJar.putNextEntry(je); - writeSignatureBlock(new CMSProcessableByteArray(signedData), - publicKey, privateKey, outputJar); - } - - public static void main(String[] args) { - boolean minSign = false; - int argStart = 0; - - if (args.length < 4) { - System.err.println("Usage: zipsigner [-m] publickey.x509[.pem] privatekey.pk8 input.jar output.jar"); - System.exit(2); - } - - if (args[0].equals("-m")) { - minSign = true; - argStart = 1; - } - - sBouncyCastleProvider = new BouncyCastleProvider(); - Security.insertProviderAt(sBouncyCastleProvider, 1); - - File pubKey = new File(args[argStart]); - File privKey = new File(args[argStart + 1]); - File input = new File(args[argStart + 2]); - File output = new File(args[argStart + 3]); - - int alignment = 4; - JarFile inputJar = null; - FileOutputStream outputFile = null; - int hashes = 0; - try { - X509Certificate publicKey = readPublicKey(new FileInputStream(pubKey)); - hashes |= getDigestAlgorithm(publicKey); - - // Set the ZIP file timestamp to the starting valid time - // of the 0th certificate plus one hour (to match what - // we've historically done). - long timestamp = publicKey.getNotBefore().getTime() + 3600L * 1000; - PrivateKey privateKey = readPrivateKey(new FileInputStream(privKey)); - - outputFile = new FileOutputStream(output); - if (minSign) { - ZipSigner.signWholeFile(input, publicKey, privateKey, outputFile); - } else { - inputJar = new JarFile(input, false); // Don't verify. - JarOutputStream outputJar = new JarOutputStream(outputFile); - // For signing .apks, use the maximum compression to make - // them as small as possible (since they live forever on - // the system partition). For OTA packages, use the - // default compression level, which is much much faster - // and produces output that is only a tiny bit larger - // (~0.1% on full OTA packages I tested). - outputJar.setLevel(9); - Manifest manifest = addDigestsToManifest(inputJar, hashes); - copyFiles(manifest, inputJar, outputJar, timestamp, alignment); - signFile(manifest, inputJar, publicKey, privateKey, outputJar); - outputJar.close(); - } - } 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); - } - } - } -} \ No newline at end of file