From 2e3511d03c8198bbdb9336888df038a33fb3ab8c Mon Sep 17 00:00:00 2001 From: Dawid Krajcarz <80264606+drobotk@users.noreply.github.com> Date: Tue, 6 May 2025 09:31:56 +0200 Subject: [PATCH] feat(Spotify): Add `Sanitize sharing links` patch (#4829) --- .../privacy/SanitizeSharingLinksPatch.java | 43 ++++++++++++ patches/api/patches.api | 4 ++ .../spotify/misc/UnlockPremiumPatch.kt | 4 +- .../spotify/misc/privacy/Fingerprints.kt | 41 +++++++++++ .../misc/privacy/SanitizeSharingLinksPatch.kt | 70 +++++++++++++++++++ 5 files changed, 160 insertions(+), 2 deletions(-) create mode 100644 extensions/spotify/src/main/java/app/revanced/extension/spotify/misc/privacy/SanitizeSharingLinksPatch.java create mode 100644 patches/src/main/kotlin/app/revanced/patches/spotify/misc/privacy/Fingerprints.kt create mode 100644 patches/src/main/kotlin/app/revanced/patches/spotify/misc/privacy/SanitizeSharingLinksPatch.kt diff --git a/extensions/spotify/src/main/java/app/revanced/extension/spotify/misc/privacy/SanitizeSharingLinksPatch.java b/extensions/spotify/src/main/java/app/revanced/extension/spotify/misc/privacy/SanitizeSharingLinksPatch.java new file mode 100644 index 000000000..55541ec9c --- /dev/null +++ b/extensions/spotify/src/main/java/app/revanced/extension/spotify/misc/privacy/SanitizeSharingLinksPatch.java @@ -0,0 +1,43 @@ +package app.revanced.extension.spotify.misc.privacy; + +import android.net.Uri; + +import java.util.List; + +import app.revanced.extension.shared.Logger; + +@SuppressWarnings("unused") +public final class SanitizeSharingLinksPatch { + + /** + * Parameters that are considered undesirable and should be stripped away. + */ + private static final List SHARE_PARAMETERS_TO_REMOVE = List.of( + "si", // Share tracking parameter. + "utm_source" // Share source, such as "copy-link". + ); + + /** + * Injection point. + */ + public static String sanitizeUrl(String url) { + try { + Uri uri = Uri.parse(url); + Uri.Builder builder = uri.buildUpon().clearQuery(); + + for (String paramName : uri.getQueryParameterNames()) { + if (!SHARE_PARAMETERS_TO_REMOVE.contains(paramName)) { + for (String value : uri.getQueryParameters(paramName)) { + builder.appendQueryParameter(paramName, value); + } + } + } + + return builder.build().toString(); + } catch (Exception ex) { + Logger.printException(() -> "sanitizeUrl failure", ex); + + return url; + } + } +} diff --git a/patches/api/patches.api b/patches/api/patches.api index 1efb1246b..df4e58df1 100644 --- a/patches/api/patches.api +++ b/patches/api/patches.api @@ -852,6 +852,10 @@ public final class app/revanced/patches/spotify/misc/fix/SpoofSignaturePatchKt { public static final fun getSpoofSignaturePatch ()Lapp/revanced/patcher/patch/BytecodePatch; } +public final class app/revanced/patches/spotify/misc/privacy/SanitizeSharingLinksPatchKt { + public static final fun getSanitizeSharingLinksPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + public final class app/revanced/patches/spotify/navbar/PremiumNavbarTabPatchKt { public static final fun getPremiumNavbarTabPatch ()Lapp/revanced/patcher/patch/BytecodePatch; } diff --git a/patches/src/main/kotlin/app/revanced/patches/spotify/misc/UnlockPremiumPatch.kt b/patches/src/main/kotlin/app/revanced/patches/spotify/misc/UnlockPremiumPatch.kt index 8678517f9..74eaa578c 100644 --- a/patches/src/main/kotlin/app/revanced/patches/spotify/misc/UnlockPremiumPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/spotify/misc/UnlockPremiumPatch.kt @@ -7,7 +7,6 @@ import app.revanced.patcher.extensions.InstructionExtensions.removeInstruction import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction import app.revanced.patcher.fingerprint import app.revanced.patcher.patch.bytecodePatch -import app.revanced.patches.spotify.misc.check.checkEnvironmentPatch import app.revanced.patches.spotify.misc.extension.IS_SPOTIFY_LEGACY_APP_TARGET import app.revanced.patches.spotify.misc.extension.sharedExtensionPatch import app.revanced.util.getReference @@ -72,9 +71,10 @@ val unlockPremiumPatch = bytecodePatch( if (IS_SPOTIFY_LEGACY_APP_TARGET) { - return@execute Logger.getLogger(this::class.java.name).warning( + Logger.getLogger(this::class.java.name).warning( "Patching a legacy Spotify version. Patch functionality may be limited." ) + return@execute } diff --git a/patches/src/main/kotlin/app/revanced/patches/spotify/misc/privacy/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/spotify/misc/privacy/Fingerprints.kt new file mode 100644 index 000000000..3d60abf9b --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/spotify/misc/privacy/Fingerprints.kt @@ -0,0 +1,41 @@ +package app.revanced.patches.spotify.misc.privacy + +import app.revanced.patcher.fingerprint +import app.revanced.util.literal +import com.android.tools.smali.dexlib2.AccessFlags + +internal val shareCopyUrlFingerprint = fingerprint { + returns("Ljava/lang/Object;") + parameters("Ljava/lang/Object;") + strings("clipboard", "Spotify Link") + custom { method, _ -> + method.name == "invokeSuspend" + } +} + +internal val shareCopyUrlLegacyFingerprint = fingerprint { + returns("Ljava/lang/Object;") + parameters("Ljava/lang/Object;") + strings("clipboard", "createNewSession failed") + custom { method, _ -> + method.name == "apply" + } +} + +internal val formatAndroidShareSheetUrlFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC) + returns("Ljava/lang/String;") + parameters("L", "Ljava/lang/String;") + literal { + '\n'.code.toLong() + } +} + +internal val formatAndroidShareSheetUrlLegacyFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC) + returns("Ljava/lang/String;") + parameters("Lcom/spotify/share/social/sharedata/ShareData;", "Ljava/lang/String;") + literal { + '\n'.code.toLong() + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/spotify/misc/privacy/SanitizeSharingLinksPatch.kt b/patches/src/main/kotlin/app/revanced/patches/spotify/misc/privacy/SanitizeSharingLinksPatch.kt new file mode 100644 index 000000000..8df4c7720 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/spotify/misc/privacy/SanitizeSharingLinksPatch.kt @@ -0,0 +1,70 @@ +package app.revanced.patches.spotify.misc.privacy + +import app.revanced.patcher.Fingerprint +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.spotify.misc.extension.IS_SPOTIFY_LEGACY_APP_TARGET +import app.revanced.patches.spotify.misc.extension.sharedExtensionPatch +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstructionOrThrow +import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.MethodReference + +private const val EXTENSION_CLASS_DESCRIPTOR = + "Lapp/revanced/extension/spotify/misc/privacy/SanitizeSharingLinksPatch;" + +@Suppress("unused") +val sanitizeSharingLinksPatch = bytecodePatch( + name = "Sanitize sharing links", + description = "Removes the tracking query parameters from links before they are shared.", +) { + compatibleWith("com.spotify.music") + + dependsOn(sharedExtensionPatch) + + execute { + val extensionMethodDescriptor = "$EXTENSION_CLASS_DESCRIPTOR->" + + "sanitizeUrl(Ljava/lang/String;)Ljava/lang/String;" + + val copyFingerprint = if (IS_SPOTIFY_LEGACY_APP_TARGET) { + shareCopyUrlLegacyFingerprint + } else { + shareCopyUrlFingerprint + } + + copyFingerprint.method.apply { + val newPlainTextInvokeIndex = indexOfFirstInstructionOrThrow { + getReference()?.name == "newPlainText" + } + val register = getInstruction(newPlainTextInvokeIndex).registerD + + addInstructions( + newPlainTextInvokeIndex, + """ + invoke-static { v$register }, $extensionMethodDescriptor + move-result-object v$register + """ + ) + } + + // Android native share sheet is used for all other quick share types (X, WhatsApp, etc). + val shareUrlParameter : String + val shareSheetFingerprint : Fingerprint + if (IS_SPOTIFY_LEGACY_APP_TARGET) { + shareSheetFingerprint = formatAndroidShareSheetUrlLegacyFingerprint + shareUrlParameter = "p2" + } else { + shareSheetFingerprint = formatAndroidShareSheetUrlFingerprint + shareUrlParameter = "p1" + } + + shareSheetFingerprint.method.addInstructions( + 0, + """ + invoke-static { $shareUrlParameter }, $extensionMethodDescriptor + move-result-object $shareUrlParameter + """ + ) + } +}