From e19275fb7d60a0623f82127aeb4e5a242723c54c Mon Sep 17 00:00:00 2001 From: rospino74 <34315725+rospino74@users.noreply.github.com> Date: Wed, 18 Mar 2026 16:40:22 +0100 Subject: [PATCH] feat!: Add `Spoof Play Age Signals` patch (#6692) Co-authored-by: ADudeCalledLeo <7997354+Leo40Git@users.noreply.github.com> Co-authored-by: Dawid Krajcarz <80264606+drobotk@users.noreply.github.com> Co-authored-by: oSumAtrIX --- .../DisablePlayIntegrityPatch.java | 2 +- patches/api/patches.api | 6 +- .../DisablePlayIntegrity.kt | 4 +- .../all/misc/play/SpoofPlayAgeSignals.kt | 138 ++++++++++++++++++ 4 files changed, 146 insertions(+), 4 deletions(-) rename extensions/all/misc/disable-play-integrity/src/main/java/app/revanced/extension/{playintegrity => play}/DisablePlayIntegrityPatch.java (92%) rename patches/src/main/kotlin/app/revanced/patches/all/misc/{playintegrity => play}/DisablePlayIntegrity.kt (95%) create mode 100644 patches/src/main/kotlin/app/revanced/patches/all/misc/play/SpoofPlayAgeSignals.kt diff --git a/extensions/all/misc/disable-play-integrity/src/main/java/app/revanced/extension/playintegrity/DisablePlayIntegrityPatch.java b/extensions/all/misc/disable-play-integrity/src/main/java/app/revanced/extension/play/DisablePlayIntegrityPatch.java similarity index 92% rename from extensions/all/misc/disable-play-integrity/src/main/java/app/revanced/extension/playintegrity/DisablePlayIntegrityPatch.java rename to extensions/all/misc/disable-play-integrity/src/main/java/app/revanced/extension/play/DisablePlayIntegrityPatch.java index a27e56be95..4dd09f693f 100644 --- a/extensions/all/misc/disable-play-integrity/src/main/java/app/revanced/extension/playintegrity/DisablePlayIntegrityPatch.java +++ b/extensions/all/misc/disable-play-integrity/src/main/java/app/revanced/extension/play/DisablePlayIntegrityPatch.java @@ -1,4 +1,4 @@ -package app.revanced.extension.playintegrity; +package app.revanced.extension.play; import android.content.Context; import android.content.Intent; diff --git a/patches/api/patches.api b/patches/api/patches.api index 0bc30b14f9..49548475b6 100644 --- a/patches/api/patches.api +++ b/patches/api/patches.api @@ -89,10 +89,14 @@ public final class app/revanced/patches/all/misc/packagename/ChangePackageNamePa public static final fun setOrGetFallbackPackageName (Ljava/lang/String;)Ljava/lang/String; } -public final class app/revanced/patches/all/misc/playintegrity/DisablePlayIntegrityKt { +public final class app/revanced/patches/all/misc/play/DisablePlayIntegrityKt { public static final fun getDisablePlayIntegrityPatch ()Lapp/revanced/patcher/patch/Patch; } +public final class app/revanced/patches/all/misc/play/SpoofPlayAgeSignalsKt { + public static final fun getSpoofPlayAgeSignalsPatch ()Lapp/revanced/patcher/patch/Patch; +} + public final class app/revanced/patches/all/misc/resources/AddResourcesPatchKt { public static final fun addResource (Ljava/lang/String;Lapp/revanced/util/resource/BaseResource;)Z public static final fun addResources (Lapp/revanced/patcher/patch/Patch;Lkotlin/jvm/functions/Function1;)Z diff --git a/patches/src/main/kotlin/app/revanced/patches/all/misc/playintegrity/DisablePlayIntegrity.kt b/patches/src/main/kotlin/app/revanced/patches/all/misc/play/DisablePlayIntegrity.kt similarity index 95% rename from patches/src/main/kotlin/app/revanced/patches/all/misc/playintegrity/DisablePlayIntegrity.kt rename to patches/src/main/kotlin/app/revanced/patches/all/misc/play/DisablePlayIntegrity.kt index 12461fc40a..dd5dad79e4 100644 --- a/patches/src/main/kotlin/app/revanced/patches/all/misc/playintegrity/DisablePlayIntegrity.kt +++ b/patches/src/main/kotlin/app/revanced/patches/all/misc/play/DisablePlayIntegrity.kt @@ -1,4 +1,4 @@ -package app.revanced.patches.all.misc.playintegrity +package app.revanced.patches.all.misc.play import app.revanced.patcher.extensions.replaceInstruction import app.revanced.patcher.patch.bytecodePatch @@ -9,7 +9,7 @@ import com.android.tools.smali.dexlib2.iface.reference.MethodReference import com.android.tools.smali.dexlib2.immutable.reference.ImmutableMethodReference import com.android.tools.smali.dexlib2.util.MethodUtil -private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/playintegrity/DisablePlayIntegrityPatch;" +private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/play/DisablePlayIntegrityPatch;" private val CONTEXT_BIND_SERVICE_METHOD_REFERENCE = ImmutableMethodReference( "Landroid/content/Context;", diff --git a/patches/src/main/kotlin/app/revanced/patches/all/misc/play/SpoofPlayAgeSignals.kt b/patches/src/main/kotlin/app/revanced/patches/all/misc/play/SpoofPlayAgeSignals.kt new file mode 100644 index 0000000000..ccb41b81b4 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/all/misc/play/SpoofPlayAgeSignals.kt @@ -0,0 +1,138 @@ +package app.revanced.patches.all.misc.play + +import app.revanced.patcher.extensions.addInstructions +import app.revanced.patcher.extensions.getInstruction +import app.revanced.patcher.extensions.removeInstructions +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.patch.intOption +import app.revanced.patcher.patch.option +import app.revanced.util.forEachInstructionAsSequence +import app.revanced.util.getReference +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction +import com.android.tools.smali.dexlib2.iface.reference.MethodReference +import com.android.tools.smali.dexlib2.immutable.reference.ImmutableMethodReference + +@Suppress("unused") +val spoofPlayAgeSignalsPatch = bytecodePatch( + name = "Spoof Play Age Signals", + description = "Spoofs Google Play data about the user's age and verification status.", + use = false, +) { + val lowerAgeBound by intOption( + name = "Lower age bound", + description = "A positive integer.", + default = 18, + validator = { it == null || it > 0 }, + ) + + val upperAgeBound by intOption( + name = "Upper age bound", + description = "A positive integer. Must be greater than the lower age bound.", + default = Int.MAX_VALUE, + validator = { it == null || it > lowerAgeBound!! }, + ) + + val userStatus by intOption( + name = "User status", + description = "An integer representing the user status.", + default = UserStatus.VERIFIED.value, + values = UserStatus.entries.associate { it.name to it.value }, + ) + + apply { + forEachInstructionAsSequence(match = { classDef, _, instruction, instructionIndex -> + // Avoid patching the library itself. + if (classDef.type.startsWith("Lcom/google/android/play/agesignals/")) return@forEachInstructionAsSequence null + + // Keep method calls only. + val reference = instruction.getReference() + ?: return@forEachInstructionAsSequence null + + val match = MethodCall.entries.firstOrNull { + reference == it.reference + } ?: return@forEachInstructionAsSequence null + + val replacement = when (match) { + MethodCall.AgeLower -> lowerAgeBound!! + MethodCall.AgeUpper -> upperAgeBound!! + MethodCall.UserStatus -> userStatus!! + } + + replacement.let { instructionIndex to it } + }, transform = { method, entry -> + val (instructionIndex, replacement) = entry + + // Get the register which would have contained the return value. + val register = method.getInstruction(instructionIndex + 1).registerA + + // Replace the call instructions with the spoofed value. + method.removeInstructions(instructionIndex, 2) + method.addInstructions( + instructionIndex, + """ + const v$register, $replacement + invoke-static { v$register }, Ljava/lang/Integer;->valueOf(I)Ljava/lang/Integer; + move-result-object v$register + """.trimIndent(), + ) + }) + } +} + +/** + * See [AgeSignalsResult](https://developer.android.com/google/play/age-signals/reference/com/google/android/play/agesignals/AgeSignalsResult). + */ +private enum class MethodCall( + val reference: MethodReference, +) { + AgeLower( + ImmutableMethodReference( + "Lcom/google/android/play/agesignals/AgeSignalsResult;", + "ageLower", + emptyList(), + "Ljava/lang/Integer;", + ), + ), + AgeUpper( + ImmutableMethodReference( + "Lcom/google/android/play/agesignals/AgeSignalsResult;", + "ageUpper", + emptyList(), + "Ljava/lang/Integer;", + ), + ), + UserStatus( + ImmutableMethodReference( + "Lcom/google/android/play/agesignals/AgeSignalsResult;", + "userStatus", + emptyList(), + "Ljava/lang/Integer;", + ), + ), +} + +/** + * All possible user verification statuses. + * + * See [AgeSignalsVerificationStatus](https://developer.android.com/google/play/age-signals/reference/com/google/android/play/agesignals/model/AgeSignalsVerificationStatus). + */ +private enum class UserStatus(val value: Int) { + /** The user provided their age, but it hasn't been verified yet. */ + DECLARED(5), + + /** The user is 18+. */ + VERIFIED(0), + + /** The user's guardian has set the age for him. */ + SUPERVISED(1), + + /** The user's guardian hasn't approved the significant changes yet. */ + SUPERVISED_APPROVAL_PENDING(2), + + /** The user's guardian has denied approval for one or more pending significant changes. */ + SUPERVISED_APPROVAL_DENIED(3), + + /** The user is not verified or supervised. */ + UNKNOWN(4), +}