From 8f3184dada6264639a9109313434e1b1f22447c1 Mon Sep 17 00:00:00 2001 From: Martin Date: Fri, 16 Oct 2026 14:52:04 +0800 Subject: [PATCH 1/5] Let a patched app sign in to Google through an installed MicroG A patched app is signed with LSPatch's key, so Google Play services refuses it wherever it checks the caller's certificate, Google sign-in among them. The MicroG option redirects the app's GMS intents, provider calls and actions to an installed MicroG, and declares the original certificate in both forms MicroG builds read: fake-signature with FAKE_PACKAGE_SIGNATURE for a ROM with signature spoofing, and app.revanced.android.gms.SPOOFED_PACKAGE_SIGNATURE (the certificate's SHA-1) for ReVanced GmsCore. QUERY_ALL_PACKAGES lets the loader find MicroG, since the app's own queries name only Google Play services. An intent the app addresses to itself keeps its action, including when it arrives through readFromParcel: the Google sign-in library opens its own SignInHubActivity with com.google.android.gms.auth.GOOGLE_SIGN_IN, and that activity finishes on any other action. The option is recorded in the patch config, so a re-patch keeps it, and is offered in the manager and as --microg on the command line. Co-Authored-By: NkBe Co-Authored-By: MrZhongzq <108169409+mrzhongzq@users.noreply.github.com> --- .../main/java/org/lsposed/lspatch/Patcher.kt | 1 + .../lspatch/data/model/PatchRequest.kt | 2 + .../lspatch/data/repository/PatchReport.kt | 1 + .../lsposed/lspatch/ui/page/NewPatchScreen.kt | 13 + .../org/lsposed/lspatch/ui/page/PatchEntry.kt | 1 + .../lspatch/ui/viewmodel/NewPatchViewModel.kt | 2 + .../lsposed/lspatch/util/LocalAppsUpdater.kt | 1 + .../src/main/res/values-zh-rCN/strings.xml | 2 + .../src/main/res/values-zh-rTW/strings.xml | 2 + manager/src/main/res/values/strings.xml | 2 + .../lsposed/lspatch/loader/GmsRedirector.java | 516 ++++++++++++++++++ .../lspatch/loader/LSPApplication.java | 5 + .../java/org/lsposed/patch/ApkPatcher.java | 60 +- .../main/java/org/lsposed/patch/LSPatch.java | 4 + .../org/lsposed/patch/ManifestOverrides.java | 18 +- .../org/lsposed/lspatch/share/Constants.java | 7 + .../lsposed/lspatch/share/PatchConfig.java | 8 + 17 files changed, 635 insertions(+), 10 deletions(-) create mode 100644 patch-loader/src/main/java/org/lsposed/lspatch/loader/GmsRedirector.java diff --git a/manager/src/main/java/org/lsposed/lspatch/Patcher.kt b/manager/src/main/java/org/lsposed/lspatch/Patcher.kt index 91eaaab68..4f0623114 100644 --- a/manager/src/main/java/org/lsposed/lspatch/Patcher.kt +++ b/manager/src/main/java/org/lsposed/lspatch/Patcher.kt @@ -47,6 +47,7 @@ object Patcher { .usesCleartextTraffic(if (usesCleartextTraffic) true else null) .permissions(addedPermissions) .injectDocumentsProvider(injectDocumentsProvider) + .useMicroG(useMicroG) .build() ) .keystore( diff --git a/manager/src/main/java/org/lsposed/lspatch/data/model/PatchRequest.kt b/manager/src/main/java/org/lsposed/lspatch/data/model/PatchRequest.kt index aed721519..88ff6dc64 100644 --- a/manager/src/main/java/org/lsposed/lspatch/data/model/PatchRequest.kt +++ b/manager/src/main/java/org/lsposed/lspatch/data/model/PatchRequest.kt @@ -91,6 +91,8 @@ data class PatchRequest( // Inject a DocumentsProvider exposing the app's private data. Recorded in the config for the same // reason as the permissions above. val injectDocumentsProvider: Boolean = false, + // Redirect Google Play services to an installed MicroG. Recorded in the config for the same reason. + val useMicroG: Boolean = false, val origin: PatchOrigin = PatchOrigin.New, ) { val packageName: String get() = target.packageName diff --git a/manager/src/main/java/org/lsposed/lspatch/data/repository/PatchReport.kt b/manager/src/main/java/org/lsposed/lspatch/data/repository/PatchReport.kt index b07730d9a..9807210e8 100644 --- a/manager/src/main/java/org/lsposed/lspatch/data/repository/PatchReport.kt +++ b/manager/src/main/java/org/lsposed/lspatch/data/repository/PatchReport.kt @@ -60,6 +60,7 @@ object PatchReport { "Added perms ${if (request.addedPermissions.isEmpty()) "none" else request.addedPermissions.joinToString(", ")}" ) add("Docs provider ${request.injectDocumentsProvider}") + add("MicroG ${request.useMicroG}") add("Keystore ${if (MyKeyStore.useDefault) "built-in" else "custom (${MyKeyStore.file.name})"}") add("Verbose ${Configs.detailPatchLogs}") add("") diff --git a/manager/src/main/java/org/lsposed/lspatch/ui/page/NewPatchScreen.kt b/manager/src/main/java/org/lsposed/lspatch/ui/page/NewPatchScreen.kt index 480306558..16f9f98d9 100644 --- a/manager/src/main/java/org/lsposed/lspatch/ui/page/NewPatchScreen.kt +++ b/manager/src/main/java/org/lsposed/lspatch/ui/page/NewPatchScreen.kt @@ -38,6 +38,7 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.rounded.Notes import androidx.compose.material.icons.outlined.Ballot +import androidx.compose.material.icons.rounded.AccountCircle import androidx.compose.material.icons.rounded.Add import androidx.compose.material.icons.rounded.Android import androidx.compose.material.icons.rounded.Api @@ -340,6 +341,7 @@ fun NewPatchScreen(navigator: Navigator, token: String) { onAddPermission = viewModel::addPermission, onRemovePermission = viewModel::removePermission, onInjectDocumentsProvider = viewModel::setInjectDocumentsProvider, + onUseMicroG = viewModel::setUseMicroG, ) else -> Column( @@ -387,6 +389,7 @@ private fun ConfigureBody( onAddPermission: (String) -> Unit, onRemovePermission: (String) -> Unit, onInjectDocumentsProvider: (Boolean) -> Unit, + onUseMicroG: (Boolean) -> Unit, ) { val scope = rememberCoroutineScope() val snackbarHost = LocalSnackbarHost.current @@ -595,6 +598,13 @@ private fun ConfigureBody( checked = request.injectDocumentsProvider, onCheckedChange = onInjectDocumentsProvider, ) + ToggleRow( + title = stringResource(R.string.patch_manifest_microg), + icon = Icons.Rounded.AccountCircle, + subtitle = stringResource(R.string.patch_manifest_microg_desc), + checked = request.useMicroG, + onCheckedChange = onUseMicroG, + ) // Last of the manifest controls: its chip list and field are the tallest thing here, // so it sits below the compact toggles rather than pushing them down the screen. PermissionEditor( @@ -779,6 +789,9 @@ private fun advancedChips(request: PatchRequest): List = buildLi if (request.injectDocumentsProvider) { add(OptionChipData(stringResource(R.string.patch_manifest_documents_provider), true, Icons.Rounded.FolderOpen)) } + if (request.useMicroG) { + add(OptionChipData(stringResource(R.string.patch_manifest_microg), true, Icons.Rounded.AccountCircle)) + } if (!MyKeyStore.useDefault) { add(OptionChipData(stringResource(R.string.settings_keystore_custom), true, Icons.Outlined.Ballot)) } diff --git a/manager/src/main/java/org/lsposed/lspatch/ui/page/PatchEntry.kt b/manager/src/main/java/org/lsposed/lspatch/ui/page/PatchEntry.kt index 030d0c678..d2c7aa868 100644 --- a/manager/src/main/java/org/lsposed/lspatch/ui/page/PatchEntry.kt +++ b/manager/src/main/java/org/lsposed/lspatch/ui/page/PatchEntry.kt @@ -79,6 +79,7 @@ suspend fun rePatchRequestFor( // Recovered from the recorded config, not the original apks -- those never carried them. addedPermissions = config?.addedPermissions?.toList() ?: emptyList(), injectDocumentsProvider = config?.injectDocumentsProvider ?: false, + useMicroG = config?.useMicroG ?: false, sigBypassLevel = config?.sigBypassLevel ?: 2, injectDex = config?.injectDex ?: false, modules = modules ?: embedded, diff --git a/manager/src/main/java/org/lsposed/lspatch/ui/viewmodel/NewPatchViewModel.kt b/manager/src/main/java/org/lsposed/lspatch/ui/viewmodel/NewPatchViewModel.kt index 1f383b140..f6fcee475 100644 --- a/manager/src/main/java/org/lsposed/lspatch/ui/viewmodel/NewPatchViewModel.kt +++ b/manager/src/main/java/org/lsposed/lspatch/ui/viewmodel/NewPatchViewModel.kt @@ -109,6 +109,8 @@ class NewPatchViewModel(private val token: String) : ViewModel() { fun setInjectDocumentsProvider(value: Boolean) = mutate { it.copy(injectDocumentsProvider = value) } + fun setUseMicroG(value: Boolean) = mutate { it.copy(useMicroG = value) } + /** * Adds modules to the set, keeping what is already there. * diff --git a/manager/src/main/java/org/lsposed/lspatch/util/LocalAppsUpdater.kt b/manager/src/main/java/org/lsposed/lspatch/util/LocalAppsUpdater.kt index 34dabfc62..bfb73eb9e 100644 --- a/manager/src/main/java/org/lsposed/lspatch/util/LocalAppsUpdater.kt +++ b/manager/src/main/java/org/lsposed/lspatch/util/LocalAppsUpdater.kt @@ -127,6 +127,7 @@ object LocalAppsUpdater { .versionCode(config.versionCode) .permissions(config.addedPermissions?.toList() ?: emptyList()) .injectDocumentsProvider(config.injectDocumentsProvider) + .useMicroG(config.useMicroG) .build() ) .keystore( diff --git a/manager/src/main/res/values-zh-rCN/strings.xml b/manager/src/main/res/values-zh-rCN/strings.xml index 6fc417ca8..d97530422 100644 --- a/manager/src/main/res/values-zh-rCN/strings.xml +++ b/manager/src/main/res/values-zh-rCN/strings.xml @@ -269,6 +269,8 @@ 移除 已添加 %1$d 个 暴露私有数据 + MicroG 支持 + 将 Google Play 服务的调用转向已安装的 MicroG(ReVanced GmsCore 或 microG),并向它声明原始签名,使重新签名后仍能使用 Google 登录。 添加一个提供程序,让文件管理器无需 root 即可通过系统文件选择器浏览应用的私有数据目录。这会扩大可访问应用数据的范围——若无需要请保持关闭。 包名 更改包名 diff --git a/manager/src/main/res/values-zh-rTW/strings.xml b/manager/src/main/res/values-zh-rTW/strings.xml index c095b3448..ebbd5871b 100644 --- a/manager/src/main/res/values-zh-rTW/strings.xml +++ b/manager/src/main/res/values-zh-rTW/strings.xml @@ -165,6 +165,8 @@ 移除 已新增 %1$d 個 公開私有資料 + MicroG 支援 + 將 Google Play 服務的呼叫導向已安裝的 MicroG(ReVanced GmsCore 或 microG),並向它宣告原始簽章,讓重新簽名後仍能使用 Google 登入。 新增一個提供者,讓檔案管理器無需 root 即可透過系統檔案選擇器瀏覽應用程式的私有資料目錄。這會擴大可存取應用程式資料的範圍——若無需要請保持關閉。 選擇要修補的應用程式 正在載入已安裝的應用程式… diff --git a/manager/src/main/res/values/strings.xml b/manager/src/main/res/values/strings.xml index 764aa6648..e88530231 100644 --- a/manager/src/main/res/values/strings.xml +++ b/manager/src/main/res/values/strings.xml @@ -119,6 +119,8 @@ Remove %1$d added Expose private data + MicroG support + Send Google Play services calls to an installed MicroG (ReVanced GmsCore or microG) and declare the original signature to it, so Google sign-in keeps working after re-signing. Add a provider so a file manager can browse the app\'s private data folder through the system file picker, no root needed. Widens what can reach the app\'s data — leave off unless you need it. Patching… Patch failed diff --git a/patch-loader/src/main/java/org/lsposed/lspatch/loader/GmsRedirector.java b/patch-loader/src/main/java/org/lsposed/lspatch/loader/GmsRedirector.java new file mode 100644 index 000000000..d31a5345c --- /dev/null +++ b/patch-loader/src/main/java/org/lsposed/lspatch/loader/GmsRedirector.java @@ -0,0 +1,516 @@ +package org.lsposed.lspatch.loader; + +import android.content.ComponentName; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.Signature; +import android.net.Uri; +import android.util.Log; + +import org.lsposed.lspatch.loader.util.XLog; +import org.lsposed.lspatch.share.Constants; + +import java.util.HashMap; +import java.util.Map; + +import de.robv.android.xposed.XC_MethodHook; +import de.robv.android.xposed.XposedBridge; +import de.robv.android.xposed.XposedHelpers; + +/** + * Points a patched app's Google Play services traffic at an installed MicroG. + * + * A patched app is signed with LSPatch's key, so Google Play services rejects it wherever it checks the caller's + * certificate -- most visibly at Google sign-in. MicroG answers the same requests under its own package, so this + * rewrites the package, component, provider authority and action of every GMS-bound intent or provider call to the + * MicroG package found on the device. Which certificate MicroG then reports for the app is decided by the manifest + * metadata the patcher wrote, not here. + * + * Community MicroG builds such as ReVanced GmsCore rename the package to {@code .android.gms} and every action + * along with it, so actions are rewritten too. An intent the app addresses to itself is never rewritten: the Google + * sign-in library opens its own {@code SignInHubActivity} with {@code com.google.android.gms.auth.GOOGLE_SIGN_IN}, and + * that activity finishes on any other action. + */ +public class GmsRedirector { + private static final String TAG = "LSPatch-GmsRedirect"; + private static final String REAL_GMS = Constants.REAL_GMS_PACKAGE_NAME; + + private static final String[] MICROG_PACKAGES = { + Constants.MICROG_RE_PACKAGE_NAME, // ReVanced GmsCore + "org.microg.gms", // upstream microG + }; + + private static String targetGms = null; + private static String selfPackage = null; + private static String originalSignature; + + public static void activate(Context context, String origSig) { + originalSignature = origSig; + selfPackage = context.getPackageName(); + + targetGms = findInstalledMicroG(context); + if (targetGms == null) { + Log.w(TAG, "No MicroG/GmsCore found! GMS redirect disabled."); + return; + } + + Log.i(TAG, "Activating GMS redirect: " + REAL_GMS + " -> " + targetGms); + setupC2dmRedirects(); + + hookIntentSetPackage(); + hookIntentSetAction(); + hookIntentGetAction(); + hookIntentSetComponent(); + hookIntentSetClass(); + hookIntentResolve(); + hookActivityStart(); + hookContentResolverAcquire(); + hookPackageManagerGetPackageInfo(context); + + Log.i(TAG, "GMS redirect hooks installed"); + } + + private static String findInstalledMicroG(Context context) { + PackageManager pm = context.getPackageManager(); + for (String pkg : MICROG_PACKAGES) { + try { + pm.getPackageInfo(pkg, 0); + return pkg; + } catch (PackageManager.NameNotFoundException ignored) {} + } + return null; + } + + static void setTargetGmsForTest(String target, String self) { + targetGms = target; + selfPackage = self; + setupC2dmRedirects(); + } + + static String redirectPackage(String pkg) { + if (REAL_GMS.equals(pkg) || "com.google.android.gsf".equals(pkg)) { + return targetGms; + } + return null; + } + + // MicroG does not provide Dynamite/Chimera modules. Redirecting chimera causes + // provider Authority mismatch and Uri disagreements. Left untouched, real GMS answers it. + private static final String CHIMERA_AUTHORITY = REAL_GMS + ".chimera"; + + static String redirectAuthority(String authority) { + if (authority == null) return null; + if (CHIMERA_AUTHORITY.equals(authority)) return null; + if (authority.startsWith(REAL_GMS + ".")) { + return targetGms + authority.substring(REAL_GMS.length()); + } + if (authority.equals(REAL_GMS)) { + return targetGms; + } + if (authority.startsWith("com.google.android.gsf")) { + return authority.replace("com.google.android.gsf", targetGms); + } + return null; + } + + private static final String CHOOSE_ACCOUNT_ACTION = + "com.google.android.gms.common.account.CHOOSE_ACCOUNT"; + private static final ComponentName SYSTEM_ACCOUNT_PICKER; + static { + ComponentName picker = null; + try { + picker = ComponentName.unflattenFromString("android/.accounts.ChooseTypeAndAccountActivity"); + } catch (Throwable ignored) {} + SYSTEM_ACCOUNT_PICKER = picker; + } + + // Standard Google C2DM actions that need vendor redirection if targetGms has a custom namespace. + private static final Map c2dmRedirectMap = new HashMap<>(); + private static final String[] C2DM_STANDARD_ACTIONS = { + "com.google.android.c2dm.intent.REGISTER", + "com.google.android.c2dm.intent.RECEIVE", + "com.google.android.c2dm.intent.UNREGISTER", + "com.google.android.c2dm.intent.REGISTRATION", + }; + private static final String GMS_ACTION_PREFIX = "com.google.android.gms."; + + private static void setupC2dmRedirects() { + c2dmRedirectMap.clear(); + if (hasVendorNamespace()) { + String vendorC2dmPrefix = targetGms.substring(0, targetGms.length() - ".android.gms".length()) + ".android.c2dm"; + for (String standard : C2DM_STANDARD_ACTIONS) { + c2dmRedirectMap.put(standard, vendorC2dmPrefix + standard.substring("com.google.android.c2dm".length())); + } + Log.i(TAG, "C2DM redirect prefix derived: " + vendorC2dmPrefix); + } else { + Log.i(TAG, "C2DM redirect disabled for targetGms: " + targetGms); + } + } + + /** Whether the MicroG build renamed {@code com.google.android.gms} to {@code .android.gms}, actions included. */ + private static boolean hasVendorNamespace() { + return targetGms != null && !REAL_GMS.equals(targetGms) && targetGms.endsWith(".android.gms"); + } + + static boolean isChooseAccountAction(String action) { + return CHOOSE_ACCOUNT_ACTION.equals(action) + || (targetGms != null && (targetGms + ".common.account.CHOOSE_ACCOUNT").equals(action)); + } + + private static void routeChooseAccountToSystem(Intent intent) { + if (intent == null || SYSTEM_ACCOUNT_PICKER == null || !isChooseAccountAction(intent.getAction())) { + return; + } + if (!SYSTEM_ACCOUNT_PICKER.equals(intent.getComponent()) || intent.getPackage() != null) { + XLog.d(TAG, "Routing CHOOSE_ACCOUNT directly to the system account picker"); + intent.setComponent(SYSTEM_ACCOUNT_PICKER); + intent.setPackage(null); + } + } + + static String redirectAction(String action) { + if (action == null) return null; + if (isChooseAccountAction(action)) return null; + String redirected = c2dmRedirectMap.get(action); + if (redirected != null) return redirected; + if (hasVendorNamespace() && action.startsWith(GMS_ACTION_PREFIX)) { + return targetGms + "." + action.substring(GMS_ACTION_PREFIX.length()); + } + return null; + } + + /** The action {@link #redirectAction} rewrote into {@code action}, or null if it is not a rewritten one. */ + static String originalAction(String action) { + if (action == null) return null; + for (Map.Entry entry : c2dmRedirectMap.entrySet()) { + if (entry.getValue().equals(action)) return entry.getKey(); + } + if (hasVendorNamespace() && action.startsWith(targetGms + ".") && !isChooseAccountAction(action)) { + return GMS_ACTION_PREFIX + action.substring(targetGms.length() + 1); + } + return null; + } + + static boolean isSelfTargeted(String pkg, ComponentName component) { + if (selfPackage == null) return false; + return selfPackage.equals(pkg) || (component != null && selfPackage.equals(component.getPackageName())); + } + + /** + * Undoes the action rewrite on an intent addressed to the app itself. + * + * The action is usually set before the target -- {@code new Intent(action).setClass(...)} -- so it has already been + * rewritten by the time the intent turns out to be the app's own. The field is written directly because + * {@code setAction} is hooked and would rewrite it again. + */ + private static void restoreSelfTargetedAction(Intent intent) { + if (intent == null || !isSelfTargeted(intent.getPackage(), intent.getComponent())) return; + String action = (String) XposedHelpers.getObjectField(intent, "mAction"); + String original = originalAction(action); + if (original != null) { + XLog.d(TAG, "Keeping app-internal action: " + action + " -> " + original); + XposedHelpers.setObjectField(intent, "mAction", original); + } + } + + private static void hookIntentSetPackage() { + try { + XposedBridge.hookAllMethods(Intent.class, "setPackage", new XC_MethodHook() { + @Override + protected void beforeHookedMethod(MethodHookParam param) { + Intent intent = (Intent) param.thisObject; + // Defense for the specific ordering of "setAction(CHOOSE_ACCOUNT)" before "setPackage(...)". + // The reverse sequence ("setPackage" before "setAction") is guarded by hookIntentSetAction's + // afterHookedMethod and final egress in hookActivityStart. + if (isChooseAccountAction(intent.getAction())) { + param.args[0] = null; + return; + } + String pkg = (String) param.args[0]; + String redirected = redirectPackage(pkg); + if (redirected != null) param.args[0] = redirected; + } + + @Override + protected void afterHookedMethod(MethodHookParam param) { + Intent intent = (Intent) param.thisObject; + restoreSelfTargetedAction(intent); + routeChooseAccountToSystem(intent); + } + }); + } catch (Throwable t) { + Log.e(TAG, "Failed to hook Intent.setPackage", t); + } + } + + private static void hookIntentSetAction() { + try { + XposedBridge.hookAllMethods(Intent.class, "setAction", new XC_MethodHook() { + @Override + protected void beforeHookedMethod(MethodHookParam param) { + Intent intent = (Intent) param.thisObject; + if (isSelfTargeted(intent.getPackage(), intent.getComponent())) return; + String action = (String) param.args[0]; + String redirected = redirectAction(action); + if (redirected != null) { + XLog.d(TAG, "Redirecting action in setAction: " + action + " -> " + redirected); + param.args[0] = redirected; + } + } + + @Override + protected void afterHookedMethod(MethodHookParam param) { + routeChooseAccountToSystem((Intent) param.thisObject); + } + }); + } catch (Throwable t) { + Log.e(TAG, "Failed to hook Intent.setAction", t); + } + } + + private static void hookIntentGetAction() { + try { + XposedBridge.hookAllMethods(Intent.class, "getAction", new XC_MethodHook() { + @Override + protected void afterHookedMethod(MethodHookParam param) { + String action = (String) param.getResult(); + if (action != null && targetGms != null) { + for (Map.Entry entry : c2dmRedirectMap.entrySet()) { + if (entry.getValue().equals(action)) { + XLog.d(TAG, "Restoring original action for getAction: " + action + " -> " + entry.getKey()); + param.setResult(entry.getKey()); + return; + } + } + } + } + }); + } catch (Throwable t) { + Log.e(TAG, "Failed to hook Intent.getAction", t); + } + } + + private static void hookIntentSetComponent() { + try { + XposedBridge.hookAllMethods(Intent.class, "setComponent", new XC_MethodHook() { + @Override + protected void beforeHookedMethod(MethodHookParam param) { + ComponentName cn = (ComponentName) param.args[0]; + if (cn != null) { + String redirected = redirectPackage(cn.getPackageName()); + if (redirected != null) { + param.args[0] = new ComponentName(redirected, cn.getClassName()); + } + } + } + + @Override + protected void afterHookedMethod(MethodHookParam param) { + Intent intent = (Intent) param.thisObject; + restoreSelfTargetedAction(intent); + routeChooseAccountToSystem(intent); + } + }); + } catch (Throwable t) { + Log.e(TAG, "Failed to hook Intent.setComponent", t); + } + } + + /** + * {@code setClass} and {@code setClassName} assign the component without going through {@code setComponent}, and + * {@code readFromParcel} -- how an activity's own launch intent arrives -- reads the action through + * {@code setAction} before it reads the component. + */ + private static void hookIntentSetClass() { + XC_MethodHook hook = new XC_MethodHook() { + @Override + protected void afterHookedMethod(MethodHookParam param) { + restoreSelfTargetedAction((Intent) param.thisObject); + } + }; + for (String method : new String[]{"setClass", "setClassName", "readFromParcel"}) { + try { + XposedBridge.hookAllMethods(Intent.class, method, hook); + } catch (Throwable t) { + Log.e(TAG, "Failed to hook Intent." + method, t); + } + } + } + + private static void hookIntentResolve() { + try { + XposedBridge.hookAllConstructors(Intent.class, new XC_MethodHook() { + @Override + protected void afterHookedMethod(MethodHookParam param) { + Intent intent = (Intent) param.thisObject; + ComponentName cn = intent.getComponent(); + if (cn != null) { + String redirected = redirectPackage(cn.getPackageName()); + if (redirected != null) { + intent.setComponent(new ComponentName(redirected, cn.getClassName())); + } + } + String pkg = intent.getPackage(); + if (pkg != null) { + String redirected = redirectPackage(pkg); + if (redirected != null) { + intent.setPackage(redirected); + } + } + String action = intent.getAction(); + if (action != null) { + String redirectedAction = redirectAction(action); + if (redirectedAction != null) { + intent.setAction(redirectedAction); + } + } + restoreSelfTargetedAction(intent); + routeChooseAccountToSystem(intent); + } + }); + } catch (Throwable t) { + Log.e(TAG, "Failed to hook Intent constructors", t); + } + } + + /** + * Egress interceptor for direct startActivity / startActivityForResult invocations. + * Note: Only covers direct caller invocations; indirect deliveries (e.g. wrapped in PendingIntent) + * are not in this scope and rely on Intent constructor / setter hooks. + */ + private static void hookActivityStart() { + try { + XC_MethodHook egressHook = new XC_MethodHook() { + @Override + protected void beforeHookedMethod(MethodHookParam param) { + if (param.args == null) return; + for (Object arg : param.args) { + if (arg instanceof Intent) { + restoreSelfTargetedAction((Intent) arg); + routeChooseAccountToSystem((Intent) arg); + break; + } + } + } + }; + XposedBridge.hookAllMethods(android.content.ContextWrapper.class, "startActivity", egressHook); + XposedBridge.hookAllMethods(android.app.Activity.class, "startActivityForResult", egressHook); + } catch (Throwable t) { + Log.e(TAG, "Failed to hook startActivity/startActivityForResult", t); + } + } + + private static void hookContentResolverAcquire() { + try { + for (String method : new String[]{ + "acquireProvider", "acquireContentProviderClient", + "acquireUnstableProvider", "acquireUnstableContentProviderClient" + }) { + try { + XposedBridge.hookAllMethods(ContentResolver.class, method, new XC_MethodHook() { + @Override + protected void beforeHookedMethod(MethodHookParam param) { + if (param.args[0] instanceof Uri) { + Uri uri = (Uri) param.args[0]; + String newAuth = redirectAuthority(uri.getAuthority()); + if (newAuth != null) { + param.args[0] = uri.buildUpon().authority(newAuth).build(); + } + } else if (param.args[0] instanceof String) { + String newAuth = redirectAuthority((String) param.args[0]); + if (newAuth != null) { + param.args[0] = newAuth; + } + } + } + }); + } catch (Throwable ignored) {} + } + + // A provider call real GMS refuses for the patched signature is retried against MicroG. + try { + XposedBridge.hookAllMethods(ContentResolver.class, "call", new XC_MethodHook() { + @Override + protected void beforeHookedMethod(MethodHookParam param) { + for (int i = 0; i < param.args.length; i++) { + if (param.args[i] instanceof Uri) { + Uri uri = (Uri) param.args[i]; + String newAuth = redirectAuthority(uri.getAuthority()); + if (newAuth != null) { + param.args[i] = uri.buildUpon().authority(newAuth).build(); + } + } else if (param.args[i] instanceof String && i == 0) { + String newAuth = redirectAuthority((String) param.args[i]); + if (newAuth != null) { + param.args[i] = newAuth; + } + } + } + } + + @Override + protected void afterHookedMethod(MethodHookParam param) { + if (param.getThrowable() instanceof SecurityException) { + String msg = param.getThrowable().getMessage(); + if (msg != null && (msg.contains("GoogleCertificatesRslt") || + msg.contains("not allowed") || + msg.contains("Access denied"))) { + Log.i(TAG, "GMS rejected call, retrying with MicroG"); + for (int i = 0; i < param.args.length; i++) { + if (param.args[i] instanceof Uri) { + Uri uri = (Uri) param.args[i]; + String authority = uri.getAuthority(); + if (authority != null && authority.contains(REAL_GMS)) { + param.args[i] = uri.buildUpon() + .authority(authority.replace(REAL_GMS, targetGms)) + .build(); + } + } else if (param.args[i] instanceof String && i == 0) { + String s = (String) param.args[i]; + if (s.contains(REAL_GMS)) { + param.args[i] = s.replace(REAL_GMS, targetGms); + } + } + } + param.setThrowable(null); + param.setResult(null); + } + } + } + }); + } catch (Throwable ignored) {} + } catch (Throwable t) { + Log.e(TAG, "Failed to hook ContentResolver", t); + } + } + + private static void hookPackageManagerGetPackageInfo(Context context) { + try { + XposedHelpers.findAndHookMethod( + context.getPackageManager().getClass(), + "getPackageInfo", + String.class, int.class, + new XC_MethodHook() { + @Override + protected void afterHookedMethod(MethodHookParam param) { + PackageInfo pi = (PackageInfo) param.getResult(); + if (pi != null && targetGms != null) { + if (targetGms.equals(pi.packageName) && (((int) param.args[1]) & PackageManager.GET_SIGNATURES) != 0) { + if (originalSignature != null && !originalSignature.isEmpty()) { + try { + pi.signatures = new Signature[]{new Signature(originalSignature)}; + } catch (Throwable ignored) {} + } + } + } + } + } + ); + } catch (Throwable t) { + Log.e(TAG, "Failed to hook PackageManager.getPackageInfo", t); + } + } +} diff --git a/patch-loader/src/main/java/org/lsposed/lspatch/loader/LSPApplication.java b/patch-loader/src/main/java/org/lsposed/lspatch/loader/LSPApplication.java index 06d0b13d6..06e61ce52 100644 --- a/patch-loader/src/main/java/org/lsposed/lspatch/loader/LSPApplication.java +++ b/patch-loader/src/main/java/org/lsposed/lspatch/loader/LSPApplication.java @@ -134,6 +134,11 @@ public static void onLoad() throws RemoteException, IOException { // realized, so a signature/APK read performed from that initializer already sees the spoof. SigBypass.doSigBypass(context, config.optInt("sigBypassLevel")); + // Armed before the class loader for the same reason: the app's GMS client may already run from that initializer. + if (config.optBoolean("useMicroG")) { + GmsRedirector.activate(context, config.optString("originalSignature")); + } + // Realize the target's class loader now that hooks, modules and signature bypass are all armed. // getClassLoader() -> createOrUpdateClassLoaderLocked -> createAppFactory triggers // onPackageLoaded (pre-) then, on return, onPackageReady and legacy handleLoadPackage. diff --git a/patch/src/main/java/org/lsposed/patch/ApkPatcher.java b/patch/src/main/java/org/lsposed/patch/ApkPatcher.java index 2a92ad7e3..68ea7a20e 100644 --- a/patch/src/main/java/org/lsposed/patch/ApkPatcher.java +++ b/patch/src/main/java/org/lsposed/patch/ApkPatcher.java @@ -187,10 +187,11 @@ private void patchOne(File srcApkFile, File outputFile, int index, int total) th spec.injectDex(), spec.manifestOverrides().addedPermissions.toArray(new String[0]), spec.manifestOverrides().injectDocumentsProvider, + spec.manifestOverrides().useMicroG, spec.useManager() ? spec.managerPackageName() : null); byte[] configBytes = new Gson().toJson(config).getBytes(StandardCharsets.UTF_8); - rewriteManifest(srcZFile, dstZFile, configBytes, manifest.packageName, manifest.minSdkVersion, index, total); + rewriteManifest(srcZFile, dstZFile, configBytes, manifest.packageName, manifest.minSdkVersion, originalSignature, index, total); injectLoader(srcZFile, dstZFile, index, total); if (!spec.useManager()) { addLoaderPayload(dstZFile); @@ -240,11 +241,12 @@ private void registerSigner(ZFile dstZFile, int index, int total) throws IOExcep /** * The original app's signature, recorded so the loader can report it back to the app. * - * Only read when signature bypass is on: a patched apk is signed with LSPatch's key, and an app - * that checks its own signature would otherwise notice. Nothing needs it when the bypass is off. + * Only read when signature bypass or MicroG is on: a patched apk is signed with LSPatch's key, and + * an app that checks its own signature -- or MicroG, reporting the app to Google -- would otherwise + * notice. Nothing needs it when both are off. */ private String readOriginalSignature(File srcApkFile) throws IOException { - if (spec.sigBypassLevel() <= Constants.SIGBYPASS_LV_DISABLE) return null; + if (spec.sigBypassLevel() <= Constants.SIGBYPASS_LV_DISABLE && !spec.manifestOverrides().useMicroG) return null; String signature = ApkSignatureHelper.getApkSignInfo(srcApkFile.getAbsolutePath()); if (signature == null || signature.isEmpty()) { throw new PatchException("Could not read the original apk's signature"); @@ -290,13 +292,13 @@ private void packSplit(NestedZip srcZFile, ZFile dstZFile, int index, int total) */ private void rewriteManifest( NestedZip srcZFile, ZFile dstZFile, byte[] configBytes, String packageName, int minSdkVersion, - int index, int total) + String originalSignature, int index, int total) throws IOException { logger.stage(Logger.Stage.REWRITING, index, total); logger.i("Patching apk..."); String metadata = Base64.getEncoder().encodeToString(configBytes); StoredEntry manifestEntry = Objects.requireNonNull(srcZFile.get(ANDROID_MANIFEST_XML)); - try (InputStream is = new ByteArrayInputStream(modifyManifestFile(manifestEntry.open(), metadata, packageName, minSdkVersion))) { + try (InputStream is = new ByteArrayInputStream(modifyManifestFile(manifestEntry.open(), metadata, packageName, minSdkVersion, originalSignature))) { dstZFile.add(ANDROID_MANIFEST_XML, is); } catch (IOException e) { throw new PatchException("Error when modifying manifest", e); @@ -427,7 +429,7 @@ private static boolean isSignatureEntry(String name) { && (name.endsWith(".SF") || name.endsWith(".MF") || name.endsWith(".RSA")); } - private byte[] modifyManifestFile(InputStream is, String metadata, String packageName, int minSdkVersion) + private byte[] modifyManifestFile(InputStream is, String metadata, String packageName, int minSdkVersion, String originalSignature) throws IOException { ModificationProperty property = new ModificationProperty(); @@ -438,7 +440,7 @@ private byte[] modifyManifestFile(InputStream is, String metadata, String packag property.addApplicationAttribute(new AttributeItem(NodeValue.Application.DEBUGGABLE, spec.debuggable())); property.addApplicationAttribute(new AttributeItem("appComponentFactory", PROXY_APP_COMPONENT_FACTORY)); property.addMetaData(new ModificationProperty.MetaData("lspatch", metadata)); - applyManifestOverrides(property, packageName); + applyManifestOverrides(property, packageName, originalSignature); // TODO: replace query_all with queries -> manager if (spec.useManager()) { property.addUsesPermission("android.permission.QUERY_ALL_PACKAGES"); @@ -461,7 +463,7 @@ private byte[] modifyManifestFile(InputStream is, String metadata, String packag * override changes the compatibility behaviours the platform applies; the two booleans flip * install-time and network policy an app otherwise fixes against a module's needs. */ - private void applyManifestOverrides(ModificationProperty property, String packageName) { + private void applyManifestOverrides(ModificationProperty property, String packageName, String originalSignature) { ManifestOverrides o = spec.manifestOverrides(); if (o.isEmpty()) return; if (o.versionCode != null) { @@ -493,6 +495,46 @@ private void applyManifestOverrides(ModificationProperty property, String packag if (o.injectDocumentsProvider) { addDocumentsProvider(property, packageName); } + if (o.useMicroG) { + addMicroGSupport(property, originalSignature); + } + } + + /** + * Declares the app's original certificate to MicroG, which otherwise reports LSPatch's to Google. + * + * MicroG builds read it two ways. Upstream microG relies on a ROM with signature spoofing, which + * reports the {@code fake-signature} metadata to an app holding {@code FAKE_PACKAGE_SIGNATURE}; + * ReVanced GmsCore instead reads {@code app.revanced.android.gms.SPOOFED_PACKAGE_SIGNATURE} from the + * calling app's manifest -- the certificate's SHA-1 in lower-case hex -- and needs no ROM support. + * Both are written so either build works. + * + * {@code QUERY_ALL_PACKAGES} lets the loader find the installed MicroG: the app's own + * {@code } name Google Play services, not MicroG, and the manifest editor cannot add one. + */ + private void addMicroGSupport(ModificationProperty property, String originalSignature) { + logger.i("Add MicroG support"); + property.addMetaData(new ModificationProperty.MetaData("fake-signature", originalSignature)); + property.addUsesPermission("android.permission.FAKE_PACKAGE_SIGNATURE"); + property.addMetaData(new ModificationProperty.MetaData( + Constants.MICROG_RE_PACKAGE_NAME + ".SPOOFED_PACKAGE_SIGNATURE", sha1Hex(originalSignature))); + property.addUsesPermission("android.permission.QUERY_ALL_PACKAGES"); + } + + /** SHA-1 of a certificate given as hex, as lower-case hex. */ + static String sha1Hex(String certificateHex) { + byte[] certificate = new byte[certificateHex.length() / 2]; + for (int i = 0; i < certificate.length; i++) { + certificate[i] = (byte) Integer.parseInt(certificateHex.substring(2 * i, 2 * i + 2), 16); + } + try { + byte[] digest = java.security.MessageDigest.getInstance("SHA-1").digest(certificate); + StringBuilder hex = new StringBuilder(digest.length * 2); + for (byte b : digest) hex.append(String.format(Locale.ROOT, "%02x", b)); + return hex.toString(); + } catch (java.security.NoSuchAlgorithmException e) { + throw new IllegalStateException(e); + } } /** diff --git a/patch/src/main/java/org/lsposed/patch/LSPatch.java b/patch/src/main/java/org/lsposed/patch/LSPatch.java index a4d2cfc86..25a055201 100644 --- a/patch/src/main/java/org/lsposed/patch/LSPatch.java +++ b/patch/src/main/java/org/lsposed/patch/LSPatch.java @@ -83,6 +83,9 @@ public class LSPatch { @Parameter(names = {"--documents-provider"}, description = "Inject a DocumentsProvider exposing the app's private data to the system file picker") private boolean injectDocumentsProvider = false; + @Parameter(names = {"--microg"}, description = "Redirect Google Play services calls to an installed MicroG and declare the original signature to it") + private boolean useMicroG = false; + private final JCommander jCommander; public LSPatch(String... args) { @@ -138,6 +141,7 @@ public PatchSpec toSpec() throws PatchException { .map(ManifestOverrides::normalizePermission) .collect(Collectors.toList())) .injectDocumentsProvider(injectDocumentsProvider) + .useMicroG(useMicroG) .build(); return PatchSpec.builder() .apks(apkPaths.stream().map(File::new).collect(Collectors.toList())) diff --git a/patch/src/main/java/org/lsposed/patch/ManifestOverrides.java b/patch/src/main/java/org/lsposed/patch/ManifestOverrides.java index a40cc3945..bac18fa2b 100644 --- a/patch/src/main/java/org/lsposed/patch/ManifestOverrides.java +++ b/patch/src/main/java/org/lsposed/patch/ManifestOverrides.java @@ -67,6 +67,15 @@ public final class ManifestOverrides { */ public final boolean injectDocumentsProvider; + /** + * Whether to redirect the app's Google Play services traffic to an installed MicroG, so Google sign-in and the + * other GMS APIs keep working for an app re-signed with LSPatch's key. The redirect ships in the loader; this + * declares the original certificate in the two forms MicroG builds read -- {@code fake-signature} with + * {@code FAKE_PACKAGE_SIGNATURE} for a ROM with signature spoofing, and ReVanced GmsCore's + * {@code SPOOFED_PACKAGE_SIGNATURE} -- and lets the app see which MicroG is installed. + */ + public final boolean useMicroG; + private ManifestOverrides(Builder b) { this.versionCode = b.versionCode; this.label = b.label; @@ -75,13 +84,14 @@ private ManifestOverrides(Builder b) { this.usesCleartextTraffic = b.usesCleartextTraffic; this.addedPermissions = Collections.unmodifiableList(new ArrayList<>(b.addedPermissions)); this.injectDocumentsProvider = b.injectDocumentsProvider; + this.useMicroG = b.useMicroG; } /** True when nothing is overridden, so the patcher can skip the work entirely. */ public boolean isEmpty() { return versionCode == null && label == null && targetSdkVersion == null && extractNativeLibs == null && usesCleartextTraffic == null - && addedPermissions.isEmpty() && !injectDocumentsProvider; + && addedPermissions.isEmpty() && !injectDocumentsProvider && !useMicroG; } /** @@ -121,6 +131,7 @@ public static final class Builder { // order the user added them in is what the report and the re-patch see. private final LinkedHashSet addedPermissions = new LinkedHashSet<>(); private boolean injectDocumentsProvider; + private boolean useMicroG; public Builder versionCode(Integer versionCode) { this.versionCode = versionCode; @@ -167,6 +178,11 @@ public Builder injectDocumentsProvider(boolean injectDocumentsProvider) { return this; } + public Builder useMicroG(boolean useMicroG) { + this.useMicroG = useMicroG; + return this; + } + public ManifestOverrides build() { return new ManifestOverrides(this); } diff --git a/share/java/src/main/java/org/lsposed/lspatch/share/Constants.java b/share/java/src/main/java/org/lsposed/lspatch/share/Constants.java index 65bd4a279..c12c71d2d 100644 --- a/share/java/src/main/java/org/lsposed/lspatch/share/Constants.java +++ b/share/java/src/main/java/org/lsposed/lspatch/share/Constants.java @@ -22,6 +22,13 @@ public class Constants { final static public String MIGRATE_ASSET_PATH = "assets/lspatch/migrate.zip"; final static public int MIN_ROLLING_VERSION_CODE = 348; + final static public String REAL_GMS_PACKAGE_NAME = "com.google.android.gms"; + /** + * ReVanced GmsCore, the MicroG build that reads a calling app's certificate from the app's manifest metadata + * {@code .SPOOFED_PACKAGE_SIGNATURE} rather than relying on a ROM's signature spoofing. + */ + final static public String MICROG_RE_PACKAGE_NAME = "app.revanced.android.gms"; + final static public int SIGBYPASS_LV_DISABLE = 0; final static public int SIGBYPASS_LV_PM = 1; final static public int SIGBYPASS_LV_PM_OPENAT = 2; diff --git a/share/java/src/main/java/org/lsposed/lspatch/share/PatchConfig.java b/share/java/src/main/java/org/lsposed/lspatch/share/PatchConfig.java index 2f2f34726..dcc082d2c 100644 --- a/share/java/src/main/java/org/lsposed/lspatch/share/PatchConfig.java +++ b/share/java/src/main/java/org/lsposed/lspatch/share/PatchConfig.java @@ -33,6 +33,12 @@ public class PatchConfig { */ public final boolean injectDocumentsProvider; + /** + * Whether the app's Google Play services traffic is redirected to an installed MicroG. Read by the loader, and + * recorded for the same reason as {@link #addedPermissions}: a re-patch would otherwise silently drop it. + */ + public final boolean useMicroG; + /** * The manager package a manager-mode app should bind to for module loading, or null for an * apk patched before this was recorded (and for integrated apps, which never bind a manager). @@ -53,6 +59,7 @@ public PatchConfig( boolean injectDex, String[] addedPermissions, boolean injectDocumentsProvider, + boolean useMicroG, String managerPackageName ) { this.useManager = useManager; @@ -64,6 +71,7 @@ public PatchConfig( this.injectDex = injectDex; this.addedPermissions = addedPermissions; this.injectDocumentsProvider = injectDocumentsProvider; + this.useMicroG = useMicroG; if (useManager) { this.managerPackageName = (managerPackageName != null && !managerPackageName.isEmpty()) ? managerPackageName From 73828307e7f60ba720aafdbeb1df509e064196ee Mon Sep 17 00:00:00 2001 From: Martin Date: Fri, 16 Oct 2026 15:40:37 +0800 Subject: [PATCH 2/5] Deliver MicroG pushes, and drop redirect hooks that did nothing useful MicroG looks a patched app's push receiver up by its own RECEIVE action, and the receiver only accepts a sender holding com.google.android.c2dm.permission.SEND, a signature permission of Google Play services that MicroG cannot hold, so every push it delivered was dropped. With MicroG on, every com.google.android.c2dm.* string in the manifest -- intent-filter actions and permissions alike -- is renamed to ReVanced GmsCore's app.revanced.android.c2dm.*, the same rename Morphe applies; the manifest editor never descends into intent filters, so this is a pass of its own. Two hooks carried over from NPatch are removed. The ContentResolver.call retry rewrote its arguments after the call had already returned and then swallowed the SecurityException, so callers got null instead of an error. The getPackageInfo hook reported the app's own certificate as MicroG's, which matches no check a GMS client makes. --- .../lsposed/lspatch/loader/GmsRedirector.java | 97 +++---------------- .../lspatch/loader/LSPApplication.java | 2 +- .../java/org/lsposed/patch/ApkPatcher.java | 51 +++++++++- 3 files changed, 67 insertions(+), 83 deletions(-) diff --git a/patch-loader/src/main/java/org/lsposed/lspatch/loader/GmsRedirector.java b/patch-loader/src/main/java/org/lsposed/lspatch/loader/GmsRedirector.java index d31a5345c..11cc91c88 100644 --- a/patch-loader/src/main/java/org/lsposed/lspatch/loader/GmsRedirector.java +++ b/patch-loader/src/main/java/org/lsposed/lspatch/loader/GmsRedirector.java @@ -4,9 +4,7 @@ import android.content.ContentResolver; import android.content.Context; import android.content.Intent; -import android.content.pm.PackageInfo; import android.content.pm.PackageManager; -import android.content.pm.Signature; import android.net.Uri; import android.util.Log; @@ -45,10 +43,8 @@ public class GmsRedirector { private static String targetGms = null; private static String selfPackage = null; - private static String originalSignature; - public static void activate(Context context, String origSig) { - originalSignature = origSig; + public static void activate(Context context) { selfPackage = context.getPackageName(); targetGms = findInstalledMicroG(context); @@ -68,7 +64,6 @@ public static void activate(Context context, String origSig) { hookIntentResolve(); hookActivityStart(); hookContentResolverAcquire(); - hookPackageManagerGetPackageInfo(context); Log.i(TAG, "GMS redirect hooks installed"); } @@ -430,87 +425,27 @@ protected void beforeHookedMethod(MethodHookParam param) { } catch (Throwable ignored) {} } - // A provider call real GMS refuses for the patched signature is retried against MicroG. - try { - XposedBridge.hookAllMethods(ContentResolver.class, "call", new XC_MethodHook() { - @Override - protected void beforeHookedMethod(MethodHookParam param) { - for (int i = 0; i < param.args.length; i++) { - if (param.args[i] instanceof Uri) { - Uri uri = (Uri) param.args[i]; - String newAuth = redirectAuthority(uri.getAuthority()); - if (newAuth != null) { - param.args[i] = uri.buildUpon().authority(newAuth).build(); - } - } else if (param.args[i] instanceof String && i == 0) { - String newAuth = redirectAuthority((String) param.args[i]); - if (newAuth != null) { - param.args[i] = newAuth; - } + XposedBridge.hookAllMethods(ContentResolver.class, "call", new XC_MethodHook() { + @Override + protected void beforeHookedMethod(MethodHookParam param) { + for (int i = 0; i < param.args.length; i++) { + if (param.args[i] instanceof Uri) { + Uri uri = (Uri) param.args[i]; + String newAuth = redirectAuthority(uri.getAuthority()); + if (newAuth != null) { + param.args[i] = uri.buildUpon().authority(newAuth).build(); } - } - } - - @Override - protected void afterHookedMethod(MethodHookParam param) { - if (param.getThrowable() instanceof SecurityException) { - String msg = param.getThrowable().getMessage(); - if (msg != null && (msg.contains("GoogleCertificatesRslt") || - msg.contains("not allowed") || - msg.contains("Access denied"))) { - Log.i(TAG, "GMS rejected call, retrying with MicroG"); - for (int i = 0; i < param.args.length; i++) { - if (param.args[i] instanceof Uri) { - Uri uri = (Uri) param.args[i]; - String authority = uri.getAuthority(); - if (authority != null && authority.contains(REAL_GMS)) { - param.args[i] = uri.buildUpon() - .authority(authority.replace(REAL_GMS, targetGms)) - .build(); - } - } else if (param.args[i] instanceof String && i == 0) { - String s = (String) param.args[i]; - if (s.contains(REAL_GMS)) { - param.args[i] = s.replace(REAL_GMS, targetGms); - } - } - } - param.setThrowable(null); - param.setResult(null); + } else if (param.args[i] instanceof String && i == 0) { + String newAuth = redirectAuthority((String) param.args[i]); + if (newAuth != null) { + param.args[i] = newAuth; } } } - }); - } catch (Throwable ignored) {} + } + }); } catch (Throwable t) { Log.e(TAG, "Failed to hook ContentResolver", t); } } - - private static void hookPackageManagerGetPackageInfo(Context context) { - try { - XposedHelpers.findAndHookMethod( - context.getPackageManager().getClass(), - "getPackageInfo", - String.class, int.class, - new XC_MethodHook() { - @Override - protected void afterHookedMethod(MethodHookParam param) { - PackageInfo pi = (PackageInfo) param.getResult(); - if (pi != null && targetGms != null) { - if (targetGms.equals(pi.packageName) && (((int) param.args[1]) & PackageManager.GET_SIGNATURES) != 0) { - if (originalSignature != null && !originalSignature.isEmpty()) { - try { - pi.signatures = new Signature[]{new Signature(originalSignature)}; - } catch (Throwable ignored) {} - } - } - } - } - } - ); - } catch (Throwable t) { - Log.e(TAG, "Failed to hook PackageManager.getPackageInfo", t); - } - } } diff --git a/patch-loader/src/main/java/org/lsposed/lspatch/loader/LSPApplication.java b/patch-loader/src/main/java/org/lsposed/lspatch/loader/LSPApplication.java index 06e61ce52..454e7feb9 100644 --- a/patch-loader/src/main/java/org/lsposed/lspatch/loader/LSPApplication.java +++ b/patch-loader/src/main/java/org/lsposed/lspatch/loader/LSPApplication.java @@ -136,7 +136,7 @@ public static void onLoad() throws RemoteException, IOException { // Armed before the class loader for the same reason: the app's GMS client may already run from that initializer. if (config.optBoolean("useMicroG")) { - GmsRedirector.activate(context, config.optString("originalSignature")); + GmsRedirector.activate(context); } // Realize the target's class loader now that hooks, modules and signature bypass are all armed. diff --git a/patch/src/main/java/org/lsposed/patch/ApkPatcher.java b/patch/src/main/java/org/lsposed/patch/ApkPatcher.java index 68ea7a20e..6e201ae86 100644 --- a/patch/src/main/java/org/lsposed/patch/ApkPatcher.java +++ b/patch/src/main/java/org/lsposed/patch/ApkPatcher.java @@ -19,6 +19,11 @@ import com.wind.meditor.property.ModificationProperty; import com.wind.meditor.utils.NodeValue; +import pxb.android.axml.AxmlReader; +import pxb.android.axml.AxmlVisitor; +import pxb.android.axml.AxmlWriter; +import pxb.android.axml.NodeVisitor; + import org.apache.commons.io.FilenameUtils; import org.lsposed.lspatch.share.Constants; import org.lsposed.lspatch.share.LSPConfig; @@ -451,7 +456,7 @@ private byte[] modifyManifestFile(InputStream is, String metadata, String packag new ManifestEditor(in, os, property).processManifest(); } os.flush(); - return os.toByteArray(); + return spec.manifestOverrides().useMicroG ? renameC2dmForMicroG(os.toByteArray()) : os.toByteArray(); } /** @@ -521,6 +526,50 @@ private void addMicroGSupport(ModificationProperty property, String originalSign property.addUsesPermission("android.permission.QUERY_ALL_PACKAGES"); } + private static final String GOOGLE_C2DM_PREFIX = "com.google.android.c2dm."; + private static final String MICROG_RE_C2DM_PREFIX = Constants.MICROG_RE_PACKAGE_NAME.substring( + 0, Constants.MICROG_RE_PACKAGE_NAME.length() - ".gms".length()) + ".c2dm."; + + /** + * Renames every {@code com.google.android.c2dm.*} string in the manifest to ReVanced GmsCore's + * {@code app.revanced.android.c2dm.*}, so pushes MicroG delivers reach the app. + * + * MicroG looks the app's push receiver up by its own RECEIVE action, and the receiver only accepts + * a sender holding {@code com.google.android.c2dm.permission.SEND}, a signature permission of Google + * Play services that MicroG cannot hold; both the intent-filter action and the permissions have to + * be renamed. The manifest editor maps permissions but never descends into intent filters, so this + * is a pass of its own over every attribute. + */ + private static byte[] renameC2dmForMicroG(byte[] manifest) throws IOException { + AxmlWriter writer = new AxmlWriter(); + new AxmlReader(manifest).accept(new AxmlVisitor(writer) { + @Override + public NodeVisitor child(String ns, String name) { + return new C2dmRenamer(super.child(ns, name)); + } + }); + return writer.toByteArray(); + } + + private static final class C2dmRenamer extends NodeVisitor { + C2dmRenamer(NodeVisitor nv) { + super(nv); + } + + @Override + public NodeVisitor child(String ns, String name) { + return new C2dmRenamer(super.child(ns, name)); + } + + @Override + public void attr(String ns, String name, int resourceId, int type, Object obj) { + if (obj instanceof String && ((String) obj).startsWith(GOOGLE_C2DM_PREFIX)) { + obj = MICROG_RE_C2DM_PREFIX + ((String) obj).substring(GOOGLE_C2DM_PREFIX.length()); + } + super.attr(ns, name, resourceId, type, obj); + } + } + /** SHA-1 of a certificate given as hex, as lower-case hex. */ static String sha1Hex(String certificateHex) { byte[] certificate = new byte[certificateHex.length() / 2]; From 100583cea10bf42d44f277671104c73b06f8d4ce Mon Sep 17 00:00:00 2001 From: Martin Date: Fri, 16 Oct 2026 16:18:08 +0800 Subject: [PATCH 3/5] Name the MicroG option after MicroG RE, and offer fake-signature on its own Everything the option does is specific to MicroG RE (ReVanced GmsCore): the SPOOFED_PACKAGE_SIGNATURE metadata, the app.revanced C2DM rename and the redirect to app.revanced.android.gms. Upstream microG installs as Google Play services itself, so it needs no redirect, and renaming its standard C2DM actions would stop its pushes. The option is now "MicroG RE support" (--microg-re), and the redirect no longer falls back to org.microg.gms, which the C2DM rename would leave without pushes. fake-signature with FAKE_PACKAGE_SIGNATURE is the one declaration upstream microG can use, through a ROM with signature spoofing, so it becomes an option of its own (--fake-signature) rather than a side effect of MicroG RE support. Both are recorded in the patch config so a re-patch keeps them. --- .../main/java/org/lsposed/lspatch/Patcher.kt | 3 +- .../lspatch/data/model/PatchRequest.kt | 6 ++- .../lspatch/data/repository/PatchReport.kt | 3 +- .../lsposed/lspatch/ui/page/NewPatchScreen.kt | 29 +++++++++---- .../org/lsposed/lspatch/ui/page/PatchEntry.kt | 3 +- .../lspatch/ui/viewmodel/NewPatchViewModel.kt | 4 +- .../lsposed/lspatch/util/LocalAppsUpdater.kt | 3 +- .../src/main/res/values-zh-rCN/strings.xml | 6 ++- .../src/main/res/values-zh-rTW/strings.xml | 6 ++- manager/src/main/res/values/strings.xml | 6 ++- .../lsposed/lspatch/loader/GmsRedirector.java | 39 +++++++---------- .../lspatch/loader/LSPApplication.java | 2 +- .../java/org/lsposed/patch/ApkPatcher.java | 42 ++++++++++--------- .../main/java/org/lsposed/patch/LSPatch.java | 10 +++-- .../org/lsposed/patch/ManifestOverrides.java | 37 +++++++++++----- .../lsposed/lspatch/share/PatchConfig.java | 13 ++++-- 16 files changed, 130 insertions(+), 82 deletions(-) diff --git a/manager/src/main/java/org/lsposed/lspatch/Patcher.kt b/manager/src/main/java/org/lsposed/lspatch/Patcher.kt index 4f0623114..bb32d062e 100644 --- a/manager/src/main/java/org/lsposed/lspatch/Patcher.kt +++ b/manager/src/main/java/org/lsposed/lspatch/Patcher.kt @@ -47,7 +47,8 @@ object Patcher { .usesCleartextTraffic(if (usesCleartextTraffic) true else null) .permissions(addedPermissions) .injectDocumentsProvider(injectDocumentsProvider) - .useMicroG(useMicroG) + .useMicroGRe(useMicroGRe) + .declareFakeSignature(declareFakeSignature) .build() ) .keystore( diff --git a/manager/src/main/java/org/lsposed/lspatch/data/model/PatchRequest.kt b/manager/src/main/java/org/lsposed/lspatch/data/model/PatchRequest.kt index 88ff6dc64..41bfefda2 100644 --- a/manager/src/main/java/org/lsposed/lspatch/data/model/PatchRequest.kt +++ b/manager/src/main/java/org/lsposed/lspatch/data/model/PatchRequest.kt @@ -91,8 +91,10 @@ data class PatchRequest( // Inject a DocumentsProvider exposing the app's private data. Recorded in the config for the same // reason as the permissions above. val injectDocumentsProvider: Boolean = false, - // Redirect Google Play services to an installed MicroG. Recorded in the config for the same reason. - val useMicroG: Boolean = false, + // Redirect Google Play services to an installed MicroG RE. Recorded in the config for the same reason. + val useMicroGRe: Boolean = false, + // Declare the original signature as fake-signature for ROM signature spoofing. Recorded for the same reason. + val declareFakeSignature: Boolean = false, val origin: PatchOrigin = PatchOrigin.New, ) { val packageName: String get() = target.packageName diff --git a/manager/src/main/java/org/lsposed/lspatch/data/repository/PatchReport.kt b/manager/src/main/java/org/lsposed/lspatch/data/repository/PatchReport.kt index 9807210e8..1a1266035 100644 --- a/manager/src/main/java/org/lsposed/lspatch/data/repository/PatchReport.kt +++ b/manager/src/main/java/org/lsposed/lspatch/data/repository/PatchReport.kt @@ -60,7 +60,8 @@ object PatchReport { "Added perms ${if (request.addedPermissions.isEmpty()) "none" else request.addedPermissions.joinToString(", ")}" ) add("Docs provider ${request.injectDocumentsProvider}") - add("MicroG ${request.useMicroG}") + add("MicroG RE ${request.useMicroGRe}") + add("Fake sig ${request.declareFakeSignature}") add("Keystore ${if (MyKeyStore.useDefault) "built-in" else "custom (${MyKeyStore.file.name})"}") add("Verbose ${Configs.detailPatchLogs}") add("") diff --git a/manager/src/main/java/org/lsposed/lspatch/ui/page/NewPatchScreen.kt b/manager/src/main/java/org/lsposed/lspatch/ui/page/NewPatchScreen.kt index 16f9f98d9..331ff5140 100644 --- a/manager/src/main/java/org/lsposed/lspatch/ui/page/NewPatchScreen.kt +++ b/manager/src/main/java/org/lsposed/lspatch/ui/page/NewPatchScreen.kt @@ -64,6 +64,7 @@ import androidx.compose.material.icons.rounded.Share import androidx.compose.material.icons.rounded.Smartphone import androidx.compose.material.icons.rounded.Terminal import androidx.compose.material.icons.rounded.Tune +import androidx.compose.material.icons.rounded.VerifiedUser import androidx.compose.material.icons.rounded.WorkOutline import androidx.compose.material3.Badge import androidx.compose.material3.BadgedBox @@ -341,7 +342,8 @@ fun NewPatchScreen(navigator: Navigator, token: String) { onAddPermission = viewModel::addPermission, onRemovePermission = viewModel::removePermission, onInjectDocumentsProvider = viewModel::setInjectDocumentsProvider, - onUseMicroG = viewModel::setUseMicroG, + onUseMicroGRe = viewModel::setUseMicroGRe, + onDeclareFakeSignature = viewModel::setDeclareFakeSignature, ) else -> Column( @@ -389,7 +391,8 @@ private fun ConfigureBody( onAddPermission: (String) -> Unit, onRemovePermission: (String) -> Unit, onInjectDocumentsProvider: (Boolean) -> Unit, - onUseMicroG: (Boolean) -> Unit, + onUseMicroGRe: (Boolean) -> Unit, + onDeclareFakeSignature: (Boolean) -> Unit, ) { val scope = rememberCoroutineScope() val snackbarHost = LocalSnackbarHost.current @@ -599,11 +602,18 @@ private fun ConfigureBody( onCheckedChange = onInjectDocumentsProvider, ) ToggleRow( - title = stringResource(R.string.patch_manifest_microg), + title = stringResource(R.string.patch_manifest_microg_re), icon = Icons.Rounded.AccountCircle, - subtitle = stringResource(R.string.patch_manifest_microg_desc), - checked = request.useMicroG, - onCheckedChange = onUseMicroG, + subtitle = stringResource(R.string.patch_manifest_microg_re_desc), + checked = request.useMicroGRe, + onCheckedChange = onUseMicroGRe, + ) + ToggleRow( + title = stringResource(R.string.patch_manifest_fake_signature), + icon = Icons.Rounded.VerifiedUser, + subtitle = stringResource(R.string.patch_manifest_fake_signature_desc), + checked = request.declareFakeSignature, + onCheckedChange = onDeclareFakeSignature, ) // Last of the manifest controls: its chip list and field are the tallest thing here, // so it sits below the compact toggles rather than pushing them down the screen. @@ -789,8 +799,11 @@ private fun advancedChips(request: PatchRequest): List = buildLi if (request.injectDocumentsProvider) { add(OptionChipData(stringResource(R.string.patch_manifest_documents_provider), true, Icons.Rounded.FolderOpen)) } - if (request.useMicroG) { - add(OptionChipData(stringResource(R.string.patch_manifest_microg), true, Icons.Rounded.AccountCircle)) + if (request.useMicroGRe) { + add(OptionChipData(stringResource(R.string.patch_manifest_microg_re), true, Icons.Rounded.AccountCircle)) + } + if (request.declareFakeSignature) { + add(OptionChipData(stringResource(R.string.patch_manifest_fake_signature), true, Icons.Rounded.VerifiedUser)) } if (!MyKeyStore.useDefault) { add(OptionChipData(stringResource(R.string.settings_keystore_custom), true, Icons.Outlined.Ballot)) diff --git a/manager/src/main/java/org/lsposed/lspatch/ui/page/PatchEntry.kt b/manager/src/main/java/org/lsposed/lspatch/ui/page/PatchEntry.kt index d2c7aa868..9536146f1 100644 --- a/manager/src/main/java/org/lsposed/lspatch/ui/page/PatchEntry.kt +++ b/manager/src/main/java/org/lsposed/lspatch/ui/page/PatchEntry.kt @@ -79,7 +79,8 @@ suspend fun rePatchRequestFor( // Recovered from the recorded config, not the original apks -- those never carried them. addedPermissions = config?.addedPermissions?.toList() ?: emptyList(), injectDocumentsProvider = config?.injectDocumentsProvider ?: false, - useMicroG = config?.useMicroG ?: false, + useMicroGRe = config?.useMicroGRe ?: false, + declareFakeSignature = config?.declareFakeSignature ?: false, sigBypassLevel = config?.sigBypassLevel ?: 2, injectDex = config?.injectDex ?: false, modules = modules ?: embedded, diff --git a/manager/src/main/java/org/lsposed/lspatch/ui/viewmodel/NewPatchViewModel.kt b/manager/src/main/java/org/lsposed/lspatch/ui/viewmodel/NewPatchViewModel.kt index f6fcee475..f1958aa53 100644 --- a/manager/src/main/java/org/lsposed/lspatch/ui/viewmodel/NewPatchViewModel.kt +++ b/manager/src/main/java/org/lsposed/lspatch/ui/viewmodel/NewPatchViewModel.kt @@ -109,7 +109,9 @@ class NewPatchViewModel(private val token: String) : ViewModel() { fun setInjectDocumentsProvider(value: Boolean) = mutate { it.copy(injectDocumentsProvider = value) } - fun setUseMicroG(value: Boolean) = mutate { it.copy(useMicroG = value) } + fun setUseMicroGRe(value: Boolean) = mutate { it.copy(useMicroGRe = value) } + + fun setDeclareFakeSignature(value: Boolean) = mutate { it.copy(declareFakeSignature = value) } /** * Adds modules to the set, keeping what is already there. diff --git a/manager/src/main/java/org/lsposed/lspatch/util/LocalAppsUpdater.kt b/manager/src/main/java/org/lsposed/lspatch/util/LocalAppsUpdater.kt index bfb73eb9e..601b47ff0 100644 --- a/manager/src/main/java/org/lsposed/lspatch/util/LocalAppsUpdater.kt +++ b/manager/src/main/java/org/lsposed/lspatch/util/LocalAppsUpdater.kt @@ -127,7 +127,8 @@ object LocalAppsUpdater { .versionCode(config.versionCode) .permissions(config.addedPermissions?.toList() ?: emptyList()) .injectDocumentsProvider(config.injectDocumentsProvider) - .useMicroG(config.useMicroG) + .useMicroGRe(config.useMicroGRe) + .declareFakeSignature(config.declareFakeSignature) .build() ) .keystore( diff --git a/manager/src/main/res/values-zh-rCN/strings.xml b/manager/src/main/res/values-zh-rCN/strings.xml index d97530422..ded121b9b 100644 --- a/manager/src/main/res/values-zh-rCN/strings.xml +++ b/manager/src/main/res/values-zh-rCN/strings.xml @@ -269,8 +269,10 @@ 移除 已添加 %1$d 个 暴露私有数据 - MicroG 支持 - 将 Google Play 服务的调用转向已安装的 MicroG(ReVanced GmsCore 或 microG),并向它声明原始签名,使重新签名后仍能使用 Google 登录。 + MicroG RE 支持 + 将 Google Play 服务的调用与推送转向已安装的 MicroG RE(ReVanced GmsCore),并向它声明原始签名,使重新签名后仍能使用 Google 登录与推送。不适用原版 microG。 + 声明伪装签名 + 以 fake-signature 声明原始签名,让支持签名伪装的 ROM 报告原始签名,供原版 microG 使用。ROM 不支持签名伪装时没有作用。 添加一个提供程序,让文件管理器无需 root 即可通过系统文件选择器浏览应用的私有数据目录。这会扩大可访问应用数据的范围——若无需要请保持关闭。 包名 更改包名 diff --git a/manager/src/main/res/values-zh-rTW/strings.xml b/manager/src/main/res/values-zh-rTW/strings.xml index ebbd5871b..97779a55f 100644 --- a/manager/src/main/res/values-zh-rTW/strings.xml +++ b/manager/src/main/res/values-zh-rTW/strings.xml @@ -165,8 +165,10 @@ 移除 已新增 %1$d 個 公開私有資料 - MicroG 支援 - 將 Google Play 服務的呼叫導向已安裝的 MicroG(ReVanced GmsCore 或 microG),並向它宣告原始簽章,讓重新簽名後仍能使用 Google 登入。 + MicroG RE 支援 + 將 Google Play 服務的呼叫與推播導向已安裝的 MicroG RE(ReVanced GmsCore),並向它宣告原始簽章,讓重新簽名後仍能使用 Google 登入與推播。不適用原版 microG。 + 宣告偽裝簽章 + 以 fake-signature 宣告原始簽章,讓支援簽章偽裝的 ROM 回報原始簽章,供原版 microG 使用。ROM 不支援簽章偽裝時沒有作用。 新增一個提供者,讓檔案管理器無需 root 即可透過系統檔案選擇器瀏覽應用程式的私有資料目錄。這會擴大可存取應用程式資料的範圍——若無需要請保持關閉。 選擇要修補的應用程式 正在載入已安裝的應用程式… diff --git a/manager/src/main/res/values/strings.xml b/manager/src/main/res/values/strings.xml index e88530231..d422cd3fa 100644 --- a/manager/src/main/res/values/strings.xml +++ b/manager/src/main/res/values/strings.xml @@ -119,8 +119,10 @@ Remove %1$d added Expose private data - MicroG support - Send Google Play services calls to an installed MicroG (ReVanced GmsCore or microG) and declare the original signature to it, so Google sign-in keeps working after re-signing. + MicroG RE support + Send Google Play services calls and pushes to an installed MicroG RE (ReVanced GmsCore) and declare the original signature to it, so Google sign-in and push keep working after re-signing. Not for upstream microG. + Declare fake signature + Declare the original signature as fake-signature, for a ROM with signature spoofing to report in place of the patched one — for upstream microG. No effect on a ROM without signature spoofing. Add a provider so a file manager can browse the app\'s private data folder through the system file picker, no root needed. Widens what can reach the app\'s data — leave off unless you need it. Patching… Patch failed diff --git a/patch-loader/src/main/java/org/lsposed/lspatch/loader/GmsRedirector.java b/patch-loader/src/main/java/org/lsposed/lspatch/loader/GmsRedirector.java index 11cc91c88..66892a485 100644 --- a/patch-loader/src/main/java/org/lsposed/lspatch/loader/GmsRedirector.java +++ b/patch-loader/src/main/java/org/lsposed/lspatch/loader/GmsRedirector.java @@ -19,16 +19,16 @@ import de.robv.android.xposed.XposedHelpers; /** - * Points a patched app's Google Play services traffic at an installed MicroG. + * Points a patched app's Google Play services traffic at an installed MicroG RE (ReVanced GmsCore). * * A patched app is signed with LSPatch's key, so Google Play services rejects it wherever it checks the caller's - * certificate -- most visibly at Google sign-in. MicroG answers the same requests under its own package, so this - * rewrites the package, component, provider authority and action of every GMS-bound intent or provider call to the - * MicroG package found on the device. Which certificate MicroG then reports for the app is decided by the manifest - * metadata the patcher wrote, not here. + * certificate -- most visibly at Google sign-in. MicroG RE answers the same requests under its own package, so this + * rewrites the package, component, provider authority and action of every GMS-bound intent or provider call to it. + * Which certificate MicroG RE then reports for the app is decided by the manifest metadata the patcher wrote, not here. * - * Community MicroG builds such as ReVanced GmsCore rename the package to {@code .android.gms} and every action - * along with it, so actions are rewritten too. An intent the app addresses to itself is never rewritten: the Google + * MicroG RE renames the package to {@code app.revanced.android.gms} and every action along with it, so actions are + * rewritten too. Upstream microG is not a target: it installs as Google Play services itself, so nothing needs + * redirecting, and the patcher's C2DM rename is MicroG RE's. An intent the app addresses to itself is never rewritten: the Google * sign-in library opens its own {@code SignInHubActivity} with {@code com.google.android.gms.auth.GOOGLE_SIGN_IN}, and * that activity finishes on any other action. */ @@ -36,22 +36,17 @@ public class GmsRedirector { private static final String TAG = "LSPatch-GmsRedirect"; private static final String REAL_GMS = Constants.REAL_GMS_PACKAGE_NAME; - private static final String[] MICROG_PACKAGES = { - Constants.MICROG_RE_PACKAGE_NAME, // ReVanced GmsCore - "org.microg.gms", // upstream microG - }; - private static String targetGms = null; private static String selfPackage = null; public static void activate(Context context) { selfPackage = context.getPackageName(); - targetGms = findInstalledMicroG(context); - if (targetGms == null) { - Log.w(TAG, "No MicroG/GmsCore found! GMS redirect disabled."); + if (!isInstalled(context, Constants.MICROG_RE_PACKAGE_NAME)) { + Log.w(TAG, "MicroG RE is not installed, GMS redirect disabled"); return; } + targetGms = Constants.MICROG_RE_PACKAGE_NAME; Log.i(TAG, "Activating GMS redirect: " + REAL_GMS + " -> " + targetGms); setupC2dmRedirects(); @@ -68,15 +63,13 @@ public static void activate(Context context) { Log.i(TAG, "GMS redirect hooks installed"); } - private static String findInstalledMicroG(Context context) { - PackageManager pm = context.getPackageManager(); - for (String pkg : MICROG_PACKAGES) { - try { - pm.getPackageInfo(pkg, 0); - return pkg; - } catch (PackageManager.NameNotFoundException ignored) {} + private static boolean isInstalled(Context context, String pkg) { + try { + context.getPackageManager().getPackageInfo(pkg, 0); + return true; + } catch (PackageManager.NameNotFoundException e) { + return false; } - return null; } static void setTargetGmsForTest(String target, String self) { diff --git a/patch-loader/src/main/java/org/lsposed/lspatch/loader/LSPApplication.java b/patch-loader/src/main/java/org/lsposed/lspatch/loader/LSPApplication.java index 454e7feb9..16ca737ad 100644 --- a/patch-loader/src/main/java/org/lsposed/lspatch/loader/LSPApplication.java +++ b/patch-loader/src/main/java/org/lsposed/lspatch/loader/LSPApplication.java @@ -135,7 +135,7 @@ public static void onLoad() throws RemoteException, IOException { SigBypass.doSigBypass(context, config.optInt("sigBypassLevel")); // Armed before the class loader for the same reason: the app's GMS client may already run from that initializer. - if (config.optBoolean("useMicroG")) { + if (config.optBoolean("useMicroGRe")) { GmsRedirector.activate(context); } diff --git a/patch/src/main/java/org/lsposed/patch/ApkPatcher.java b/patch/src/main/java/org/lsposed/patch/ApkPatcher.java index 6e201ae86..5a3ce197a 100644 --- a/patch/src/main/java/org/lsposed/patch/ApkPatcher.java +++ b/patch/src/main/java/org/lsposed/patch/ApkPatcher.java @@ -192,7 +192,8 @@ private void patchOne(File srcApkFile, File outputFile, int index, int total) th spec.injectDex(), spec.manifestOverrides().addedPermissions.toArray(new String[0]), spec.manifestOverrides().injectDocumentsProvider, - spec.manifestOverrides().useMicroG, + spec.manifestOverrides().useMicroGRe, + spec.manifestOverrides().declareFakeSignature, spec.useManager() ? spec.managerPackageName() : null); byte[] configBytes = new Gson().toJson(config).getBytes(StandardCharsets.UTF_8); @@ -246,12 +247,15 @@ private void registerSigner(ZFile dstZFile, int index, int total) throws IOExcep /** * The original app's signature, recorded so the loader can report it back to the app. * - * Only read when signature bypass or MicroG is on: a patched apk is signed with LSPatch's key, and - * an app that checks its own signature -- or MicroG, reporting the app to Google -- would otherwise - * notice. Nothing needs it when both are off. + * Only read when signature bypass, MicroG RE or the fake signature is on: a patched apk is signed + * with LSPatch's key, and an app that checks its own signature -- or MicroG, reporting the app to + * Google -- would otherwise notice. Nothing needs it when all are off. */ private String readOriginalSignature(File srcApkFile) throws IOException { - if (spec.sigBypassLevel() <= Constants.SIGBYPASS_LV_DISABLE && !spec.manifestOverrides().useMicroG) return null; + if (spec.sigBypassLevel() <= Constants.SIGBYPASS_LV_DISABLE + && !spec.manifestOverrides().useMicroGRe && !spec.manifestOverrides().declareFakeSignature) { + return null; + } String signature = ApkSignatureHelper.getApkSignInfo(srcApkFile.getAbsolutePath()); if (signature == null || signature.isEmpty()) { throw new PatchException("Could not read the original apk's signature"); @@ -456,7 +460,7 @@ private byte[] modifyManifestFile(InputStream is, String metadata, String packag new ManifestEditor(in, os, property).processManifest(); } os.flush(); - return spec.manifestOverrides().useMicroG ? renameC2dmForMicroG(os.toByteArray()) : os.toByteArray(); + return spec.manifestOverrides().useMicroGRe ? renameC2dmForMicroGRe(os.toByteArray()) : os.toByteArray(); } /** @@ -500,27 +504,27 @@ private void applyManifestOverrides(ModificationProperty property, String packag if (o.injectDocumentsProvider) { addDocumentsProvider(property, packageName); } - if (o.useMicroG) { - addMicroGSupport(property, originalSignature); + if (o.useMicroGRe) { + addMicroGReSupport(property, originalSignature); + } + if (o.declareFakeSignature) { + logger.i("Add fake signature"); + property.addMetaData(new ModificationProperty.MetaData("fake-signature", originalSignature)); + property.addUsesPermission("android.permission.FAKE_PACKAGE_SIGNATURE"); } } /** - * Declares the app's original certificate to MicroG, which otherwise reports LSPatch's to Google. + * Declares the app's original certificate to MicroG RE, which otherwise reports LSPatch's to Google. * - * MicroG builds read it two ways. Upstream microG relies on a ROM with signature spoofing, which - * reports the {@code fake-signature} metadata to an app holding {@code FAKE_PACKAGE_SIGNATURE}; - * ReVanced GmsCore instead reads {@code app.revanced.android.gms.SPOOFED_PACKAGE_SIGNATURE} from the - * calling app's manifest -- the certificate's SHA-1 in lower-case hex -- and needs no ROM support. - * Both are written so either build works. + * MicroG RE reads {@code app.revanced.android.gms.SPOOFED_PACKAGE_SIGNATURE} from the calling app's + * manifest -- the certificate's SHA-1 in lower-case hex -- and needs no ROM support. * * {@code QUERY_ALL_PACKAGES} lets the loader find the installed MicroG: the app's own * {@code } name Google Play services, not MicroG, and the manifest editor cannot add one. */ - private void addMicroGSupport(ModificationProperty property, String originalSignature) { - logger.i("Add MicroG support"); - property.addMetaData(new ModificationProperty.MetaData("fake-signature", originalSignature)); - property.addUsesPermission("android.permission.FAKE_PACKAGE_SIGNATURE"); + private void addMicroGReSupport(ModificationProperty property, String originalSignature) { + logger.i("Add MicroG RE support"); property.addMetaData(new ModificationProperty.MetaData( Constants.MICROG_RE_PACKAGE_NAME + ".SPOOFED_PACKAGE_SIGNATURE", sha1Hex(originalSignature))); property.addUsesPermission("android.permission.QUERY_ALL_PACKAGES"); @@ -540,7 +544,7 @@ private void addMicroGSupport(ModificationProperty property, String originalSign * be renamed. The manifest editor maps permissions but never descends into intent filters, so this * is a pass of its own over every attribute. */ - private static byte[] renameC2dmForMicroG(byte[] manifest) throws IOException { + private static byte[] renameC2dmForMicroGRe(byte[] manifest) throws IOException { AxmlWriter writer = new AxmlWriter(); new AxmlReader(manifest).accept(new AxmlVisitor(writer) { @Override diff --git a/patch/src/main/java/org/lsposed/patch/LSPatch.java b/patch/src/main/java/org/lsposed/patch/LSPatch.java index 25a055201..a0305558c 100644 --- a/patch/src/main/java/org/lsposed/patch/LSPatch.java +++ b/patch/src/main/java/org/lsposed/patch/LSPatch.java @@ -83,8 +83,11 @@ public class LSPatch { @Parameter(names = {"--documents-provider"}, description = "Inject a DocumentsProvider exposing the app's private data to the system file picker") private boolean injectDocumentsProvider = false; - @Parameter(names = {"--microg"}, description = "Redirect Google Play services calls to an installed MicroG and declare the original signature to it") - private boolean useMicroG = false; + @Parameter(names = {"--microg-re"}, description = "Redirect Google Play services calls and pushes to an installed MicroG RE (ReVanced GmsCore) and declare the original signature to it") + private boolean useMicroGRe = false; + + @Parameter(names = {"--fake-signature"}, description = "Declare the original signature as fake-signature, for a ROM with signature spoofing") + private boolean declareFakeSignature = false; private final JCommander jCommander; @@ -141,7 +144,8 @@ public PatchSpec toSpec() throws PatchException { .map(ManifestOverrides::normalizePermission) .collect(Collectors.toList())) .injectDocumentsProvider(injectDocumentsProvider) - .useMicroG(useMicroG) + .useMicroGRe(useMicroGRe) + .declareFakeSignature(declareFakeSignature) .build(); return PatchSpec.builder() .apks(apkPaths.stream().map(File::new).collect(Collectors.toList())) diff --git a/patch/src/main/java/org/lsposed/patch/ManifestOverrides.java b/patch/src/main/java/org/lsposed/patch/ManifestOverrides.java index bac18fa2b..4492594c8 100644 --- a/patch/src/main/java/org/lsposed/patch/ManifestOverrides.java +++ b/patch/src/main/java/org/lsposed/patch/ManifestOverrides.java @@ -68,13 +68,20 @@ public final class ManifestOverrides { public final boolean injectDocumentsProvider; /** - * Whether to redirect the app's Google Play services traffic to an installed MicroG, so Google sign-in and the - * other GMS APIs keep working for an app re-signed with LSPatch's key. The redirect ships in the loader; this - * declares the original certificate in the two forms MicroG builds read -- {@code fake-signature} with - * {@code FAKE_PACKAGE_SIGNATURE} for a ROM with signature spoofing, and ReVanced GmsCore's - * {@code SPOOFED_PACKAGE_SIGNATURE} -- and lets the app see which MicroG is installed. + * Whether to redirect the app's Google Play services traffic to an installed MicroG RE (ReVanced GmsCore), so + * Google sign-in and push keep working for an app re-signed with LSPatch's key. The redirect ships in the loader; + * this declares the original certificate in the {@code SPOOFED_PACKAGE_SIGNATURE} metadata MicroG RE reads, + * renames the app's C2DM actions and permissions to MicroG RE's, and lets the app see MicroG RE. Specific to + * MicroG RE: upstream microG installs as Google Play services itself and needs none of this. */ - public final boolean useMicroG; + public final boolean useMicroGRe; + + /** + * Whether to declare the original certificate as {@code fake-signature}, with {@code FAKE_PACKAGE_SIGNATURE}, for a + * ROM with signature spoofing to report in place of LSPatch's. Upstream microG has no metadata of its own for this, + * so it is how that setup sees the original certificate. Only takes effect on a ROM that implements the spoofing. + */ + public final boolean declareFakeSignature; private ManifestOverrides(Builder b) { this.versionCode = b.versionCode; @@ -84,14 +91,16 @@ private ManifestOverrides(Builder b) { this.usesCleartextTraffic = b.usesCleartextTraffic; this.addedPermissions = Collections.unmodifiableList(new ArrayList<>(b.addedPermissions)); this.injectDocumentsProvider = b.injectDocumentsProvider; - this.useMicroG = b.useMicroG; + this.useMicroGRe = b.useMicroGRe; + this.declareFakeSignature = b.declareFakeSignature; } /** True when nothing is overridden, so the patcher can skip the work entirely. */ public boolean isEmpty() { return versionCode == null && label == null && targetSdkVersion == null && extractNativeLibs == null && usesCleartextTraffic == null - && addedPermissions.isEmpty() && !injectDocumentsProvider && !useMicroG; + && addedPermissions.isEmpty() && !injectDocumentsProvider && !useMicroGRe + && !declareFakeSignature; } /** @@ -131,7 +140,8 @@ public static final class Builder { // order the user added them in is what the report and the re-patch see. private final LinkedHashSet addedPermissions = new LinkedHashSet<>(); private boolean injectDocumentsProvider; - private boolean useMicroG; + private boolean useMicroGRe; + private boolean declareFakeSignature; public Builder versionCode(Integer versionCode) { this.versionCode = versionCode; @@ -178,8 +188,13 @@ public Builder injectDocumentsProvider(boolean injectDocumentsProvider) { return this; } - public Builder useMicroG(boolean useMicroG) { - this.useMicroG = useMicroG; + public Builder useMicroGRe(boolean useMicroGRe) { + this.useMicroGRe = useMicroGRe; + return this; + } + + public Builder declareFakeSignature(boolean declareFakeSignature) { + this.declareFakeSignature = declareFakeSignature; return this; } diff --git a/share/java/src/main/java/org/lsposed/lspatch/share/PatchConfig.java b/share/java/src/main/java/org/lsposed/lspatch/share/PatchConfig.java index dcc082d2c..8a7dc2f41 100644 --- a/share/java/src/main/java/org/lsposed/lspatch/share/PatchConfig.java +++ b/share/java/src/main/java/org/lsposed/lspatch/share/PatchConfig.java @@ -34,10 +34,13 @@ public class PatchConfig { public final boolean injectDocumentsProvider; /** - * Whether the app's Google Play services traffic is redirected to an installed MicroG. Read by the loader, and + * Whether the app's Google Play services traffic is redirected to an installed MicroG RE. Read by the loader, and * recorded for the same reason as {@link #addedPermissions}: a re-patch would otherwise silently drop it. */ - public final boolean useMicroG; + public final boolean useMicroGRe; + + /** Whether {@code fake-signature} was declared. Recorded for the same reason as {@link #addedPermissions}. */ + public final boolean declareFakeSignature; /** * The manager package a manager-mode app should bind to for module loading, or null for an @@ -59,7 +62,8 @@ public PatchConfig( boolean injectDex, String[] addedPermissions, boolean injectDocumentsProvider, - boolean useMicroG, + boolean useMicroGRe, + boolean declareFakeSignature, String managerPackageName ) { this.useManager = useManager; @@ -71,7 +75,8 @@ public PatchConfig( this.injectDex = injectDex; this.addedPermissions = addedPermissions; this.injectDocumentsProvider = injectDocumentsProvider; - this.useMicroG = useMicroG; + this.useMicroGRe = useMicroGRe; + this.declareFakeSignature = declareFakeSignature; if (useManager) { this.managerPackageName = (managerPackageName != null && !managerPackageName.isEmpty()) ? managerPackageName From 13fb7152d5e136e3a175bce38c60784b902e1f99 Mon Sep 17 00:00:00 2001 From: Martin Date: Fri, 16 Oct 2026 16:46:39 +0800 Subject: [PATCH 4/5] Leave QUERY_ALL_PACKAGES to the user rather than adding it for MicroG RE The loader has to see MicroG RE to start the redirect, and the app's own queries do not name it. Granting every package's visibility for that is broader than the option needs: a manager-mode app already holds QUERY_ALL_PACKAGES, and an integrated one can add it through the added permissions. The option's description and the loader's warning now say so. --- manager/src/main/res/values-zh-rCN/strings.xml | 2 +- manager/src/main/res/values-zh-rTW/strings.xml | 2 +- manager/src/main/res/values/strings.xml | 2 +- .../java/org/lsposed/lspatch/loader/GmsRedirector.java | 10 ++++++---- patch/src/main/java/org/lsposed/patch/ApkPatcher.java | 4 ---- .../main/java/org/lsposed/patch/ManifestOverrides.java | 7 +++++-- 6 files changed, 14 insertions(+), 13 deletions(-) diff --git a/manager/src/main/res/values-zh-rCN/strings.xml b/manager/src/main/res/values-zh-rCN/strings.xml index ded121b9b..632328b4f 100644 --- a/manager/src/main/res/values-zh-rCN/strings.xml +++ b/manager/src/main/res/values-zh-rCN/strings.xml @@ -270,7 +270,7 @@ 已添加 %1$d 个 暴露私有数据 MicroG RE 支持 - 将 Google Play 服务的调用与推送转向已安装的 MicroG RE(ReVanced GmsCore),并向它声明原始签名,使重新签名后仍能使用 Google 登录与推送。不适用原版 microG。 + 将 Google Play 服务的调用与推送转向已安装的 MicroG RE(ReVanced GmsCore),并向它声明原始签名,使重新签名后仍能使用 Google 登录与推送。集成模式需另外加入 QUERY_ALL_PACKAGES 权限。不适用原版 microG。 声明伪装签名 以 fake-signature 声明原始签名,让支持签名伪装的 ROM 报告原始签名,供原版 microG 使用。ROM 不支持签名伪装时没有作用。 添加一个提供程序,让文件管理器无需 root 即可通过系统文件选择器浏览应用的私有数据目录。这会扩大可访问应用数据的范围——若无需要请保持关闭。 diff --git a/manager/src/main/res/values-zh-rTW/strings.xml b/manager/src/main/res/values-zh-rTW/strings.xml index 97779a55f..c184e3985 100644 --- a/manager/src/main/res/values-zh-rTW/strings.xml +++ b/manager/src/main/res/values-zh-rTW/strings.xml @@ -166,7 +166,7 @@ 已新增 %1$d 個 公開私有資料 MicroG RE 支援 - 將 Google Play 服務的呼叫與推播導向已安裝的 MicroG RE(ReVanced GmsCore),並向它宣告原始簽章,讓重新簽名後仍能使用 Google 登入與推播。不適用原版 microG。 + 將 Google Play 服務的呼叫與推播導向已安裝的 MicroG RE(ReVanced GmsCore),並向它宣告原始簽章,讓重新簽名後仍能使用 Google 登入與推播。整合模式需另外加入 QUERY_ALL_PACKAGES 權限。不適用原版 microG。 宣告偽裝簽章 以 fake-signature 宣告原始簽章,讓支援簽章偽裝的 ROM 回報原始簽章,供原版 microG 使用。ROM 不支援簽章偽裝時沒有作用。 新增一個提供者,讓檔案管理器無需 root 即可透過系統檔案選擇器瀏覽應用程式的私有資料目錄。這會擴大可存取應用程式資料的範圍——若無需要請保持關閉。 diff --git a/manager/src/main/res/values/strings.xml b/manager/src/main/res/values/strings.xml index d422cd3fa..1dd613215 100644 --- a/manager/src/main/res/values/strings.xml +++ b/manager/src/main/res/values/strings.xml @@ -120,7 +120,7 @@ %1$d added Expose private data MicroG RE support - Send Google Play services calls and pushes to an installed MicroG RE (ReVanced GmsCore) and declare the original signature to it, so Google sign-in and push keep working after re-signing. Not for upstream microG. + Send Google Play services calls and pushes to an installed MicroG RE (ReVanced GmsCore) and declare the original signature to it, so Google sign-in and push keep working after re-signing. In Integrated mode, also add the QUERY_ALL_PACKAGES permission. Not for upstream microG. Declare fake signature Declare the original signature as fake-signature, for a ROM with signature spoofing to report in place of the patched one — for upstream microG. No effect on a ROM without signature spoofing. Add a provider so a file manager can browse the app\'s private data folder through the system file picker, no root needed. Widens what can reach the app\'s data — leave off unless you need it. diff --git a/patch-loader/src/main/java/org/lsposed/lspatch/loader/GmsRedirector.java b/patch-loader/src/main/java/org/lsposed/lspatch/loader/GmsRedirector.java index 66892a485..6eb46bedb 100644 --- a/patch-loader/src/main/java/org/lsposed/lspatch/loader/GmsRedirector.java +++ b/patch-loader/src/main/java/org/lsposed/lspatch/loader/GmsRedirector.java @@ -28,9 +28,11 @@ * * MicroG RE renames the package to {@code app.revanced.android.gms} and every action along with it, so actions are * rewritten too. Upstream microG is not a target: it installs as Google Play services itself, so nothing needs - * redirecting, and the patcher's C2DM rename is MicroG RE's. An intent the app addresses to itself is never rewritten: the Google - * sign-in library opens its own {@code SignInHubActivity} with {@code com.google.android.gms.auth.GOOGLE_SIGN_IN}, and - * that activity finishes on any other action. + * redirecting, and the patcher's C2DM rename is MicroG RE's. + * + * An intent the app addresses to itself is never rewritten: the Google sign-in library opens its own + * {@code SignInHubActivity} with {@code com.google.android.gms.auth.GOOGLE_SIGN_IN}, and that activity finishes on any + * other action. */ public class GmsRedirector { private static final String TAG = "LSPatch-GmsRedirect"; @@ -43,7 +45,7 @@ public static void activate(Context context) { selfPackage = context.getPackageName(); if (!isInstalled(context, Constants.MICROG_RE_PACKAGE_NAME)) { - Log.w(TAG, "MicroG RE is not installed, GMS redirect disabled"); + Log.w(TAG, "MicroG RE is not installed or not visible to the app (add QUERY_ALL_PACKAGES), GMS redirect disabled"); return; } targetGms = Constants.MICROG_RE_PACKAGE_NAME; diff --git a/patch/src/main/java/org/lsposed/patch/ApkPatcher.java b/patch/src/main/java/org/lsposed/patch/ApkPatcher.java index 5a3ce197a..36cd39ec5 100644 --- a/patch/src/main/java/org/lsposed/patch/ApkPatcher.java +++ b/patch/src/main/java/org/lsposed/patch/ApkPatcher.java @@ -519,15 +519,11 @@ private void applyManifestOverrides(ModificationProperty property, String packag * * MicroG RE reads {@code app.revanced.android.gms.SPOOFED_PACKAGE_SIGNATURE} from the calling app's * manifest -- the certificate's SHA-1 in lower-case hex -- and needs no ROM support. - * - * {@code QUERY_ALL_PACKAGES} lets the loader find the installed MicroG: the app's own - * {@code } name Google Play services, not MicroG, and the manifest editor cannot add one. */ private void addMicroGReSupport(ModificationProperty property, String originalSignature) { logger.i("Add MicroG RE support"); property.addMetaData(new ModificationProperty.MetaData( Constants.MICROG_RE_PACKAGE_NAME + ".SPOOFED_PACKAGE_SIGNATURE", sha1Hex(originalSignature))); - property.addUsesPermission("android.permission.QUERY_ALL_PACKAGES"); } private static final String GOOGLE_C2DM_PREFIX = "com.google.android.c2dm."; diff --git a/patch/src/main/java/org/lsposed/patch/ManifestOverrides.java b/patch/src/main/java/org/lsposed/patch/ManifestOverrides.java index 4492594c8..5ae5aa333 100644 --- a/patch/src/main/java/org/lsposed/patch/ManifestOverrides.java +++ b/patch/src/main/java/org/lsposed/patch/ManifestOverrides.java @@ -71,8 +71,11 @@ public final class ManifestOverrides { * Whether to redirect the app's Google Play services traffic to an installed MicroG RE (ReVanced GmsCore), so * Google sign-in and push keep working for an app re-signed with LSPatch's key. The redirect ships in the loader; * this declares the original certificate in the {@code SPOOFED_PACKAGE_SIGNATURE} metadata MicroG RE reads, - * renames the app's C2DM actions and permissions to MicroG RE's, and lets the app see MicroG RE. Specific to - * MicroG RE: upstream microG installs as Google Play services itself and needs none of this. + * and renames the app's C2DM actions and permissions to MicroG RE's. Specific to MicroG RE: upstream microG + * installs as Google Play services itself and needs none of this. + * + * The app must also be able to see MicroG RE, which its own {@code } do not cover: a local-mode + * app already holds {@code QUERY_ALL_PACKAGES}, and an integrated one needs it added as a permission. */ public final boolean useMicroGRe; From 8bf7e3e4cc59a9cd37a6a68c36a17a9e6c3ddbee Mon Sep 17 00:00:00 2001 From: Martin <86138471+kuomartin@users.noreply.github.com> Date: Sat, 17 Oct 2026 01:10:03 +0800 Subject: [PATCH 5/5] Show MicroG RE and fake-signature options in app details --- .../java/org/lsposed/lspatch/ui/page/AppDetailScreen.kt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/manager/src/main/java/org/lsposed/lspatch/ui/page/AppDetailScreen.kt b/manager/src/main/java/org/lsposed/lspatch/ui/page/AppDetailScreen.kt index 5148a09f3..1561bda06 100644 --- a/manager/src/main/java/org/lsposed/lspatch/ui/page/AppDetailScreen.kt +++ b/manager/src/main/java/org/lsposed/lspatch/ui/page/AppDetailScreen.kt @@ -460,6 +460,14 @@ fun AppDetailScreen(navigator: Navigator, packageName: String) { stringResource(R.string.appdetail_info_inject_dex), yesNo(config?.injectDex == true), ) + InfoRow( + stringResource(R.string.patch_manifest_microg_re), + yesNo(config?.useMicroGRe == true), + ) + InfoRow( + stringResource(R.string.patch_manifest_fake_signature), + yesNo(config?.declareFakeSignature == true), + ) // Manifest attributes, read from the installed manifest rather than the patch // config: they are baked into the manifest at patch time and are not carried in the // runtime config, so this reflects the app's actual manifest. Grouped and ordered to