#!/usr/bin/env python3 """Apply sunfish-a16 005 signature spoofing extensions on LOS 23.2 tree. Builds on LuK1337 microG allow-list spoofing already in ComputerEngine: - Accept project-signed Phonesky (not only MICROG_REAL) - Gate on Settings.Secure.allow_signature_fake (default off) - Require FAKE_PACKAGE_SIGNATURE grant (EXTENDROM-style; Self-Check) - Add permission + Developer Options toggle (EXTENDROM A15 port) Attribution: EXTENDROM (steadfasterX), LineageOS microG spoof (LuK1337). Run on rom-dev01: python3 apply_005_sigspoof.py """ from __future__ import annotations import re import shutil from pathlib import Path TREE = Path("/srv/rom/src/lineage") PHONESKY_HEX = Path("/srv/rom/artifacts/005-spoofing/phonesky-cert.hex").read_text().strip() MARKER = "SUNFISH_A16_005_SIGSPOOF" def backup(path: Path) -> None: # Never leave *.bak inside source trees (aapt2 compiles everything under res/). bak_root = Path("/srv/rom/artifacts/005-spoofing/bak-snapshots") bak_root.mkdir(parents=True, exist_ok=True) rel = path try: rel = path.relative_to(TREE) except ValueError: pass bak = bak_root / str(rel).replace("/", "_") if not bak.exists(): shutil.copy2(path, bak) def patch_computer_engine() -> None: path = TREE / "frameworks/base/services/core/java/com/android/server/pm/ComputerEngine.java" text = path.read_text() # AXP/EXTENDROM spoofs on user images (GOLDEN ro.debuggable=0). LOS stock # isDebuggable() would disable that on lineage_sunfish-user. deb_gate = """ public static boolean isMicrogSigned(AndroidPackage p) { if (!isDebuggable()) { return false; } """ user_ok = """ public static boolean isMicrogSigned(AndroidPackage p) { // %s: no isDebuggable() gate (AXP user / GOLDEN). """ % MARKER if MARKER in text: if deb_gate not in text: print("ComputerEngine: already patched, user gate ok") return backup(path) path.write_text(text.replace(deb_gate, user_ok, 1)) print("ComputerEngine: dropped isDebuggable gate") return backup(path) real_pat = re.compile( r'(private static final Signature MICROG_REAL_SIGNATURE = new Signature\("[0-9a-f]+"\);)' ) m = real_pat.search(text) if not m: raise SystemExit("MICROG_REAL_SIGNATURE not found") insert = ( m.group(1) + "\n\n" + " // %s: project JKS for patched Phonesky (sha256 " "313186cb7df2d40133721207b72973e852f61c8767c321e5c75039011b83bd56)\n" % MARKER + ' private static final Signature SUNFISH_PHONESKY_SIGNATURE = new Signature("%s");' % PHONESKY_HEX ) text = real_pat.sub(insert, text, count=1) old_is = """ public static boolean isMicrogSigned(AndroidPackage p) { if (!isDebuggable()) { return false; } // Allowlist the following apps: // * com.android.vending - microG Companion // * com.google.android.gms - microG Services if (!p.getPackageName().equals("com.android.vending") && !p.getPackageName().equals("com.google.android.gms")) { return false; } return Signature.areExactMatch( p.getSigningDetails(), new Signature[]{MICROG_REAL_SIGNATURE}); }""" new_is = """ public static boolean isMicrogSigned(AndroidPackage p) { // SUNFISH_A16_005_SIGSPOOF: no isDebuggable() gate (AXP user / GOLDEN). // Allowlist the following apps: // * com.android.vending - microG Companion / patched Phonesky // * com.google.android.gms - microG Services if (!p.getPackageName().equals("com.android.vending") && !p.getPackageName().equals("com.google.android.gms")) { return false; } if (Signature.areExactMatch( p.getSigningDetails(), new Signature[]{MICROG_REAL_SIGNATURE})) { return true; } // %s: accept project-signed Phonesky (AXP patch-playstore + our JKS) return p.getPackageName().equals("com.android.vending") && Signature.areExactMatch( p.getSigningDetails(), new Signature[]{SUNFISH_PHONESKY_SIGNATURE}); }""" % MARKER if old_is not in text: raise SystemExit("isMicrogSigned block not found exactly") text = text.replace(old_is, new_is, 1) old_gen = """ private static Optional generateFakeSignature(AndroidPackage p) { if (!isMicrogSigned(p)) { return Optional.empty(); } Bundle metadata = p.getMetaData(); if (metadata == null) { return Optional.empty(); } String fakeSignatureStr = metadata.getString("fake-signature"); if (TextUtils.isEmpty(fakeSignatureStr)) { return Optional.empty(); } // Only MICROG_FAKE_SIGNATURE can be faked Signature fakeSignature = new Signature(fakeSignatureStr); if (!fakeSignature.equals(MICROG_FAKE_SIGNATURE)) { return Optional.empty(); } return Optional.of(fakeSignature); }""" new_gen = """ private boolean requestsFakeSignature(AndroidPackage p) { return p.getMetaData() != null && p.getMetaData().getString("fake-signature") != null; } private Optional generateFakeSignature(AndroidPackage p, Set permissions) { // %s: EXTENDROM global gate (Developer Options) try { if (android.provider.Settings.Secure.getInt(mContext.getContentResolver(), android.provider.Settings.Secure.ALLOW_SIGNATURE_FAKE, 0) == 0) { return Optional.empty(); } } catch (Throwable t) { Slog.w(TAG, "allow_signature_fake read failed", t); return Optional.empty(); } if (!isMicrogSigned(p)) { return Optional.empty(); } // EXTENDROM: require FAKE_PACKAGE_SIGNATURE grant (Self-Check path) if (!permissions.contains("android.permission.FAKE_PACKAGE_SIGNATURE")) { return Optional.empty(); } Bundle metadata = p.getMetaData(); if (metadata == null) { return Optional.empty(); } String fakeSignatureStr = metadata.getString("fake-signature"); if (TextUtils.isEmpty(fakeSignatureStr)) { return Optional.empty(); } // Only MICROG_FAKE_SIGNATURE (platform Google) can be faked Signature fakeSignature = new Signature(fakeSignatureStr); if (!fakeSignature.equals(MICROG_FAKE_SIGNATURE)) { return Optional.empty(); } return Optional.of(fakeSignature); }""" % MARKER if old_gen not in text: raise SystemExit("generateFakeSignature block not found exactly") text = text.replace(old_gen, new_gen, 1) old_perm = """ // Compute granted permissions only if package has requested permissions final Set grantedPermissions = ((flags & PackageManager.GET_PERMISSIONS) == 0 || ArrayUtils.isEmpty(p.getRequestedPermissions())) ? Collections.emptySet() : mPermissionManager.getGrantedPermissions(ps.getPackageName(), userId);""" new_perm = """ // Compute granted permissions only if package has requested permissions // SUNFISH_A16_005_SIGSPOOF: also when fake-signature meta present (need grant check) final Set grantedPermissions = (((flags & PackageManager.GET_PERMISSIONS) == 0 && !requestsFakeSignature(p)) || ArrayUtils.isEmpty(p.getRequestedPermissions())) ? Collections.emptySet() : mPermissionManager.getGrantedPermissions(ps.getPackageName(), userId);""" if old_perm not in text: raise SystemExit("grantedPermissions block not found") text = text.replace(old_perm, new_perm, 1) old_call = "generateFakeSignature(p).ifPresent(fakeSignature -> {" new_call = "generateFakeSignature(p, grantedPermissions).ifPresent(fakeSignature -> {" if old_call not in text: raise SystemExit("generateFakeSignature call site not found") text = text.replace(old_call, new_call, 1) path.write_text(text) print("ComputerEngine: patched OK") def patch_settings_secure() -> None: path = TREE / "frameworks/base/core/java/android/provider/Settings.java" text = path.read_text() if MARKER in text: print("Settings.java: already patched") return backup(path) needle = ( ' public static final String INSTALL_NON_MARKET_APPS = "install_non_market_apps";\n' "\n" " /**\n" " * A flag to tell {@link com.android.server.devicepolicy.DevicePolicyManagerService} that\n" " * the default for {@link #INSTALL_NON_MARKET_APPS} is reversed for this user on OTA. So it\n" ) insert = ( ' public static final String INSTALL_NON_MARKET_APPS = "install_non_market_apps";\n' "\n" " /**\n" " * %s / EXTENDROM: whether applications can fake a signature.\n" " *\n" " *

1 = permit apps to fake signature\n" " *

0 = disable this feature\n" " * @hide\n" " */\n" ' public static final String ALLOW_SIGNATURE_FAKE = "allow_signature_fake";\n' "\n" " /**\n" " * A flag to tell {@link com.android.server.devicepolicy.DevicePolicyManagerService} that\n" " * the default for {@link #INSTALL_NON_MARKET_APPS} is reversed for this user on OTA. So it\n" ) % MARKER if needle not in text: raise SystemExit("Settings.java insertion point not found") path.write_text(text.replace(needle, insert, 1)) print("Settings.java: patched OK") def patch_android_manifest() -> None: path = TREE / "frameworks/base/core/res/AndroidManifest.xml" text = path.read_text() if MARKER in text: print("AndroidManifest: already patched") return backup(path) m = re.search( r'( \s*)', text, ) if not m: m = re.search( r'( ]*>[\s\S]*?\s*)', text, ) if not m: raise SystemExit("UNDEFINED permission-group not found") block = ( m.group(1) + "\n" + " \n" % MARKER + ' \n' + "\n" + ' \n' + "\n" ) text = text[: m.start()] + block + text[m.end() :] path.write_text(text) print("AndroidManifest: patched OK") def patch_framework_strings() -> None: path = TREE / "frameworks/base/core/res/res/values/strings.xml" text = path.read_text() if MARKER in text: print("framework strings: already patched") return backup(path) addition = ( "\n" " \n" ' Spoof package signature\n' ' Allows applications to impersonate another application (known as \\"signature spoofing\\").\n' ' Spoof package signature\n' ' Spoof package signature\n' ) % MARKER if "" not in text: raise SystemExit("framework strings: no ") path.write_text(text.replace("", addition + "", 1)) print("framework strings: patched OK") def patch_api_current() -> None: path = TREE / "frameworks/base/core/api/current.txt" text = path.read_text() if "FAKE_PACKAGE_SIGNATURE" in text: print("current.txt: already has FAKE_PACKAGE_SIGNATURE") return backup(path) text2 = text.replace( ' field public static final String FACTORY_TEST = "android.permission.FACTORY_TEST";\n', ' field public static final String FACTORY_TEST = "android.permission.FACTORY_TEST";\n' ' field public static final String FAKE_PACKAGE_SIGNATURE = "android.permission.FAKE_PACKAGE_SIGNATURE";\n', 1, ) if 'field public static final String FAKE_PACKAGE = "android.permission-group.FAKE_PACKAGE";' not in text2: text2 = text2.replace( ' field public static final String CONTACTS = "android.permission-group.CONTACTS";\n', ' field public static final String CONTACTS = "android.permission-group.CONTACTS";\n' ' field public static final String FAKE_PACKAGE = "android.permission-group.FAKE_PACKAGE";\n', 1, ) if text2 == text: raise SystemExit("current.txt: failed to insert") path.write_text(text2) print("current.txt: patched OK") def patch_lint_baseline() -> None: path = TREE / "frameworks/base/core/api/lint-baseline.txt" text = path.read_text() if "FAKE_PACKAGE_SIGNATURE" in text: print("lint-baseline: already patched") return backup(path) addition = """UnflaggedApi: android.Manifest.permission#FAKE_PACKAGE_SIGNATURE: New API must be flagged with @FlaggedApi: field android.Manifest.permission.FAKE_PACKAGE_SIGNATURE UnflaggedApi: android.Manifest.permission_group#FAKE_PACKAGE: New API must be flagged with @FlaggedApi: field android.Manifest.permission_group.FAKE_PACKAGE """ path.write_text(text + addition) print("lint-baseline: patched OK") def patch_activity() -> None: path = TREE / "frameworks/base/core/java/android/app/Activity.java" text = path.read_text() if MARKER in text: print("Activity.java: already patched") return backup(path) needle = """ final PackageManager packageManager = context.getPackageManager(); final Intent intent = packageManager.buildRequestPermissionsIntent(permissions); startActivityForResult(REQUEST_PERMISSIONS_WHO_PREFIX, intent, requestCode, null); mHasCurrentPermissionsRequest = true; } @NonNull private int[] getPermissionRequestStates(@NonNull Context deviceContext, """ insert = """ final PackageManager packageManager = context.getPackageManager(); // %s / EXTENDROM: deny FAKE_PACKAGE dialog when spoofing toggle off java.util.List permlist = java.util.Arrays.asList(permissions); if (permlist.contains("android.permission.FAKE_PACKAGE_SIGNATURE")) { if (android.provider.Settings.Secure.getInt(getContentResolver(), android.provider.Settings.Secure.ALLOW_SIGNATURE_FAKE, 0) == 0) { Log.w(TAG, "FAKE_PACKAGE_SIGNATURE denied: allow_signature_fake is off"); return; } } final Intent intent = packageManager.buildRequestPermissionsIntent(permissions); startActivityForResult(REQUEST_PERMISSIONS_WHO_PREFIX, intent, requestCode, null); mHasCurrentPermissionsRequest = true; } @NonNull private int[] getPermissionRequestStates(@NonNull Context deviceContext, """ % MARKER if needle not in text: raise SystemExit("Activity.java insertion point not found") path.write_text(text.replace(needle, insert, 1)) print("Activity.java: patched OK") def patch_permission_mapping() -> None: path = ( TREE / "packages/modules/Permission/PermissionController/src/com/android/permissioncontroller/permission/utils/PermissionMapping.kt" ) text = path.read_text() if MARKER in text: print("PermissionMapping: already patched") return backup(path) needle = "PLATFORM_PERMISSIONS[Manifest.permission.BODY_SENSORS] = Manifest.permission_group.SENSORS" addition = ( "\n\n // %s\n" " PLATFORM_PERMISSIONS[Manifest.permission.FAKE_PACKAGE_SIGNATURE] =" " Manifest.permission_group.FAKE_PACKAGE" % MARKER ) if needle in text: text = text.replace(needle, needle + addition, 1) else: needle2 = ( "PLATFORM_PERMISSIONS[Manifest.permission.GET_ACCOUNTS] = Manifest.permission_group.CONTACTS" ) if needle2 not in text: raise SystemExit("PermissionMapping insertion point not found") text = text.replace(needle2, needle2 + addition, 1) path.write_text(text) print("PermissionMapping: patched OK") def patch_settings_app() -> None: dash = ( TREE / "packages/apps/Settings/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java" ) text = dash.read_text() if MARKER not in text: backup(dash) needle = " controllers.add(new SensitiveContentProtectionPreferenceController(context));\n" if needle not in text: raise SystemExit("Dashboard insertion not found") text = text.replace( needle, needle + " // %s\n" % MARKER + " controllers.add(new SpoofSignaturePreferenceController(context));\n" + " controllers.add(new SpoofSignatureInfo(context));\n", 1, ) dash.write_text(text) print("Settings dashboard: patched OK") else: print("Settings dashboard: already patched") ctrl = ( TREE / "packages/apps/Settings/src/com/android/settings/development/SpoofSignaturePreferenceController.java" ) if not ctrl.exists(): ctrl.write_text( """/* * Copyright (C) 2019-2024 steadfasterX * LICENSE: GNU General Public License v3.0 * Ported to LOS 23.2 / sunfish-a16-rom (%s). */ package com.android.settings.development; import android.content.Context; import android.provider.Settings; import androidx.annotation.VisibleForTesting; import androidx.preference.Preference; import androidx.preference.SwitchPreference; import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.development.DeveloperOptionsPreferenceController; public class SpoofSignaturePreferenceController extends DeveloperOptionsPreferenceController implements Preference.OnPreferenceChangeListener, PreferenceControllerMixin { private static final String ALLOW_SIGNATURE_FAKE_KEY = "allow_signature_fake"; @VisibleForTesting static final int SETTING_VALUE_ON = 1; @VisibleForTesting static final int SETTING_VALUE_OFF = 0; public SpoofSignaturePreferenceController(Context context) { super(context); } @Override public String getPreferenceKey() { return ALLOW_SIGNATURE_FAKE_KEY; } @Override public boolean onPreferenceChange(Preference preference, Object newValue) { final boolean isEnabled = (Boolean) newValue; Settings.Secure.putInt(mContext.getContentResolver(), Settings.Secure.ALLOW_SIGNATURE_FAKE, isEnabled ? SETTING_VALUE_ON : SETTING_VALUE_OFF); return true; } @Override public void updateState(Preference preference) { final int spoofingMode = Settings.Secure.getInt(mContext.getContentResolver(), Settings.Secure.ALLOW_SIGNATURE_FAKE, SETTING_VALUE_OFF); ((SwitchPreference) mPreference).setChecked(spoofingMode != SETTING_VALUE_OFF); } @Override protected void onDeveloperOptionsSwitchDisabled() { super.onDeveloperOptionsSwitchDisabled(); Settings.Secure.putInt(mContext.getContentResolver(), Settings.Secure.ALLOW_SIGNATURE_FAKE, SETTING_VALUE_OFF); ((SwitchPreference) mPreference).setChecked(false); } } """ % MARKER ) print("SpoofSignaturePreferenceController: written") info = ( TREE / "packages/apps/Settings/src/com/android/settings/development/SpoofSignatureInfo.java" ) if not info.exists(): info.write_text( """/* * Copyright (C) 2019-2024 steadfasterX * LICENSE: GNU General Public License v3.0 * Ported to LOS 23.2 / sunfish-a16-rom (%s). */ package com.android.settings.development; import android.content.Context; import androidx.preference.Preference; import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.development.DeveloperOptionsPreferenceController; public class SpoofSignatureInfo extends DeveloperOptionsPreferenceController implements Preference.OnPreferenceChangeListener, PreferenceControllerMixin { private static final String ALLOW_SIGNATURE_FAKE_KEY_INFO = "allow_signature_fake_info"; public SpoofSignatureInfo(Context context) { super(context); } @Override public String getPreferenceKey() { return ALLOW_SIGNATURE_FAKE_KEY_INFO; } @Override public boolean onPreferenceChange(Preference preference, Object newValue) { return true; } } """ % MARKER ) print("SpoofSignatureInfo: written") xml = TREE / "packages/apps/Settings/res/xml/development_settings.xml" xtext = xml.read_text() if MARKER not in xtext: backup(xml) needle = """ android:key="development_prefs_screen" android:title="@string/development_settings_title"> \n" ' Signature spoofing support\n' ' Allow signature spoofing\n' ' If disabled, any previously granted signature spoofing permissions remain in place, but no longer have any effect\n' ' Allows applications to impersonate another application (known as \\"signature spoofing\\").\\n\\nLegitimate applications include microG or Phonesky (Google Play Store for microG).\\n\\nThis option is a global ON/OFF setting, i.e. applications must explicitly request signature spoofing, or it must be manually enabled within each App\\\'s \\"Permission\\" screen (this permission can also be revoked there).\n' ) % MARKER sxml.write_text(stext.replace("", addition + "", 1)) print("Settings strings: patched OK") else: print("Settings strings: already patched") def main() -> None: if not PHONESKY_HEX or len(PHONESKY_HEX) < 100: raise SystemExit("phonesky-cert.hex missing/short") print("Phonesky cert hex len=%d" % len(PHONESKY_HEX)) patch_computer_engine() patch_settings_secure() patch_android_manifest() patch_framework_strings() patch_api_current() patch_lint_baseline() patch_activity() patch_permission_mapping() patch_settings_app() print("ALL DONE") if __name__ == "__main__": main()