diff --git a/.gitignore b/.gitignore
index 7b7d0aa31..2865ee369 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,9 +1,18 @@
-out/
-obj/
-libs/
+/out
+/obj
+/libs
*.zip
*.jks
*.apk
# Built binaries
ziptools/zipadjust
+
+# Android Studio / Gradle
+*.iml
+.gradle
+/local.properties
+/.idea
+/build
+/captures
+.externalNativeBuild
diff --git a/.gitmodules b/.gitmodules
index 1c63eaa4c..cdc9d7a6e 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,27 +1,27 @@
[submodule "jni/selinux"]
- path = jni/external/selinux
+ path = core/jni/external/selinux
url = https://github.com/topjohnwu/selinux.git
[submodule "jni/su"]
- path = jni/su
+ path = core/jni/su
url = https://github.com/topjohnwu/MagiskSU.git
[submodule "jni/magiskpolicy"]
- path = jni/magiskpolicy
+ path = core/jni/magiskpolicy
url = https://github.com/topjohnwu/magiskpolicy.git
[submodule "MagiskManager"]
- path = java
+ path = app
url = https://github.com/topjohnwu/MagiskManager.git
[submodule "jni/busybox"]
- path = jni/external/busybox
+ path = core/jni/external/busybox
url = https://github.com/topjohnwu/ndk-busybox.git
[submodule "jni/external/dtc"]
- path = jni/external/dtc
+ path = core/jni/external/dtc
url = https://github.com/dgibson/dtc
[submodule "jni/external/lz4"]
- path = jni/external/lz4
+ path = core/jni/external/lz4
url = https://github.com/lz4/lz4.git
[submodule "jni/external/bzip2"]
- path = jni/external/bzip2
+ path = core/jni/external/bzip2
url = https://github.com/nemequ/bzip2.git
[submodule "jni/external/xz"]
- path = jni/external/xz
+ path = core/jni/external/xz
url = https://github.com/xz-mirror/xz.git
diff --git a/app b/app
new file mode 160000
index 000000000..863b13a69
--- /dev/null
+++ b/app
@@ -0,0 +1 @@
+Subproject commit 863b13a6946c851a4df1043acb8c5003a1b0f3c0
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 000000000..e6b32bc78
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,27 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+
+buildscript {
+
+ repositories {
+ google()
+ jcenter()
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:3.0.1'
+
+
+ // NOTE: Do not place your application dependencies here; they belong
+ // in the individual module build.gradle files
+ }
+}
+
+allprojects {
+ repositories {
+ google()
+ jcenter()
+ }
+}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+}
diff --git a/build.py b/build.py
index cc82e1bd1..b3027eecc 100755
--- a/build.py
+++ b/build.py
@@ -43,6 +43,11 @@ import lzma
import base64
import tempfile
+if 'ANDROID_NDK' in os.environ:
+ ndk_build = os.path.join(os.environ['ANDROID_NDK'], 'ndk-build')
+else:
+ ndk_build = os.path.join(os.environ['ANDROID_HOME'], 'ndk-bundle', 'ndk-build')
+
def mv(source, target):
print('mv: {} -> {}'.format(source, target))
shutil.move(source, target)
@@ -84,18 +89,13 @@ def build_binary(args):
header('* Building Magisk binaries')
# Force update Android.mk timestamp to trigger recompilation
- os.utime(os.path.join('jni', 'Android.mk'))
+ os.utime(os.path.join('core', 'jni', 'Android.mk'))
debug_flag = '' if args.release else '-DMAGISK_DEBUG'
cflag = 'MAGISK_FLAGS=\"-DMAGISK_VERSION=\\\"{}\\\" -DMAGISK_VER_CODE={} {}\"'.format(args.versionString, args.versionCode, debug_flag)
- if 'ANDROID_NDK' in os.environ:
- ndk_build = os.path.join(os.environ['ANDROID_NDK'], 'ndk-build')
- else:
- ndk_build = os.path.join(os.environ['ANDROID_HOME'], 'ndk-bundle', 'ndk-build')
-
# Prebuild
- proc = subprocess.run('{} PRECOMPILE=true {} -j{}'.format(ndk_build, cflag, multiprocessing.cpu_count()), shell=True)
+ proc = subprocess.run('{} -C core PRECOMPILE=true {} -j{}'.format(ndk_build, cflag, multiprocessing.cpu_count()), shell=True)
if proc.returncode != 0:
error('Build Magisk binary failed!')
@@ -104,7 +104,7 @@ def build_binary(args):
mkdir_p(os.path.join('out', arch))
with open(os.path.join('out', arch, 'dump.h'), 'w') as dump:
dump.write('#include "stdlib.h"\n')
- mv(os.path.join('libs', arch, 'magisk'), os.path.join('out', arch, 'magisk'))
+ mv(os.path.join('core', 'libs', arch, 'magisk'), os.path.join('out', arch, 'magisk'))
with open(os.path.join('out', arch, 'magisk'), 'rb') as bin:
dump.write('const uint8_t magisk_dump[] = "')
dump.write(''.join("\\x{:02X}".format(c) for c in lzma.compress(bin.read(), preset=9)))
@@ -112,7 +112,7 @@ def build_binary(args):
print('')
- proc = subprocess.run('{} {} -j{}'.format(ndk_build, cflag, multiprocessing.cpu_count()), shell=True)
+ proc = subprocess.run('{} -C core {} -j{}'.format(ndk_build, cflag, multiprocessing.cpu_count()), shell=True)
if proc.returncode != 0:
error('Build Magisk binary failed!')
@@ -120,7 +120,7 @@ def build_binary(args):
for arch in ['arm64-v8a', 'armeabi-v7a', 'x86', 'x86_64']:
for binary in ['magiskinit', 'magiskboot', 'b64xz', 'busybox']:
try:
- mv(os.path.join('libs', arch, binary), os.path.join('out', arch, binary))
+ mv(os.path.join('core', 'libs', arch, binary), os.path.join('out', arch, binary))
except:
pass
@@ -129,18 +129,16 @@ def build_apk(args):
for key in ['public.certificate.x509.pem', 'private.key.pk8']:
source = os.path.join('ziptools', key)
- target = os.path.join('java', 'app', 'src', 'main', 'assets', key)
+ target = os.path.join('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('java', 'app', 'src', 'main', 'assets', script)
+ target = os.path.join('app', 'src', 'main', 'assets', script)
cp(source, target)
- os.chdir('java')
-
if args.release:
- if not os.path.exists(os.path.join('..', 'release_signature.jks')):
+ if not os.path.exists('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)
@@ -173,17 +171,15 @@ def build_apk(args):
error('Cannot find apksigner.jar in Android SDK build tools')
proc = subprocess.run('java -jar {} sign --ks {} --out {} {}'.format(
- apksigner,
- os.path.join('..', 'release_signature.jks'),
- release, aligned), shell=True)
+ apksigner, 'release_signature.jks', release, aligned), shell=True)
if proc.returncode != 0:
error('Release sign Magisk Manager failed!')
rm(unsigned)
rm(aligned)
- mkdir(os.path.join('..', 'out'))
- target = os.path.join('..', 'out', 'app-release.apk')
+ mkdir('out')
+ target = os.path.join('out', 'app-release.apk')
print('')
mv(release, target)
else:
@@ -192,25 +188,20 @@ def build_apk(args):
error('Build Magisk Manager failed!')
source = os.path.join('app', 'build', 'outputs', 'apk', 'debug', 'app-debug.apk')
- mkdir(os.path.join('..', 'out'))
- target = os.path.join('..', 'out', 'app-debug.apk')
+ mkdir('out')
+ target = os.path.join('out', 'app-debug.apk')
print('')
mv(source, target)
- # Return to upper directory
- os.chdir('..')
-
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')
- mkdir(os.path.join('..', 'out'))
- target = os.path.join('..', 'out', 'snet.apk')
+ mkdir('out')
+ target = os.path.join('out', 'snet.apk')
print('')
mv(source, target)
- os.chdir('..')
def gen_update_binary():
update_bin = []
@@ -342,8 +333,8 @@ def zip_uninstaller(args):
sign_adjust_zip(unsigned, output)
def sign_adjust_zip(unsigned, output):
- signer_name = 'zipsigner-1.0.jar'
- jarsigner = os.path.join('java', 'crypto', 'build', 'libs', signer_name)
+ signer_name = 'zipsigner-1.1.jar'
+ jarsigner = os.path.join('crypto', 'build', 'libs', signer_name)
if os.name != 'nt' and not os.path.exists(os.path.join('ziptools', 'zipadjust')):
header('* Building zipadjust')
@@ -353,11 +344,9 @@ def sign_adjust_zip(unsigned, output):
error('Build zipadjust failed!')
if not os.path.exists(jarsigner):
header('* Building ' + signer_name)
- os.chdir('java')
proc = subprocess.run('{} crypto:shadowJar'.format(os.path.join('.', 'gradlew')), shell=True)
if proc.returncode != 0:
error('Build {} failed!'.format(signer_name))
- os.chdir('..')
header('* Signing / Adjusting Zip')
@@ -396,15 +385,13 @@ def cleanup(args):
if 'binary' in args.target:
header('* Cleaning binaries')
- subprocess.run(os.path.join(os.environ['ANDROID_HOME'], 'ndk-bundle', 'ndk-build') + ' clean', shell=True)
+ subprocess.run(ndk_build + ' -C core COMPILEALL=true clean', shell=True)
for arch in ['arm64-v8a', 'armeabi-v7a', 'x86', 'x86_64']:
shutil.rmtree(os.path.join('out', arch), ignore_errors=True)
if 'java' in args.target:
header('* Cleaning java')
- os.chdir('java')
subprocess.run('{} clean'.format(os.path.join('.', 'gradlew')), shell=True)
- os.chdir('..')
for f in os.listdir('out'):
if '.apk' in f:
rm(os.path.join('out', f))
diff --git a/core/.gitignore b/core/.gitignore
new file mode 100644
index 000000000..796b96d1c
--- /dev/null
+++ b/core/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/core/build.gradle b/core/build.gradle
new file mode 100644
index 000000000..6fb33987b
--- /dev/null
+++ b/core/build.gradle
@@ -0,0 +1,20 @@
+apply plugin: 'com.android.library'
+
+android {
+ compileSdkVersion 27
+
+ externalNativeBuild {
+ ndkBuild {
+ path 'jni/Android.mk'
+ }
+ }
+
+ defaultConfig {
+ externalNativeBuild {
+ ndkBuild {
+ // Passes an optional argument to ndk-build.
+ arguments "COMPILEALL=true"
+ }
+ }
+ }
+}
diff --git a/jni/Android.mk b/core/jni/Android.mk
similarity index 96%
rename from jni/Android.mk
rename to core/jni/Android.mk
index 8263025d7..8f5d728cd 100644
--- a/jni/Android.mk
+++ b/core/jni/Android.mk
@@ -14,7 +14,7 @@ LIBFDT := $(EXT_PATH)/dtc/libfdt
# Binaries
########################
-ifdef PRECOMPILE
+ifneq "$(or $(PRECOMPILE), $(COMPILEALL))" ""
# magisk main binary
include $(CLEAR_VARS)
@@ -54,8 +54,9 @@ LOCAL_CFLAGS := -DIS_DAEMON -DSELINUX
LOCAL_LDLIBS := -llog
include $(BUILD_EXECUTABLE)
-# precompile
-else
+endif
+
+ifndef PRECOMPILE
# magiskinit
include $(CLEAR_VARS)
@@ -64,7 +65,7 @@ LOCAL_STATIC_LIBRARIES := libsepol liblzma
LOCAL_C_INCLUDES := \
jni/include \
jni/magiskpolicy \
- out/$(TARGET_ARCH_ABI) \
+ ../out/$(TARGET_ARCH_ABI) \
$(LIBSEPOL) \
$(LIBLZMA)
diff --git a/jni/Application.mk b/core/jni/Application.mk
similarity index 100%
rename from jni/Application.mk
rename to core/jni/Application.mk
diff --git a/jni/b64xz.c b/core/jni/b64xz.c
similarity index 100%
rename from jni/b64xz.c
rename to core/jni/b64xz.c
diff --git a/jni/core/bootstages.c b/core/jni/core/bootstages.c
similarity index 100%
rename from jni/core/bootstages.c
rename to core/jni/core/bootstages.c
diff --git a/jni/core/daemon.c b/core/jni/core/daemon.c
similarity index 100%
rename from jni/core/daemon.c
rename to core/jni/core/daemon.c
diff --git a/jni/core/log_monitor.c b/core/jni/core/log_monitor.c
similarity index 100%
rename from jni/core/log_monitor.c
rename to core/jni/core/log_monitor.c
diff --git a/jni/core/magisk.c b/core/jni/core/magisk.c
similarity index 100%
rename from jni/core/magisk.c
rename to core/jni/core/magisk.c
diff --git a/jni/core/magiskinit.c b/core/jni/core/magiskinit.c
similarity index 100%
rename from jni/core/magiskinit.c
rename to core/jni/core/magiskinit.c
diff --git a/jni/core/socket.c b/core/jni/core/socket.c
similarity index 100%
rename from jni/core/socket.c
rename to core/jni/core/socket.c
diff --git a/jni/external/Android.mk b/core/jni/external/Android.mk
similarity index 100%
rename from jni/external/Android.mk
rename to core/jni/external/Android.mk
diff --git a/jni/external/busybox b/core/jni/external/busybox
similarity index 100%
rename from jni/external/busybox
rename to core/jni/external/busybox
diff --git a/jni/external/bzip2 b/core/jni/external/bzip2
similarity index 100%
rename from jni/external/bzip2
rename to core/jni/external/bzip2
diff --git a/jni/external/dtc b/core/jni/external/dtc
similarity index 100%
rename from jni/external/dtc
rename to core/jni/external/dtc
diff --git a/jni/external/include/sha1.h b/core/jni/external/include/sha1.h
similarity index 100%
rename from jni/external/include/sha1.h
rename to core/jni/external/include/sha1.h
diff --git a/jni/external/include/sqlite3.h b/core/jni/external/include/sqlite3.h
similarity index 100%
rename from jni/external/include/sqlite3.h
rename to core/jni/external/include/sqlite3.h
diff --git a/jni/external/include/xz_config/config.h b/core/jni/external/include/xz_config/config.h
similarity index 100%
rename from jni/external/include/xz_config/config.h
rename to core/jni/external/include/xz_config/config.h
diff --git a/jni/external/lz4 b/core/jni/external/lz4
similarity index 100%
rename from jni/external/lz4
rename to core/jni/external/lz4
diff --git a/jni/external/selinux b/core/jni/external/selinux
similarity index 100%
rename from jni/external/selinux
rename to core/jni/external/selinux
diff --git a/jni/external/sha1/sha1.c b/core/jni/external/sha1/sha1.c
similarity index 100%
rename from jni/external/sha1/sha1.c
rename to core/jni/external/sha1/sha1.c
diff --git a/jni/external/stubs/selinux_stub.c b/core/jni/external/stubs/selinux_stub.c
similarity index 100%
rename from jni/external/stubs/selinux_stub.c
rename to core/jni/external/stubs/selinux_stub.c
diff --git a/jni/external/stubs/sqlite3_stub.c b/core/jni/external/stubs/sqlite3_stub.c
similarity index 100%
rename from jni/external/stubs/sqlite3_stub.c
rename to core/jni/external/stubs/sqlite3_stub.c
diff --git a/jni/external/xz b/core/jni/external/xz
similarity index 100%
rename from jni/external/xz
rename to core/jni/external/xz
diff --git a/jni/include/cpio.h b/core/jni/include/cpio.h
similarity index 100%
rename from jni/include/cpio.h
rename to core/jni/include/cpio.h
diff --git a/jni/include/daemon.h b/core/jni/include/daemon.h
similarity index 100%
rename from jni/include/daemon.h
rename to core/jni/include/daemon.h
diff --git a/jni/include/list.h b/core/jni/include/list.h
similarity index 100%
rename from jni/include/list.h
rename to core/jni/include/list.h
diff --git a/jni/include/logging.h b/core/jni/include/logging.h
similarity index 100%
rename from jni/include/logging.h
rename to core/jni/include/logging.h
diff --git a/jni/include/magisk.h b/core/jni/include/magisk.h
similarity index 100%
rename from jni/include/magisk.h
rename to core/jni/include/magisk.h
diff --git a/jni/include/magiskrc.h b/core/jni/include/magiskrc.h
similarity index 100%
rename from jni/include/magiskrc.h
rename to core/jni/include/magiskrc.h
diff --git a/jni/include/resetprop.h b/core/jni/include/resetprop.h
similarity index 100%
rename from jni/include/resetprop.h
rename to core/jni/include/resetprop.h
diff --git a/jni/include/utils.h b/core/jni/include/utils.h
similarity index 100%
rename from jni/include/utils.h
rename to core/jni/include/utils.h
diff --git a/jni/include/vector.h b/core/jni/include/vector.h
similarity index 100%
rename from jni/include/vector.h
rename to core/jni/include/vector.h
diff --git a/jni/magiskboot/bootimg.c b/core/jni/magiskboot/bootimg.c
similarity index 100%
rename from jni/magiskboot/bootimg.c
rename to core/jni/magiskboot/bootimg.c
diff --git a/jni/magiskboot/bootimg.h b/core/jni/magiskboot/bootimg.h
similarity index 100%
rename from jni/magiskboot/bootimg.h
rename to core/jni/magiskboot/bootimg.h
diff --git a/jni/magiskboot/compress.c b/core/jni/magiskboot/compress.c
similarity index 100%
rename from jni/magiskboot/compress.c
rename to core/jni/magiskboot/compress.c
diff --git a/jni/magiskboot/dtb.c b/core/jni/magiskboot/dtb.c
similarity index 100%
rename from jni/magiskboot/dtb.c
rename to core/jni/magiskboot/dtb.c
diff --git a/jni/magiskboot/hexpatch.c b/core/jni/magiskboot/hexpatch.c
similarity index 100%
rename from jni/magiskboot/hexpatch.c
rename to core/jni/magiskboot/hexpatch.c
diff --git a/jni/magiskboot/magiskboot.h b/core/jni/magiskboot/magiskboot.h
similarity index 100%
rename from jni/magiskboot/magiskboot.h
rename to core/jni/magiskboot/magiskboot.h
diff --git a/jni/magiskboot/main.c b/core/jni/magiskboot/main.c
similarity index 100%
rename from jni/magiskboot/main.c
rename to core/jni/magiskboot/main.c
diff --git a/jni/magiskboot/ramdisk.c b/core/jni/magiskboot/ramdisk.c
similarity index 100%
rename from jni/magiskboot/ramdisk.c
rename to core/jni/magiskboot/ramdisk.c
diff --git a/jni/magiskboot/types.c b/core/jni/magiskboot/types.c
similarity index 100%
rename from jni/magiskboot/types.c
rename to core/jni/magiskboot/types.c
diff --git a/jni/magiskboot/types.h b/core/jni/magiskboot/types.h
similarity index 100%
rename from jni/magiskboot/types.h
rename to core/jni/magiskboot/types.h
diff --git a/jni/magiskhide/hide_utils.c b/core/jni/magiskhide/hide_utils.c
similarity index 100%
rename from jni/magiskhide/hide_utils.c
rename to core/jni/magiskhide/hide_utils.c
diff --git a/jni/magiskhide/magiskhide.c b/core/jni/magiskhide/magiskhide.c
similarity index 100%
rename from jni/magiskhide/magiskhide.c
rename to core/jni/magiskhide/magiskhide.c
diff --git a/jni/magiskhide/magiskhide.h b/core/jni/magiskhide/magiskhide.h
similarity index 100%
rename from jni/magiskhide/magiskhide.h
rename to core/jni/magiskhide/magiskhide.h
diff --git a/jni/magiskhide/proc_monitor.c b/core/jni/magiskhide/proc_monitor.c
similarity index 100%
rename from jni/magiskhide/proc_monitor.c
rename to core/jni/magiskhide/proc_monitor.c
diff --git a/jni/magiskpolicy b/core/jni/magiskpolicy
similarity index 100%
rename from jni/magiskpolicy
rename to core/jni/magiskpolicy
diff --git a/jni/resetprop/ErrnoRestorer.h b/core/jni/resetprop/ErrnoRestorer.h
similarity index 100%
rename from jni/resetprop/ErrnoRestorer.h
rename to core/jni/resetprop/ErrnoRestorer.h
diff --git a/jni/resetprop/_system_properties.h b/core/jni/resetprop/_system_properties.h
similarity index 100%
rename from jni/resetprop/_system_properties.h
rename to core/jni/resetprop/_system_properties.h
diff --git a/jni/resetprop/bionic_futex.h b/core/jni/resetprop/bionic_futex.h
similarity index 100%
rename from jni/resetprop/bionic_futex.h
rename to core/jni/resetprop/bionic_futex.h
diff --git a/jni/resetprop/bionic_lock.h b/core/jni/resetprop/bionic_lock.h
similarity index 100%
rename from jni/resetprop/bionic_lock.h
rename to core/jni/resetprop/bionic_lock.h
diff --git a/jni/resetprop/bionic_macros.h b/core/jni/resetprop/bionic_macros.h
similarity index 100%
rename from jni/resetprop/bionic_macros.h
rename to core/jni/resetprop/bionic_macros.h
diff --git a/jni/resetprop/resetprop.cpp b/core/jni/resetprop/resetprop.cpp
similarity index 100%
rename from jni/resetprop/resetprop.cpp
rename to core/jni/resetprop/resetprop.cpp
diff --git a/jni/resetprop/system_properties.cpp b/core/jni/resetprop/system_properties.cpp
similarity index 100%
rename from jni/resetprop/system_properties.cpp
rename to core/jni/resetprop/system_properties.cpp
diff --git a/jni/resetprop/system_properties.h b/core/jni/resetprop/system_properties.h
similarity index 100%
rename from jni/resetprop/system_properties.h
rename to core/jni/resetprop/system_properties.h
diff --git a/jni/su b/core/jni/su
similarity index 100%
rename from jni/su
rename to core/jni/su
diff --git a/jni/utils/cpio.c b/core/jni/utils/cpio.c
similarity index 100%
rename from jni/utils/cpio.c
rename to core/jni/utils/cpio.c
diff --git a/jni/utils/file.c b/core/jni/utils/file.c
similarity index 100%
rename from jni/utils/file.c
rename to core/jni/utils/file.c
diff --git a/jni/utils/img.c b/core/jni/utils/img.c
similarity index 100%
rename from jni/utils/img.c
rename to core/jni/utils/img.c
diff --git a/jni/utils/list.c b/core/jni/utils/list.c
similarity index 100%
rename from jni/utils/list.c
rename to core/jni/utils/list.c
diff --git a/jni/utils/misc.c b/core/jni/utils/misc.c
similarity index 100%
rename from jni/utils/misc.c
rename to core/jni/utils/misc.c
diff --git a/jni/utils/vector.c b/core/jni/utils/vector.c
similarity index 100%
rename from jni/utils/vector.c
rename to core/jni/utils/vector.c
diff --git a/jni/utils/xwrap.c b/core/jni/utils/xwrap.c
similarity index 100%
rename from jni/utils/xwrap.c
rename to core/jni/utils/xwrap.c
diff --git a/core/src/main/AndroidManifest.xml b/core/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..1d9dded58
--- /dev/null
+++ b/core/src/main/AndroidManifest.xml
@@ -0,0 +1 @@
+
diff --git a/crypto/.gitignore b/crypto/.gitignore
new file mode 100644
index 000000000..796b96d1c
--- /dev/null
+++ b/crypto/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/crypto/build.gradle b/crypto/build.gradle
new file mode 100644
index 000000000..edfb254cd
--- /dev/null
+++ b/crypto/build.gradle
@@ -0,0 +1,38 @@
+apply plugin: 'java-library'
+
+apply plugin: 'com.github.johnrengelman.shadow'
+apply plugin: 'java'
+
+sourceCompatibility = "1.8"
+targetCompatibility = "1.8"
+
+jar {
+ manifest {
+ attributes 'Main-Class': 'com.topjohnwu.crypto.ZipSigner'
+ }
+}
+
+shadowJar {
+ baseName = 'zipsigner'
+ classifier = null
+ version = 1.1
+}
+
+buildscript {
+ repositories {
+ jcenter()
+ }
+ dependencies {
+ classpath 'com.github.jengelman.gradle.plugins:shadow:2.0.1'
+ }
+}
+
+repositories {
+ jcenter()
+}
+
+dependencies {
+ implementation fileTree(include: ['*.jar'], dir: 'libs')
+ implementation 'org.bouncycastle:bcprov-jdk15on:1.58'
+ implementation 'org.bouncycastle:bcpkix-jdk15on:1.58'
+}
diff --git a/crypto/src/main/java/com/topjohnwu/crypto/ByteArrayStream.java b/crypto/src/main/java/com/topjohnwu/crypto/ByteArrayStream.java
new file mode 100644
index 000000000..ef6c7bc6d
--- /dev/null
+++ b/crypto/src/main/java/com/topjohnwu/crypto/ByteArrayStream.java
@@ -0,0 +1,34 @@
+package com.topjohnwu.crypto;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+public class ByteArrayStream extends ByteArrayOutputStream {
+ public byte[] getBuf() {
+ return buf;
+ }
+ public synchronized void readFrom(InputStream is) {
+ readFrom(is, Integer.MAX_VALUE);
+ }
+ public synchronized void readFrom(InputStream is, int len) {
+ int read;
+ byte buffer[] = new byte[4096];
+ try {
+ while ((read = is.read(buffer, 0, len < buffer.length ? len : buffer.length)) > 0) {
+ write(buffer, 0, read);
+ len -= read;
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ public synchronized void writeTo(OutputStream out, int off, int len) throws IOException {
+ out.write(buf, off, len);
+ }
+ public ByteArrayInputStream getInputStream() {
+ return new ByteArrayInputStream(buf, 0, count);
+ }
+}
diff --git a/crypto/src/main/java/com/topjohnwu/crypto/CryptoUtils.java b/crypto/src/main/java/com/topjohnwu/crypto/CryptoUtils.java
new file mode 100644
index 000000000..45bf1655c
--- /dev/null
+++ b/crypto/src/main/java/com/topjohnwu/crypto/CryptoUtils.java
@@ -0,0 +1,136 @@
+package com.topjohnwu.crypto;
+
+import org.bouncycastle.asn1.ASN1InputStream;
+import org.bouncycastle.asn1.ASN1ObjectIdentifier;
+import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
+import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
+import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
+import org.bouncycastle.asn1.x9.X9ObjectIdentifiers;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.GeneralSecurityException;
+import java.security.Key;
+import java.security.KeyFactory;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.Signature;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.security.spec.ECPrivateKeySpec;
+import java.security.spec.ECPublicKeySpec;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+
+class CryptoUtils {
+
+ private static final Map ID_TO_ALG;
+ private static final Map ALG_TO_ID;
+
+ static {
+ ID_TO_ALG = new HashMap<>();
+ ALG_TO_ID = new HashMap<>();
+ ID_TO_ALG.put(X9ObjectIdentifiers.ecdsa_with_SHA256.getId(), "SHA256withECDSA");
+ ID_TO_ALG.put(X9ObjectIdentifiers.ecdsa_with_SHA384.getId(), "SHA384withECDSA");
+ ID_TO_ALG.put(X9ObjectIdentifiers.ecdsa_with_SHA512.getId(), "SHA512withECDSA");
+ ID_TO_ALG.put(PKCSObjectIdentifiers.sha1WithRSAEncryption.getId(), "SHA1withRSA");
+ ID_TO_ALG.put(PKCSObjectIdentifiers.sha256WithRSAEncryption.getId(), "SHA256withRSA");
+ ID_TO_ALG.put(PKCSObjectIdentifiers.sha512WithRSAEncryption.getId(), "SHA512withRSA");
+ ALG_TO_ID.put("SHA256withECDSA", X9ObjectIdentifiers.ecdsa_with_SHA256.getId());
+ ALG_TO_ID.put("SHA384withECDSA", X9ObjectIdentifiers.ecdsa_with_SHA384.getId());
+ ALG_TO_ID.put("SHA512withECDSA", X9ObjectIdentifiers.ecdsa_with_SHA512.getId());
+ ALG_TO_ID.put("SHA1withRSA", PKCSObjectIdentifiers.sha1WithRSAEncryption.getId());
+ ALG_TO_ID.put("SHA256withRSA", PKCSObjectIdentifiers.sha256WithRSAEncryption.getId());
+ ALG_TO_ID.put("SHA512withRSA", PKCSObjectIdentifiers.sha512WithRSAEncryption.getId());
+ }
+
+ private static String getSignatureAlgorithm(Key key) throws Exception {
+ if ("EC".equals(key.getAlgorithm())) {
+ int curveSize;
+ KeyFactory factory = KeyFactory.getInstance("EC");
+ if (key instanceof PublicKey) {
+ ECPublicKeySpec spec = factory.getKeySpec(key, ECPublicKeySpec.class);
+ curveSize = spec.getParams().getCurve().getField().getFieldSize();
+ } else if (key instanceof PrivateKey) {
+ ECPrivateKeySpec spec = factory.getKeySpec(key, ECPrivateKeySpec.class);
+ curveSize = spec.getParams().getCurve().getField().getFieldSize();
+ } else {
+ throw new InvalidKeySpecException();
+ }
+ if (curveSize <= 256) {
+ return "SHA256withECDSA";
+ } else if (curveSize <= 384) {
+ return "SHA384withECDSA";
+ } else {
+ return "SHA512withECDSA";
+ }
+ } else if ("RSA".equals(key.getAlgorithm())) {
+ return "SHA256withRSA";
+ } else {
+ throw new IllegalArgumentException("Unsupported key type " + key.getAlgorithm());
+ }
+ }
+
+ static AlgorithmIdentifier getSignatureAlgorithmIdentifier(Key key) throws Exception {
+ String id = ALG_TO_ID.get(getSignatureAlgorithm(key));
+ if (id == null) {
+ throw new IllegalArgumentException("Unsupported key type " + key.getAlgorithm());
+ }
+ return new AlgorithmIdentifier(new ASN1ObjectIdentifier(id));
+ }
+
+ static boolean verify(PublicKey key, byte[] input, byte[] signature,
+ AlgorithmIdentifier algId) throws Exception {
+ String algName = ID_TO_ALG.get(algId.getAlgorithm().getId());
+ if (algName == null) {
+ throw new IllegalArgumentException("Unsupported algorithm " + algId.getAlgorithm());
+ }
+ Signature verifier = Signature.getInstance(algName);
+ verifier.initVerify(key);
+ verifier.update(input);
+ return verifier.verify(signature);
+ }
+
+ static byte[] sign(PrivateKey privateKey, byte[] input) throws Exception {
+ Signature signer = Signature.getInstance(getSignatureAlgorithm(privateKey));
+ signer.initSign(privateKey);
+ signer.update(input);
+ return signer.sign();
+ }
+
+ 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. */
+ 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();
+ }
+ }
+}
diff --git a/crypto/src/main/java/com/topjohnwu/crypto/JarMap.java b/crypto/src/main/java/com/topjohnwu/crypto/JarMap.java
new file mode 100644
index 000000000..9db773949
--- /dev/null
+++ b/crypto/src/main/java/com/topjohnwu/crypto/JarMap.java
@@ -0,0 +1,122 @@
+package com.topjohnwu.crypto;
+
+import java.io.Closeable;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.LinkedHashMap;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+import java.util.jar.JarInputStream;
+import java.util.jar.Manifest;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+
+/*
+* A universal random access interface for both JarFile and JarInputStream
+*
+* In the case when JarInputStream is provided to constructor, the whole stream
+* will be loaded into memory for random access purposes.
+* On the other hand, when a JarFile is provided, it simply works as a wrapper.
+* */
+
+public class JarMap implements Closeable, AutoCloseable {
+ private JarFile jarFile;
+ private JarInputStream jis;
+ private boolean isInputStream = false;
+ private LinkedHashMap bufMap;
+
+ public JarMap(File file) throws IOException {
+ this(file, true);
+ }
+
+ public JarMap(File file, boolean verify) throws IOException {
+ this(file, verify, ZipFile.OPEN_READ);
+ }
+
+ public JarMap(File file, boolean verify, int mode) throws IOException {
+ jarFile = new JarFile(file, verify, mode);
+ }
+
+ public JarMap(String name) throws IOException {
+ this(new File(name));
+ }
+
+ public JarMap(String name, boolean verify) throws IOException {
+ this(new File(name), verify);
+ }
+
+ public JarMap(InputStream is) throws IOException {
+ this(is, true);
+ }
+
+ public JarMap(InputStream is, boolean verify) throws IOException {
+ isInputStream = true;
+ bufMap = new LinkedHashMap<>();
+ jis = new JarInputStream(is, verify);
+ JarEntry entry;
+ while ((entry = jis.getNextJarEntry()) != null) {
+ bufMap.put(entry.getName(), new JarMapEntry(entry, jis));
+ }
+ }
+
+ public File getFile() {
+ return isInputStream ? null : new File(jarFile.getName());
+ }
+
+ public Manifest getManifest() throws IOException {
+ return isInputStream ? jis.getManifest() : jarFile.getManifest();
+ }
+
+ public InputStream getInputStream(ZipEntry ze) throws IOException {
+ return isInputStream ? ((JarMapEntry) bufMap.get(ze.getName())).data.getInputStream() :
+ jarFile.getInputStream(ze);
+ }
+
+ public OutputStream getOutputStream(ZipEntry ze) {
+ if (!isInputStream) // Only support InputStream mode
+ return null;
+ ByteArrayStream bs = ((JarMapEntry) bufMap.get(ze.getName())).data;
+ bs.reset();
+ return bs;
+ }
+
+ public byte[] getRawData(ZipEntry ze) throws IOException {
+ if (isInputStream) {
+ return ((JarMapEntry) bufMap.get(ze.getName())).data.toByteArray();
+ } else {
+ ByteArrayStream bytes = new ByteArrayStream();
+ bytes.readFrom(jarFile.getInputStream(ze));
+ return bytes.toByteArray();
+ }
+ }
+
+ public Enumeration entries() {
+ return isInputStream ? Collections.enumeration(bufMap.values()) : jarFile.entries();
+ }
+
+ public ZipEntry getEntry(String name) {
+ return getJarEntry(name);
+ }
+
+ public JarEntry getJarEntry(String name) {
+ return isInputStream ? bufMap.get(name) : jarFile.getJarEntry(name);
+ }
+
+ @Override
+ public void close() throws IOException {
+ (isInputStream ? jis : jarFile).close();
+ }
+
+ private static class JarMapEntry extends JarEntry {
+ ByteArrayStream data;
+ JarMapEntry(JarEntry je, InputStream is) {
+ super(je);
+ data = new ByteArrayStream();
+ data.readFrom(is);
+ }
+ }
+}
diff --git a/crypto/src/main/java/com/topjohnwu/crypto/SignAPK.java b/crypto/src/main/java/com/topjohnwu/crypto/SignAPK.java
new file mode 100644
index 000000000..96c2aca5a
--- /dev/null
+++ b/crypto/src/main/java/com/topjohnwu/crypto/SignAPK.java
@@ -0,0 +1,502 @@
+package com.topjohnwu.crypto;
+
+import org.bouncycastle.asn1.ASN1InputStream;
+import org.bouncycastle.asn1.ASN1ObjectIdentifier;
+import org.bouncycastle.asn1.DEROutputStream;
+import org.bouncycastle.asn1.cms.CMSObjectIdentifiers;
+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.BufferedOutputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileNotFoundException;
+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.io.RandomAccessFile;
+import java.security.DigestOutputStream;
+import java.security.GeneralSecurityException;
+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.X509Certificate;
+import java.util.ArrayList;
+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 SignAPK {
+
+ private static final String CERT_SF_NAME = "META-INF/CERT.SF";
+ private static final String CERT_SIG_NAME = "META-INF/CERT.%s";
+
+ public 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;
+
+ static {
+ sBouncyCastleProvider = new BouncyCastleProvider();
+ Security.insertProviderAt(sBouncyCastleProvider, 1);
+ }
+
+ public static void signZip(InputStream publicIn, InputStream privateIn,
+ JarMap input, File output, boolean minSign) throws Exception {
+ int alignment = 4;
+ BufferedOutputStream outputFile;
+ int hashes = 0;
+ X509Certificate publicKey = CryptoUtils.readPublicKey(publicIn);
+ 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 = CryptoUtils.readPrivateKey(privateIn);
+
+ outputFile = new BufferedOutputStream(new FileOutputStream(output));
+ if (minSign) {
+ signWholeFile(input.getFile(), publicKey, privateKey, outputFile);
+ } else {
+ 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(input, hashes);
+ copyFiles(manifest, input, outputJar, timestamp, alignment);
+ signFile(manifest, input, publicKey, privateKey, outputJar);
+ outputJar.close();
+ }
+ input.close();
+ outputFile.close();
+ }
+
+ /**
+ * 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) + ")$");
+
+ /**
+ * Add the hash(es) of every file to the manifest, creating it if
+ * necessary.
+ */
+ private static Manifest addDigestsToManifest(JarMap 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