From ef44eaa119b9d6c5faec051e22d20f883d0da4f1 Mon Sep 17 00:00:00 2001 From: hxreborn <32096750+hxreborn@users.noreply.github.com> Date: Sun, 26 Oct 2025 11:01:32 +0100 Subject: [PATCH] feat(TikTok): Add `Sanitize sharing links` patch (#6176) --- .../shared/privacy/LinkSanitizer.java | 13 ++- .../ExtensionPreferenceCategory.java | 6 ++ .../tiktok/share/ShareUrlSanitizer.java | 29 +++++++ patches/api/patches.api | 4 + .../patches/tiktok/misc/share/Fingerprints.kt | 25 ++++++ .../misc/share/SanitizeShareUrlsPatch.kt | 85 +++++++++++++++++++ 6 files changed, 155 insertions(+), 7 deletions(-) create mode 100644 extensions/tiktok/src/main/java/app/revanced/extension/tiktok/share/ShareUrlSanitizer.java create mode 100644 patches/src/main/kotlin/app/revanced/patches/tiktok/misc/share/Fingerprints.kt create mode 100644 patches/src/main/kotlin/app/revanced/patches/tiktok/misc/share/SanitizeShareUrlsPatch.kt diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/privacy/LinkSanitizer.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/privacy/LinkSanitizer.java index 9cfa05c1b..853ced003 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/privacy/LinkSanitizer.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/privacy/LinkSanitizer.java @@ -17,9 +17,6 @@ public class LinkSanitizer { public LinkSanitizer(String ... parametersToRemove) { final int parameterCount = parametersToRemove.length; - if (parameterCount == 0) { - throw new IllegalArgumentException("No parameters specified"); - } // List is faster if only checking a few parameters. this.parametersToRemove = parameterCount > 4 @@ -40,10 +37,12 @@ public class LinkSanitizer { try { Uri.Builder builder = uri.buildUpon().clearQuery(); - for (String paramName : uri.getQueryParameterNames()) { - if (!parametersToRemove.contains(paramName)) { - for (String value : uri.getQueryParameters(paramName)) { - builder.appendQueryParameter(paramName, value); + if (!parametersToRemove.isEmpty()) { + for (String paramName : uri.getQueryParameterNames()) { + if (!parametersToRemove.contains(paramName)) { + for (String value : uri.getQueryParameters(paramName)) { + builder.appendQueryParameter(paramName, value); + } } } } diff --git a/extensions/tiktok/src/main/java/app/revanced/extension/tiktok/settings/preference/categories/ExtensionPreferenceCategory.java b/extensions/tiktok/src/main/java/app/revanced/extension/tiktok/settings/preference/categories/ExtensionPreferenceCategory.java index 60d7983ea..7383a5582 100644 --- a/extensions/tiktok/src/main/java/app/revanced/extension/tiktok/settings/preference/categories/ExtensionPreferenceCategory.java +++ b/extensions/tiktok/src/main/java/app/revanced/extension/tiktok/settings/preference/categories/ExtensionPreferenceCategory.java @@ -23,6 +23,12 @@ public class ExtensionPreferenceCategory extends ConditionalPreferenceCategory { public void addPreferences(Context context) { addPreference(new ReVancedTikTokAboutPreference(context)); + addPreference(new TogglePreference(context, + "Sanitize sharing links", + "Remove tracking parameters from shared links.", + BaseSettings.SANITIZE_SHARED_LINKS + )); + addPreference(new TogglePreference(context, "Enable debug log", "Show extension debug log.", diff --git a/extensions/tiktok/src/main/java/app/revanced/extension/tiktok/share/ShareUrlSanitizer.java b/extensions/tiktok/src/main/java/app/revanced/extension/tiktok/share/ShareUrlSanitizer.java new file mode 100644 index 000000000..5d09c10c0 --- /dev/null +++ b/extensions/tiktok/src/main/java/app/revanced/extension/tiktok/share/ShareUrlSanitizer.java @@ -0,0 +1,29 @@ +package app.revanced.extension.tiktok.share; + +import app.revanced.extension.shared.Logger; +import app.revanced.extension.shared.privacy.LinkSanitizer; +import app.revanced.extension.shared.settings.BaseSettings; + +@SuppressWarnings("unused") +public final class ShareUrlSanitizer { + + private static final LinkSanitizer sanitizer = new LinkSanitizer(); + + /** + * Injection point for setting check. + */ + public static boolean shouldSanitize() { + return BaseSettings.SANITIZE_SHARED_LINKS.get(); + } + + /** + * Injection point for URL sanitization. + */ + public static String sanitizeShareUrl(final String url) { + if (url == null || url.isEmpty()) { + return url; + } + + return sanitizer.sanitizeUrlString(url); + } +} diff --git a/patches/api/patches.api b/patches/api/patches.api index 14907b9d1..9c3b806d3 100644 --- a/patches/api/patches.api +++ b/patches/api/patches.api @@ -1188,6 +1188,10 @@ public final class app/revanced/patches/tiktok/misc/settings/SettingsPatchKt { public static final fun getSettingsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; } +public final class app/revanced/patches/tiktok/misc/share/SanitizeShareUrlsPatchKt { + public static final fun getSanitizeShareUrlsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + public final class app/revanced/patches/tiktok/misc/spoof/sim/SpoofSimPatchKt { public static final fun getSpoofSimPatch ()Lapp/revanced/patcher/patch/BytecodePatch; } diff --git a/patches/src/main/kotlin/app/revanced/patches/tiktok/misc/share/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/tiktok/misc/share/Fingerprints.kt new file mode 100644 index 000000000..836be8900 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/tiktok/misc/share/Fingerprints.kt @@ -0,0 +1,25 @@ +package app.revanced.patches.tiktok.misc.share + +import app.revanced.patcher.fingerprint +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode + +internal val urlShorteningFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC, AccessFlags.FINAL) + returns("LX/") + parameters( + "I", + "Ljava/lang/String;", + "Ljava/lang/String;", + "Ljava/lang/String;" + ) + opcodes(Opcode.RETURN_OBJECT) + + // Same Kotlin intrinsics literal on both variants. + strings("getShortShareUrlObservab\u2026ongUrl, subBizSceneValue)") + + custom { method, _ -> + // LIZLLL is obfuscated by ProGuard/R8, but stable across both TikTok and Musically. + method.name == "LIZLLL" + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/tiktok/misc/share/SanitizeShareUrlsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/tiktok/misc/share/SanitizeShareUrlsPatch.kt new file mode 100644 index 000000000..fd616141c --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/tiktok/misc/share/SanitizeShareUrlsPatch.kt @@ -0,0 +1,85 @@ +package app.revanced.patches.tiktok.misc.share + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.shared.PATCH_DESCRIPTION_SANITIZE_SHARING_LINKS +import app.revanced.patches.shared.PATCH_NAME_SANITIZE_SHARING_LINKS +import app.revanced.patches.tiktok.misc.extension.sharedExtensionPatch +import app.revanced.util.findFreeRegister +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstructionOrThrow +import com.android.tools.smali.dexlib2.Opcode +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 + +private const val EXTENSION_CLASS_DESCRIPTOR = + "Lapp/revanced/extension/tiktok/share/ShareUrlSanitizer;" + +@Suppress("unused") +val sanitizeShareUrlsPatch = bytecodePatch( + name = PATCH_NAME_SANITIZE_SHARING_LINKS, + description = PATCH_DESCRIPTION_SANITIZE_SHARING_LINKS, +) { + dependsOn(sharedExtensionPatch) + + compatibleWith( + "com.ss.android.ugc.trill"("36.5.4"), + "com.zhiliaoapp.musically"("36.5.4"), + ) + + execute { + urlShorteningFingerprint.method.apply { + val invokeIndex = indexOfFirstInstructionOrThrow { + val ref = getReference() + ref?.name == "LIZ" && ref.definingClass.startsWith("LX/") + } + + val moveResultIndex = indexOfFirstInstructionOrThrow(invokeIndex, Opcode.MOVE_RESULT_OBJECT) + val urlRegister = getInstruction(moveResultIndex).registerA + + // Resolve Observable wrapper classes at runtime + val observableWrapperIndex = indexOfFirstInstructionOrThrow(Opcode.NEW_INSTANCE) + val observableWrapperClass = getInstruction(observableWrapperIndex) + .reference.toString() + + val observableFactoryIndex = indexOfFirstInstructionOrThrow { + val ref = getReference() + ref?.name == "LJ" && ref.definingClass.startsWith("LX/") + } + val observableFactoryRef = getInstruction(observableFactoryIndex) + .reference as MethodReference + + val observableFactoryClass = observableFactoryRef.definingClass + val observableInterfaceType = observableFactoryRef.parameterTypes.first() + val observableReturnType = observableFactoryRef.returnType + + val wrapperRegister = findFreeRegister(moveResultIndex + 1, urlRegister) + + // Check setting and conditionally sanitize share URL. + addInstructionsWithLabels( + moveResultIndex + 1, + """ + invoke-static {}, $EXTENSION_CLASS_DESCRIPTOR->shouldSanitize()Z + move-result v$wrapperRegister + if-eqz v$wrapperRegister, :skip_sanitization + + invoke-static { p1 }, $EXTENSION_CLASS_DESCRIPTOR->sanitizeShareUrl(Ljava/lang/String;)Ljava/lang/String; + move-result-object v$urlRegister + + # Wrap sanitized URL and return early to bypass ShareExtService + new-instance v$wrapperRegister, $observableWrapperClass + invoke-direct { v$wrapperRegister, v$urlRegister }, $observableWrapperClass->(Ljava/lang/String;)V + invoke-static { v$wrapperRegister }, $observableFactoryClass->LJ($observableInterfaceType)$observableReturnType + move-result-object v$urlRegister + return-object v$urlRegister + + :skip_sanitization + nop + """ + ) + } + } +}