From 28d26bff9b0f56b0852d3cc014b228aef5d2d7dc Mon Sep 17 00:00:00 2001 From: Mike Jones Date: Tue, 6 Jan 2026 01:41:34 -0700 Subject: [PATCH] Added patches for Continuum to change the client id, redirect uri, and user agent to pretend to be RedReader --- patches/api/patches.api | 21 +++ .../customclients/continuum/api/Constants.kt | 9 ++ .../continuum/api/RedditApiBytecodePatch.kt | 54 +++++++ .../continuum/api/RedditApiResourcePatch.kt | 20 +++ .../misc/RemoveClientIdCheckPatch.kt | 132 ++++++++++++++++++ 5 files changed, 236 insertions(+) create mode 100644 patches/src/main/kotlin/app/revanced/patches/reddit/customclients/continuum/api/Constants.kt create mode 100644 patches/src/main/kotlin/app/revanced/patches/reddit/customclients/continuum/api/RedditApiBytecodePatch.kt create mode 100644 patches/src/main/kotlin/app/revanced/patches/reddit/customclients/continuum/api/RedditApiResourcePatch.kt create mode 100644 patches/src/main/kotlin/app/revanced/patches/reddit/customclients/continuum/misc/RemoveClientIdCheckPatch.kt diff --git a/patches/api/patches.api b/patches/api/patches.api index fdb8c1cf25..f3981da8e5 100644 --- a/patches/api/patches.api +++ b/patches/api/patches.api @@ -717,6 +717,27 @@ public final class app/revanced/patches/reddit/customclients/boostforreddit/misc public static final fun getSharedExtensionPatch ()Lapp/revanced/patcher/patch/BytecodePatch; } +public final class app/revanced/patches/reddit/customclients/continuum/api/Constants { + public static final field INSTANCE Lapp/revanced/patches/reddit/customclients/continuum/api/Constants; + public static final field NEW_CLIENT_ID Ljava/lang/String; + public static final field NEW_REDIRECT_URI Ljava/lang/String; + public static final field NEW_USER_AGENT Ljava/lang/String; + public static final field OLD_CLIENT_ID Ljava/lang/String; + public static final field OLD_REDIRECT_URI Ljava/lang/String; +} + +public final class app/revanced/patches/reddit/customclients/continuum/api/RedditApiBytecodePatchKt { + public static final fun getRedditApiBytecodePatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/reddit/customclients/continuum/api/RedditApiResourcePatchKt { + public static final fun getRedditApiResourcePatch ()Lapp/revanced/patcher/patch/ResourcePatch; +} + +public final class app/revanced/patches/reddit/customclients/continuum/misc/RemoveClientIdCheckPatchKt { + public static final fun getRemoveClientIdCheckPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + public final class app/revanced/patches/reddit/customclients/infinityforreddit/api/SpoofClientPatchKt { public static final fun getSpoofClientPatch ()Lapp/revanced/patcher/patch/BytecodePatch; } diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/continuum/api/Constants.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/continuum/api/Constants.kt new file mode 100644 index 0000000000..4e29ed9eb9 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/continuum/api/Constants.kt @@ -0,0 +1,9 @@ +package app.revanced.patches.reddit.customclients.continuum.api + +object Constants { + const val NEW_USER_AGENT = "org.quantumbadger.redreader/1.25.1" + const val OLD_REDIRECT_URI = "continuum://localhost" + const val NEW_REDIRECT_URI = "redreader://rr_oauth_redir" + const val OLD_CLIENT_ID = "Ro2J-y8bN412oS6BeaIj0A" + const val NEW_CLIENT_ID = "QnM1dlkC_2UfSlACOTXGRw" +} diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/continuum/api/RedditApiBytecodePatch.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/continuum/api/RedditApiBytecodePatch.kt new file mode 100644 index 0000000000..5924df0ecf --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/continuum/api/RedditApiBytecodePatch.kt @@ -0,0 +1,54 @@ +package app.revanced.patches.reddit.customclients.continuum.api + +import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.all.misc.transformation.transformInstructionsPatch +import app.revanced.patches.reddit.customclients.continuum.misc.removeClientIdCheckPatch +import app.revanced.patches.shared.misc.string.replaceStringPatch +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.StringReference + +@Suppress("unused") +val redditApiBytecodePatch = bytecodePatch( + name = "Reddit API override", + description = "Overrides Reddit User-Agent, Redirect URI, and Client ID", +) { + compatibleWith("org.cygnusx1.continuum", "org.cygnusx1.continuum.debug") + + dependsOn( + redditApiResourcePatch, + // Use regex-based replacement for user agent to work across all versions + transformInstructionsPatch( + filterMap = filterMap@{ _, _, instruction, instructionIndex -> + if (instruction.opcode != Opcode.CONST_STRING && instruction.opcode != Opcode.CONST_STRING_JUMBO) { + return@filterMap null + } + + val stringReference = (instruction as? com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction) + ?.reference as? StringReference ?: return@filterMap null + + val pattern = "android:org\\.cygnusx1\\.continuum:\\d+\\.\\d+\\.\\d+\\.\\d+ \\(by /u/edgan\\)".toRegex() + if (!pattern.matches(stringReference.string)) return@filterMap null + + Triple(instructionIndex, instruction as OneRegisterInstruction, stringReference.string) + }, + transform = { mutableMethod, entry -> + val (instructionIndex, instruction, _) = entry + mutableMethod.replaceInstruction( + instructionIndex, + "${instruction.opcode.name} v${instruction.registerA}, \"${Constants.NEW_USER_AGENT}\"", + ) + }, + ), + replaceStringPatch(Constants.OLD_REDIRECT_URI, Constants.NEW_REDIRECT_URI), + removeClientIdCheckPatch + ) + + execute { + // The actual replacements are handled by the dependencies: + // - redditApiResourcePatch: modifies default_client_id in res/values/strings.xml + // - transformInstructionsPatch: replaces user agent with regex pattern matching + // - replaceStringPatch: replaces hardcoded redirect URI strings + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/continuum/api/RedditApiResourcePatch.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/continuum/api/RedditApiResourcePatch.kt new file mode 100644 index 0000000000..7f30a30da8 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/continuum/api/RedditApiResourcePatch.kt @@ -0,0 +1,20 @@ +package app.revanced.patches.reddit.customclients.continuum.api + +import app.revanced.patcher.patch.resourcePatch +import app.revanced.util.findElementByAttributeValueOrThrow + +val redditApiResourcePatch = resourcePatch( + name = "Reddit API override (resource)", + description = "Overrides Reddit client ID in strings.xml", +) { + compatibleWith("org.cygnusx1.continuum", "org.cygnusx1.continuum.debug") + + execute { + document("res/values/strings.xml").use { document -> + document.documentElement.childNodes.findElementByAttributeValueOrThrow( + "name", + "default_client_id" + ).textContent = Constants.NEW_CLIENT_ID + } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/continuum/misc/RemoveClientIdCheckPatch.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/continuum/misc/RemoveClientIdCheckPatch.kt new file mode 100644 index 0000000000..825589b4a2 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/continuum/misc/RemoveClientIdCheckPatch.kt @@ -0,0 +1,132 @@ +package app.revanced.patches.reddit.customclients.continuum.misc + +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.extensions.InstructionExtensions.removeInstructions +import app.revanced.patcher.fingerprint +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstructionOrThrow +import app.revanced.util.indexOfFirstInstructionReversedOrThrow +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.reference.MethodReference +import com.android.tools.smali.dexlib2.iface.reference.StringReference +import com.android.tools.smali.dexlib2.iface.reference.TypeReference + +internal val clientIdCheckFingerprint = fingerprint { + strings("client_id_pref_key") +} + +@Suppress("unused") +val removeClientIdCheckPatch = bytecodePatch( + name = "Remove client ID check", + description = "Removes the dialog that prevents login with default client ID", +) { + compatibleWith("org.cygnusx1.continuum", "org.cygnusx1.continuum.debug") + + execute { + // Find all classes and methods containing "client_id_pref_key" + var targetMethod: com.android.tools.smali.dexlib2.iface.Method? = null + var targetClassDef: com.android.tools.smali.dexlib2.iface.ClassDef? = null + + var methodsWithClientIdPrefKey = 0 + var methodsWithDialog = 0 + + classes.forEach { classDef -> + classDef.methods.forEach { method -> + // Check if method contains "client_id_pref_key" string + val hasClientIdPrefKey = method.implementation?.instructions?.any { instruction -> + instruction.getReference()?.string == "client_id_pref_key" + } == true + + if (hasClientIdPrefKey) { + methodsWithClientIdPrefKey++ + + // Check if this method also has MaterialAlertDialogBuilder + val hasDialog = method.implementation?.instructions?.any { + it.opcode == Opcode.NEW_INSTANCE && + it.getReference()?.toString()?.contains("MaterialAlertDialogBuilder") == true + } == true + + if (hasDialog) { + methodsWithDialog++ + targetMethod = method + targetClassDef = classDef + return@forEach + } + } + } + if (targetMethod != null) return@forEach + } + + if (targetMethod == null || targetClassDef == null) { + throw Exception("Could not find MainActivity method with client ID check") + } + + // Now patch the method + val mutableMethod = proxy(targetClassDef).mutableClass + .methods.first { it.name == targetMethod!!.name && it.parameterTypes == targetMethod!!.parameterTypes } + + + mutableMethod.apply { + // First find where "client_id_pref_key" is loaded + val clientIdPrefKeyIndex = indexOfFirstInstructionOrThrow { + opcode == Opcode.CONST_STRING && + getReference()?.string == "client_id_pref_key" + } + + // Find the String.equals call that comes AFTER the client_id_pref_key reference + val equalsIndex = indexOfFirstInstructionOrThrow(clientIdPrefKeyIndex) { + opcode == Opcode.INVOKE_VIRTUAL && + getReference()?.toString()?.contains("Ljava/lang/String;->equals") == true + } + + // The next instruction is move-result + val moveResultIndex = equalsIndex + 1 + + // After that is if-eqz that branches on the equals result + val ifEqzIndex = moveResultIndex + 1 + + // Verify that this if-eqz is followed by MaterialAlertDialogBuilder + // to ensure we're removing the right code + val dialogCheck = indexOfFirstInstructionOrThrow(ifEqzIndex) { + opcode == Opcode.NEW_INSTANCE && + getReference()?.toString()?.contains("MaterialAlertDialogBuilder") == true + } + + // Find the iget-object that loads this$0 - work backwards from equals + // Try to find it by looking for iget-object that references MainActivity + var igetObjectIndex = -1 + try { + igetObjectIndex = indexOfFirstInstructionReversedOrThrow(equalsIndex - 1) { + opcode == Opcode.IGET_OBJECT && + (toString().contains("this\$0") || toString().contains("MainActivity")) + } + } catch (e: Exception) { + // If we can't find MainActivity reference, just find the first iget-object + igetObjectIndex = indexOfFirstInstructionReversedOrThrow(equalsIndex - 1) { + opcode == Opcode.IGET_OBJECT + } + } + + // Find where the normal flow resumes (new Intent creation for LoginActivity) + // We need to find the Intent that comes AFTER the dialog code + // Look for the MaterialAlertDialogBuilder first, then find Intent after it + val dialogBuilderIndex = indexOfFirstInstructionOrThrow(ifEqzIndex) { + opcode == Opcode.NEW_INSTANCE && + getReference()?.toString()?.contains("MaterialAlertDialogBuilder") == true + } + + val intentIndex = indexOfFirstInstructionOrThrow(dialogBuilderIndex) { + opcode == Opcode.NEW_INSTANCE && + getReference()?.toString()?.contains("Landroid/content/Intent;") == true + } + + // Double-check: look at a few instructions after to see if it's LoginActivity + val nextInstruction = getInstruction(intentIndex + 1) + + // So remove from igetObjectIndex to intentIndex (exclusive) + val count = intentIndex - igetObjectIndex + removeInstructions(igetObjectIndex, count) + } + } +}