-
Notifications
You must be signed in to change notification settings - Fork 42
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add Magisk module to enable OEM unlocking on every boot
The module helps reduce the chance of OEM unlocking being disabled, whether by the user or by some OS's initial setup wizard. It works by running some Java code at boot, which connects to the `OemLockService` binder service and calls `setOemUnlockAllowedByUser(true)`, the same as what the Settings app does. Fixes: #8 Issue: #84 Signed-off-by: Andrew Gunnerson <[email protected]>
- Loading branch information
1 parent
7d5977b
commit 0c6c0b2
Showing
9 changed files
with
275 additions
and
68 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,150 @@ | ||
#!/usr/bin/env python3 | ||
|
||
import io | ||
import os | ||
import re | ||
import shutil | ||
import subprocess | ||
import sys | ||
import tempfile | ||
import zipfile | ||
|
||
|
||
def natsort_key(text, regex=re.compile(r'(\d+)')): | ||
return [int(s) if s.isdigit() else s for s in regex.split(text)] | ||
|
||
|
||
def newest_child_by_name(directory): | ||
children = os.listdir(directory) | ||
if not children: | ||
raise ValueError(f'{directory} has no children') | ||
|
||
child = sorted(children, key=natsort_key)[-1] | ||
return os.path.join(directory, child) | ||
|
||
|
||
def build_empty_zip(): | ||
stream = io.BytesIO() | ||
|
||
with zipfile.ZipFile(stream, 'w'): | ||
pass | ||
|
||
return stream.getvalue() | ||
|
||
|
||
def build_dex(sources): | ||
if 'ANDROID_HOME' not in os.environ: | ||
raise ValueError('ANDROID_HOME must be set to the Android SDK path') | ||
|
||
sdk = os.environ['ANDROID_HOME'] | ||
build_tools = newest_child_by_name(os.path.join(sdk, 'build-tools')) | ||
platform = newest_child_by_name(os.path.join(sdk, 'platforms')) | ||
d8 = os.path.join(build_tools, 'd8') | ||
android_jar = os.path.join(platform, 'android.jar') | ||
|
||
with tempfile.TemporaryDirectory() as temp_dir: | ||
subprocess.check_call([ | ||
'javac', | ||
'-source', '1.8', | ||
'-target', '1.8', | ||
'-cp', android_jar, | ||
'-d', temp_dir, | ||
*sources, | ||
]) | ||
|
||
class_files = [] | ||
for root, _, files in os.walk(temp_dir): | ||
for f in files: | ||
if f.endswith('.class'): | ||
class_files.append(os.path.join(root, f)) | ||
|
||
subprocess.check_call([ | ||
d8, | ||
'--output', temp_dir, | ||
*class_files, | ||
]) | ||
|
||
with open(os.path.join(temp_dir, 'classes.dex'), 'rb') as f: | ||
return f.read() | ||
|
||
|
||
def parse_props(raw_prop): | ||
result = {} | ||
|
||
for line in raw_prop.decode('UTF-8').splitlines(): | ||
k, delim, v = line.partition('=') | ||
if not delim: | ||
raise ValueError(f'Malformed line: {repr(line)}') | ||
|
||
result[k.strip()] = v.strip() | ||
|
||
return result | ||
|
||
|
||
def build_module(dist_dir, common_dir, module_dir, extra_files): | ||
with open(os.path.join(module_dir, 'module.prop'), 'rb') as f: | ||
module_prop_raw = f.read() | ||
module_prop = parse_props(module_prop_raw) | ||
|
||
name = module_prop['name'] | ||
version = module_prop['version'].removeprefix('v') | ||
zip_path = os.path.join(dist_dir, f'{name}-{version}.zip') | ||
|
||
with zipfile.ZipFile(zip_path, 'w') as z: | ||
file_map = { | ||
'META-INF/com/google/android/update-binary': { | ||
'file': os.path.join(common_dir, 'update-binary'), | ||
}, | ||
'META-INF/com/google/android/updater-script': { | ||
'file': os.path.join(common_dir, 'updater-script'), | ||
}, | ||
'module.prop': { | ||
'data': module_prop_raw, | ||
}, | ||
**extra_files, | ||
} | ||
|
||
for name, source in sorted(file_map.items()): | ||
# Build our own ZipInfo to ensure archive is reproducible | ||
info = zipfile.ZipInfo(name) | ||
with z.open(info, 'w') as f_out: | ||
if 'data' in source: | ||
f_out.write(source['data']) | ||
else: | ||
with open(source['file'], 'rb') as f_in: | ||
shutil.copyfileobj(f_in, f_out) | ||
|
||
return zip_path | ||
|
||
|
||
def main(): | ||
dist_dir = os.path.join(sys.path[0], 'dist') | ||
os.makedirs(dist_dir, exist_ok=True) | ||
|
||
common_dir = os.path.join(sys.path[0], 'common') | ||
|
||
for module in ('clearotacerts', 'oemunlockonboot'): | ||
module_dir = os.path.join(sys.path[0], module) | ||
|
||
if module == 'clearotacerts': | ||
extra_files = { | ||
'system/etc/security/otacerts.zip': { | ||
'data': build_empty_zip(), | ||
}, | ||
} | ||
elif module == 'oemunlockonboot': | ||
extra_files = { | ||
'classes.dex': { | ||
'data': build_dex([os.path.join(module_dir, 'Main.java')]), | ||
}, | ||
'service.sh': { | ||
'file': os.path.join(module_dir, 'service.sh'), | ||
}, | ||
} | ||
|
||
module_zip = build_module(dist_dir, common_dir, module_dir, extra_files) | ||
print('Built module', module_zip) | ||
|
||
|
||
if __name__ == '__main__': | ||
main() |
File renamed without changes.
File renamed without changes.
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
import android.annotation.SuppressLint; | ||
import android.os.IBinder; | ||
import android.os.IInterface; | ||
import android.os.Process; | ||
import android.system.ErrnoException; | ||
|
||
import java.lang.reflect.Method; | ||
|
||
@SuppressLint({"DiscouragedPrivateApi", "PrivateApi", "SoonBlockedPrivateApi"}) | ||
public class Main { | ||
private static final int GET_SERVICE_ATTEMPTS = 30; | ||
|
||
@SuppressWarnings("SameParameterValue") | ||
private static IInterface getService(Class<?> interfaceClass, String serviceName) throws Exception { | ||
Class<?> serviceManager = Class.forName("android.os.ServiceManager"); | ||
Method getService = serviceManager.getDeclaredMethod("getService", String.class); | ||
|
||
Class<?> stub = Class.forName(interfaceClass.getCanonicalName() + "$Stub"); | ||
Method asInterface = stub.getDeclaredMethod("asInterface", IBinder.class); | ||
|
||
// ServiceManager.waitForService() tries to start the service, which we want to avoid to be | ||
// 100% sure we're not disrupting the boot flow. | ||
for (int attempt = 1; attempt <= GET_SERVICE_ATTEMPTS; ++attempt) { | ||
IBinder iBinder = (IBinder) getService.invoke(null, serviceName); | ||
if (iBinder != null) { | ||
return (IInterface) asInterface.invoke(null, iBinder); | ||
} | ||
|
||
if (attempt < GET_SERVICE_ATTEMPTS) { | ||
Thread.sleep(1000); | ||
} | ||
} | ||
|
||
throw new IllegalStateException( | ||
"Service " + serviceName + " not found after " + GET_SERVICE_ATTEMPTS + " attempts"); | ||
} | ||
|
||
private static void unlock() throws Exception { | ||
Class<?> iOemLockService = Class.forName("android.service.oemlock.IOemLockService"); | ||
IInterface iFace = getService(iOemLockService, "oem_lock"); | ||
|
||
Method setOemUnlockAllowedByUser = iOemLockService.getDeclaredMethod("setOemUnlockAllowedByUser", boolean.class); | ||
Method isOemUnlockAllowedByUser = iOemLockService.getDeclaredMethod("isOemUnlockAllowedByUser"); | ||
|
||
Boolean unlockAllowed = (Boolean) isOemUnlockAllowedByUser.invoke(iFace); | ||
//noinspection ConstantConditions | ||
if (unlockAllowed) { | ||
System.out.println("OEM unlocking already enabled"); | ||
return; | ||
} | ||
|
||
System.out.println("Enabling OEM unlocking"); | ||
setOemUnlockAllowedByUser.invoke(iFace, true); | ||
} | ||
|
||
@SuppressWarnings({"ConstantConditions", "JavaReflectionMemberAccess"}) | ||
private static void switchToSystemUid() throws Exception { | ||
if (Process.myUid() != Process.SYSTEM_UID) { | ||
Method setUid = Process.class.getDeclaredMethod("setUid", int.class); | ||
int errno = (int) setUid.invoke(null, Process.SYSTEM_UID); | ||
|
||
if (errno != 0) { | ||
throw new Exception("Failed to switch to SYSTEM (" + Process.SYSTEM_UID + ") user", | ||
new ErrnoException("setuid", errno)); | ||
} | ||
if (Process.myUid() != Process.SYSTEM_UID) { | ||
throw new IllegalStateException("UID didn't actually change: " + | ||
Process.myUid() + " != " + Process.SYSTEM_UID); | ||
} | ||
} | ||
} | ||
|
||
public static void main(String[] args) { | ||
try { | ||
switchToSystemUid(); | ||
unlock(); | ||
} catch (Exception e) { | ||
System.err.println("Failed to enable OEM unlocking"); | ||
e.printStackTrace(); | ||
System.exit(1); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
id=com.chiller3.avbroot.oemunlockonboot | ||
name=oemunlockonboot | ||
version=v1.0 | ||
versionCode=1 | ||
author=chenxiaolong | ||
description=Enable OEM unlocking on every boot |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
exec >/data/local/tmp/avbroot_oem_unlock.log 2>&1 | ||
|
||
mod_dir=${0%/*} | ||
|
||
header() { | ||
echo "----- ${*} -----" | ||
} | ||
|
||
header Environment | ||
echo "Timestamp: $(date)" | ||
echo "Script: ${0}" | ||
echo "UID/GID/Context: $(id)" | ||
|
||
header Enable OEM unlocking | ||
CLASSPATH="${mod_dir}/classes.dex" app_process / Main & | ||
pid=${!} | ||
wait "${pid}" | ||
echo "Exit status: ${?}" | ||
echo "Logcat:" | ||
logcat -d --pid "${pid}" |