From 942ceb13bb344d2f02f44cfaadb6043f85cefafb Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Mon, 16 Dec 2024 01:18:16 +0100 Subject: [PATCH] fix(YouTube Music): Add `Spoof client` patch to fix playback --- extensions/music/build.gradle.kts | 1 + extensions/music/src/main/AndroidManifest.xml | 1 + .../music/spoof/SpoofClientPatch.java | 27 +++++ patches/api/patches.api | 12 +- .../misc/extension/SharedExtensionPatch.kt | 1 + .../music/misc/gms/GmsCoreSupportPatch.kt | 4 +- .../patches/music/misc/spoof/Fingerprints.kt | 39 +++++++ .../music/misc/spoof/SpoofClientPatch.kt | 105 ++++++++++++++++++ .../misc/spoof/SpoofVideoStreamsPatch.kt | 7 -- .../misc/spoof/UserAgentClientSpoofPatch.kt | 5 + .../misc/spoof/UserAgentClientSpoofPatch.kt | 81 ++++++++++++++ .../misc/spoof/UserAgentClientSpoofPatch.kt | 81 +------------- 12 files changed, 274 insertions(+), 90 deletions(-) create mode 100644 extensions/music/build.gradle.kts create mode 100644 extensions/music/src/main/AndroidManifest.xml create mode 100644 extensions/music/src/main/java/app/revanced/extension/music/spoof/SpoofClientPatch.java create mode 100644 patches/src/main/kotlin/app/revanced/patches/music/misc/spoof/Fingerprints.kt create mode 100644 patches/src/main/kotlin/app/revanced/patches/music/misc/spoof/SpoofClientPatch.kt delete mode 100644 patches/src/main/kotlin/app/revanced/patches/music/misc/spoof/SpoofVideoStreamsPatch.kt create mode 100644 patches/src/main/kotlin/app/revanced/patches/music/misc/spoof/UserAgentClientSpoofPatch.kt create mode 100644 patches/src/main/kotlin/app/revanced/patches/shared/misc/spoof/UserAgentClientSpoofPatch.kt diff --git a/extensions/music/build.gradle.kts b/extensions/music/build.gradle.kts new file mode 100644 index 0000000000..f3c06ad739 --- /dev/null +++ b/extensions/music/build.gradle.kts @@ -0,0 +1 @@ +// Do not remove. Necessary for the extension plugin to be applied to the project. diff --git a/extensions/music/src/main/AndroidManifest.xml b/extensions/music/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..9b65eb06cf --- /dev/null +++ b/extensions/music/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/extensions/music/src/main/java/app/revanced/extension/music/spoof/SpoofClientPatch.java b/extensions/music/src/main/java/app/revanced/extension/music/spoof/SpoofClientPatch.java new file mode 100644 index 0000000000..05c14bb304 --- /dev/null +++ b/extensions/music/src/main/java/app/revanced/extension/music/spoof/SpoofClientPatch.java @@ -0,0 +1,27 @@ +package app.revanced.extension.music.spoof; + +/** + * @noinspection unused + */ +public class SpoofClientPatch { + private static final int CLIENT_TYPE_ID = 26; + private static final String CLIENT_VERSION = "6.21"; + private static final String DEVICE_MODEL = "iPhone16,2"; + private static final String OS_VERSION = "17.7.2.21H221"; + + public static int getClientId() { + return CLIENT_TYPE_ID; + } + + public static String getClientVersion() { + return CLIENT_VERSION; + } + + public static String getClientModel() { + return DEVICE_MODEL; + } + + public static String getOsVersion() { + return OS_VERSION; + } +} \ No newline at end of file diff --git a/patches/api/patches.api b/patches/api/patches.api index 53a61711ca..c76a2d38fb 100644 --- a/patches/api/patches.api +++ b/patches/api/patches.api @@ -324,8 +324,12 @@ public final class app/revanced/patches/music/misc/gms/GmsCoreSupportPatchKt { public static final fun getGmsCoreSupportPatch ()Lapp/revanced/patcher/patch/BytecodePatch; } -public final class app/revanced/patches/music/misc/spoof/SpoofVideoStreamsPatchKt { - public static final fun getSpoofVideoStreamsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +public final class app/revanced/patches/music/misc/spoof/SpoofClientPatchKt { + public static final fun getSpoofClientPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/music/misc/spoof/UserAgentClientSpoofPatchKt { + public static final fun getUserAgentClientSpoofPatch ()Lapp/revanced/patcher/patch/BytecodePatch; } public final class app/revanced/patches/myexpenses/misc/pro/UnlockProPatchKt { @@ -766,6 +770,10 @@ public final class app/revanced/patches/shared/misc/spoof/SpoofVideoStreamsPatch public static synthetic fun spoofVideoStreamsPatch$default (Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lapp/revanced/patcher/patch/BytecodePatch; } +public final class app/revanced/patches/shared/misc/spoof/UserAgentClientSpoofPatchKt { + public static final fun userAgentClientSpoofPatch (Ljava/lang/String;)Lapp/revanced/patcher/patch/BytecodePatch; +} + public final class app/revanced/patches/solidexplorer2/functionality/filesize/RemoveFileSizeLimitPatchKt { public static final fun getRemoveFileSizeLimitPatch ()Lapp/revanced/patcher/patch/BytecodePatch; } diff --git a/patches/src/main/kotlin/app/revanced/patches/music/misc/extension/SharedExtensionPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/misc/extension/SharedExtensionPatch.kt index 5b25a006a4..9351b600ee 100644 --- a/patches/src/main/kotlin/app/revanced/patches/music/misc/extension/SharedExtensionPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/music/misc/extension/SharedExtensionPatch.kt @@ -4,5 +4,6 @@ import app.revanced.patches.music.misc.extension.hooks.applicationInitHook import app.revanced.patches.shared.misc.extension.sharedExtensionPatch val sharedExtensionPatch = sharedExtensionPatch( + "music", applicationInitHook, ) diff --git a/patches/src/main/kotlin/app/revanced/patches/music/misc/gms/GmsCoreSupportPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/misc/gms/GmsCoreSupportPatch.kt index 0fa223b235..2cb49fd5be 100644 --- a/patches/src/main/kotlin/app/revanced/patches/music/misc/gms/GmsCoreSupportPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/music/misc/gms/GmsCoreSupportPatch.kt @@ -4,7 +4,7 @@ import app.revanced.patcher.patch.Option import app.revanced.patches.music.misc.extension.sharedExtensionPatch import app.revanced.patches.music.misc.gms.Constants.MUSIC_PACKAGE_NAME import app.revanced.patches.music.misc.gms.Constants.REVANCED_MUSIC_PACKAGE_NAME -import app.revanced.patches.music.misc.spoof.spoofVideoStreamsPatch +import app.revanced.patches.music.misc.spoof.spoofClientPatch import app.revanced.patches.shared.castContextFetchFingerprint import app.revanced.patches.shared.misc.gms.gmsCoreSupportPatch import app.revanced.patches.shared.primeMethodFingerprint @@ -21,7 +21,7 @@ val gmsCoreSupportPatch = gmsCoreSupportPatch( extensionPatch = sharedExtensionPatch, gmsCoreSupportResourcePatchFactory = ::gmsCoreSupportResourcePatch, ) { - dependsOn(spoofVideoStreamsPatch) + dependsOn(spoofClientPatch) compatibleWith(MUSIC_PACKAGE_NAME) } diff --git a/patches/src/main/kotlin/app/revanced/patches/music/misc/spoof/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/music/misc/spoof/Fingerprints.kt new file mode 100644 index 0000000000..abf19cc958 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/misc/spoof/Fingerprints.kt @@ -0,0 +1,39 @@ +package app.revanced.patches.music.misc.spoof + +import app.revanced.patcher.fingerprint +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode + +internal val playerRequestConstructorFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR) + strings("player") +} + +/** + * Matches using the class found in [playerRequestConstructorFingerprint]. + */ +internal val createPlayerRequestBodyFingerprint = fingerprint { + parameters("L") + returns("V") + opcodes( + Opcode.CHECK_CAST, + Opcode.IGET, + Opcode.AND_INT_LIT16, + ) + strings("ms") +} + +/** + * Used to get a reference to other clientInfo fields. + */ +internal val setClientInfoFieldsFingerprint = fingerprint { + returns("L") + strings("Google Inc.") +} + +/** + * Used to get a reference to the clientInfo and clientInfo.clientVersion field. + */ +internal val setClientInfoClientVersionFingerprint = fingerprint { + strings("10.29") +} diff --git a/patches/src/main/kotlin/app/revanced/patches/music/misc/spoof/SpoofClientPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/misc/spoof/SpoofClientPatch.kt new file mode 100644 index 0000000000..a8f8933ac0 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/misc/spoof/SpoofClientPatch.kt @@ -0,0 +1,105 @@ +package app.revanced.patches.music.misc.spoof + +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.extensions.InstructionExtensions.instructions +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable +import app.revanced.patches.music.misc.extension.sharedExtensionPatch +import app.revanced.util.getReference +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.FieldReference +import com.android.tools.smali.dexlib2.iface.reference.TypeReference +import com.android.tools.smali.dexlib2.immutable.ImmutableMethod +import com.android.tools.smali.dexlib2.immutable.ImmutableMethodParameter + +internal const val EXTENSION_CLASS_DESCRIPTOR = + "Lapp/revanced/extension/music/spoof/SpoofClientPatch;" + +// TODO: Replace this patch with spoofVideoStreamsPatch once possible. +val spoofClientPatch = bytecodePatch( + name = "Spoof client", + description = "Spoofs the client to fix playback.", +) { + compatibleWith("com.google.android.apps.youtube.music") + + dependsOn( + sharedExtensionPatch, + // TODO: Add settingsPatch + userAgentClientSpoofPatch, + ) + + execute { + val playerRequestClass = playerRequestConstructorFingerprint.classDef + + val createPlayerRequestBodyMatch = createPlayerRequestBodyFingerprint.match(playerRequestClass) + + val clientInfoContainerClass = createPlayerRequestBodyMatch.method + .getInstruction(createPlayerRequestBodyMatch.patternMatch!!.startIndex) + .getReference()!!.type + + val clientInfoField = setClientInfoClientVersionFingerprint.method.instructions.first { + it.opcode == Opcode.IPUT_OBJECT && it.getReference()!!.definingClass == clientInfoContainerClass + }.getReference()!! + + val setClientInfoFieldInstructions = setClientInfoFieldsFingerprint.method.instructions.filter { + (it.opcode == Opcode.IPUT_OBJECT || it.opcode == Opcode.IPUT) && + it.getReference()!!.definingClass == clientInfoField.type + }.map { it.getReference()!! } + + // Offsets are known for the fields in the clientInfo object. + val clientIdField = setClientInfoFieldInstructions[0] + val clientModelField = setClientInfoFieldInstructions[5] + val osVersionField = setClientInfoFieldInstructions[7] + val clientVersionField = setClientInfoClientVersionFingerprint.method + .getInstruction(setClientInfoClientVersionFingerprint.stringMatches!!.first().index + 1) + .getReference() + + // Helper method to spoof the client info. + val spoofClientInfoMethod = ImmutableMethod( + playerRequestClass.type, + "spoofClientInfo", + listOf(ImmutableMethodParameter(clientInfoContainerClass, null, null)), + "V", + AccessFlags.PRIVATE.value or AccessFlags.STATIC.value, + null, + null, + MutableMethodImplementation(3), + ).toMutable().also(playerRequestClass.methods::add).apply { + addInstructions( + """ + iget-object v0, p0, $clientInfoField + + invoke-static { }, $EXTENSION_CLASS_DESCRIPTOR->getClientId()I + move-result v1 + iput v1, v0, $clientIdField + + invoke-static { }, $EXTENSION_CLASS_DESCRIPTOR->getClientModel()Ljava/lang/String; + move-result-object v1 + iput-object v1, v0, $clientModelField + + invoke-static { }, $EXTENSION_CLASS_DESCRIPTOR->getClientVersion()Ljava/lang/String; + move-result-object v1 + iput-object v1, v0, $clientVersionField + + invoke-static { }, $EXTENSION_CLASS_DESCRIPTOR->getOsVersion()Ljava/lang/String; + move-result-object v1 + iput-object v1, v0, $osVersionField + + return-void + """, + ) + } + + createPlayerRequestBodyMatch.method.apply { + val checkCastIndex = createPlayerRequestBodyMatch.patternMatch!!.startIndex + val clientInfoContainerRegister = getInstruction(checkCastIndex).registerA + + addInstruction(checkCastIndex + 1, "invoke-static {v$clientInfoContainerRegister}, $spoofClientInfoMethod") + } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/music/misc/spoof/SpoofVideoStreamsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/misc/spoof/SpoofVideoStreamsPatch.kt deleted file mode 100644 index 21eb321569..0000000000 --- a/patches/src/main/kotlin/app/revanced/patches/music/misc/spoof/SpoofVideoStreamsPatch.kt +++ /dev/null @@ -1,7 +0,0 @@ -package app.revanced.patches.music.misc.spoof - -import app.revanced.patches.shared.misc.spoof.spoofVideoStreamsPatch - -val spoofVideoStreamsPatch = spoofVideoStreamsPatch({ - compatibleWith("com.google.android.apps.youtube.music") -}) \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/music/misc/spoof/UserAgentClientSpoofPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/misc/spoof/UserAgentClientSpoofPatch.kt new file mode 100644 index 0000000000..4afee1080a --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/misc/spoof/UserAgentClientSpoofPatch.kt @@ -0,0 +1,5 @@ +package app.revanced.patches.music.misc.spoof + +import app.revanced.patches.shared.misc.spoof.userAgentClientSpoofPatch + +val userAgentClientSpoofPatch = userAgentClientSpoofPatch("com.google.android.apps.youtube.music") diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/misc/spoof/UserAgentClientSpoofPatch.kt b/patches/src/main/kotlin/app/revanced/patches/shared/misc/spoof/UserAgentClientSpoofPatch.kt new file mode 100644 index 0000000000..f6ca942ad3 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/shared/misc/spoof/UserAgentClientSpoofPatch.kt @@ -0,0 +1,81 @@ +package app.revanced.patches.shared.misc.spoof + +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction +import app.revanced.patches.all.misc.transformation.IMethodCall +import app.revanced.patches.all.misc.transformation.filterMapInstruction35c +import app.revanced.patches.all.misc.transformation.transformInstructionsPatch +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstruction +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.MethodReference +import com.android.tools.smali.dexlib2.iface.reference.StringReference + +private const val USER_AGENT_STRING_BUILDER_APPEND_METHOD_REFERENCE = + "Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;" + +fun userAgentClientSpoofPatch(originalPackageName: String) = transformInstructionsPatch( + filterMap = { classDef, _, instruction, instructionIndex -> + filterMapInstruction35c( + "Lapp/revanced/extension", + classDef, + instruction, + instructionIndex, + ) + }, + transform = transform@{ mutableMethod, entry -> + val (_, _, instructionIndex) = entry + + // Replace the result of context.getPackageName(), if it is used in a user agent string. + mutableMethod.apply { + // After context.getPackageName() the result is moved to a register. + val targetRegister = ( + getInstruction(instructionIndex + 1) + as? OneRegisterInstruction ?: return@transform + ).registerA + + // IndexOutOfBoundsException is technically possible here, + // but no such occurrences are present in the app. + val referee = getInstruction(instructionIndex + 2).getReference()?.toString() + + // Only replace string builder usage. + if (referee != USER_AGENT_STRING_BUILDER_APPEND_METHOD_REFERENCE) { + return@transform + } + + // Do not change the package name in methods that use resources, or for methods that use GmsCore. + // Changing these package names will result in playback limitations, + // particularly Android VR background audio only playback. + val resourceOrGmsStringInstructionIndex = indexOfFirstInstruction { + val reference = getReference() + opcode == Opcode.CONST_STRING && + (reference?.string == "android.resource://" || reference?.string == "gcore_") + } + if (resourceOrGmsStringInstructionIndex >= 0) { + return@transform + } + + // Overwrite the result of context.getPackageName() with the original package name. + replaceInstruction( + instructionIndex + 1, + "const-string v$targetRegister, \"$originalPackageName\"", + ) + } + }, +) + +@Suppress("unused") +private enum class MethodCall( + override val definedClassName: String, + override val methodName: String, + override val methodParams: Array, + override val returnType: String, +) : IMethodCall { + GetPackageName( + "Landroid/content/Context;", + "getPackageName", + emptyArray(), + "Ljava/lang/String;", + ), +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/spoof/UserAgentClientSpoofPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/spoof/UserAgentClientSpoofPatch.kt index d9b6596138..f881b24d1a 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/spoof/UserAgentClientSpoofPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/spoof/UserAgentClientSpoofPatch.kt @@ -1,82 +1,5 @@ package app.revanced.patches.youtube.misc.spoof -import app.revanced.patcher.extensions.InstructionExtensions.getInstruction -import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction -import app.revanced.patches.all.misc.transformation.IMethodCall -import app.revanced.patches.all.misc.transformation.filterMapInstruction35c -import app.revanced.patches.all.misc.transformation.transformInstructionsPatch -import app.revanced.util.getReference -import app.revanced.util.indexOfFirstInstruction -import com.android.tools.smali.dexlib2.Opcode -import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction -import com.android.tools.smali.dexlib2.iface.reference.MethodReference -import com.android.tools.smali.dexlib2.iface.reference.StringReference +import app.revanced.patches.shared.misc.spoof.userAgentClientSpoofPatch -private const val ORIGINAL_PACKAGE_NAME = "com.google.android.youtube" -private const val USER_AGENT_STRING_BUILDER_APPEND_METHOD_REFERENCE = - "Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;" - -val userAgentClientSpoofPatch = transformInstructionsPatch( - filterMap = { classDef, _, instruction, instructionIndex -> - filterMapInstruction35c( - "Lapp/revanced/extension", - classDef, - instruction, - instructionIndex, - ) - }, - transform = transform@{ mutableMethod, entry -> - val (_, _, instructionIndex) = entry - - // Replace the result of context.getPackageName(), if it is used in a user agent string. - mutableMethod.apply { - // After context.getPackageName() the result is moved to a register. - val targetRegister = ( - getInstruction(instructionIndex + 1) - as? OneRegisterInstruction ?: return@transform - ).registerA - - // IndexOutOfBoundsException is technically possible here, - // but no such occurrences are present in the app. - val referee = getInstruction(instructionIndex + 2).getReference()?.toString() - - // Only replace string builder usage. - if (referee != USER_AGENT_STRING_BUILDER_APPEND_METHOD_REFERENCE) { - return@transform - } - - // Do not change the package name in methods that use resources, or for methods that use GmsCore. - // Changing these package names will result in playback limitations, - // particularly Android VR background audio only playback. - val resourceOrGmsStringInstructionIndex = indexOfFirstInstruction { - val reference = getReference() - opcode == Opcode.CONST_STRING && - (reference?.string == "android.resource://" || reference?.string == "gcore_") - } - if (resourceOrGmsStringInstructionIndex >= 0) { - return@transform - } - - // Overwrite the result of context.getPackageName() with the original package name. - replaceInstruction( - instructionIndex + 1, - "const-string v$targetRegister, \"$ORIGINAL_PACKAGE_NAME\"", - ) - } - }, -) - -@Suppress("unused") -private enum class MethodCall( - override val definedClassName: String, - override val methodName: String, - override val methodParams: Array, - override val returnType: String, -) : IMethodCall { - GetPackageName( - "Landroid/content/Context;", - "getPackageName", - emptyArray(), - "Ljava/lang/String;", - ), -} +val userAgentClientSpoofPatch = userAgentClientSpoofPatch("com.google.android.youtube")