fix(Spotify - Spoof client): Skip native login screens (#5228)

Co-authored-by: oSumAtrIX <johan.melkonyan1@web.de>
Co-authored-by: Nuckyz <61953774+Nuckyz@users.noreply.github.com>
Co-authored-by: Dawid Krajcarz <80264606+drobotk@users.noreply.github.com>
Co-authored-by: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com>
This commit is contained in:
brosssh
2025-07-02 12:19:20 +02:00
committed by GitHub
parent d3935f03c0
commit d7ed32571f
4 changed files with 105 additions and 7 deletions

View File

@@ -1,6 +1,7 @@
package app.revanced.extension.spotify.misc.fix;
import android.view.LayoutInflater;
import android.view.View;
import app.revanced.extension.shared.Logger;
@SuppressWarnings("unused")
@@ -38,4 +39,16 @@ public class SpoofClientPatch {
Logger.printException(() -> "launchLogin failure", ex);
}
}
/**
* Injection point.
* <br>
* Set handler to call the native login after the webview login.
*/
public static void setNativeLoginHandler(View startLoginButton) {
WebApp.nativeLoginHandler = (() -> {
startLoginButton.setSoundEffectsEnabled(false);
startLoginButton.performClick();
});
}
}

View File

@@ -37,18 +37,23 @@ class WebApp {
static volatile Session currentSession = null;
/**
* Current webview in use. Any use of the object must be done on the main thread.
* Current webview in use. Any use of the object must be done on the main thread.
*/
@SuppressLint("StaticFieldLeak")
private static volatile WebView currentWebView;
interface NativeLoginHandler {
void login();
}
static NativeLoginHandler nativeLoginHandler;
static void launchLogin(Context context) {
final Dialog dialog = newDialog(context);
Utils.runOnBackgroundThread(() -> {
Logger.printInfo(() -> "Launching login");
// A session must be obtained from a login. Repeat until a session is acquired.
boolean isAcquired = false;
do {
@@ -77,6 +82,12 @@ class WebApp {
getSessionLatch.countDown();
dialog.dismiss();
try {
nativeLoginHandler.login();
} catch (Exception ex) {
Logger.printException(() -> "nativeLoginHandler failure", ex);
}
}
});

View File

@@ -24,6 +24,22 @@ internal val startupPageLayoutInflateFingerprint = fingerprint {
strings("blueprintContainer", "gradient", "valuePropositionTextView")
}
internal val renderStartLoginScreenFingerprint = fingerprint {
strings("authenticationButtonFactory", "MORE_OPTIONS")
}
internal val renderSecondLoginScreenFingerprint = fingerprint {
strings("authenticationButtonFactory", "intent_login")
}
internal val renderThirdLoginScreenFingerprint = fingerprint {
strings("EMAIL_OR_USERNAME", "listener")
}
internal val thirdLoginScreenLoginOnClickFingerprint = fingerprint {
strings("login", "listener", "none")
}
internal val runIntegrityVerificationFingerprint = fingerprint {
accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL)
returns("V")

View File

@@ -9,11 +9,9 @@ import app.revanced.patcher.patch.intOption
import app.revanced.patches.shared.misc.hex.HexPatchBuilder
import app.revanced.patches.shared.misc.hex.hexPatch
import app.revanced.patches.spotify.misc.extension.sharedExtensionPatch
import app.revanced.util.findInstructionIndicesReversedOrThrow
import app.revanced.util.getReference
import app.revanced.util.indexOfFirstInstructionReversedOrThrow
import app.revanced.util.returnEarly
import app.revanced.util.*
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
@@ -119,12 +117,72 @@ val spoofClientPatch = bytecodePatch(
addInstructions(
0,
"invoke-static/range { p1 .. p1 }, $openLoginWebViewDescriptor"
)
}
renderStartLoginScreenFingerprint.method.apply {
val onEventIndex = indexOfFirstInstructionOrThrow {
opcode == Opcode.INVOKE_INTERFACE && getReference<MethodReference>()?.name == "getView"
}
val buttonRegister = getInstruction<OneRegisterInstruction>(onEventIndex + 1).registerA
addInstruction(
onEventIndex + 2,
"invoke-static { v$buttonRegister }, $EXTENSION_CLASS_DESCRIPTOR->setNativeLoginHandler(Landroid/view/View;)V"
)
}
renderSecondLoginScreenFingerprint.method.apply {
val getViewIndex = indexOfFirstInstructionOrThrow {
opcode == Opcode.INVOKE_INTERFACE && getReference<MethodReference>()?.name == "getView"
}
val buttonRegister = getInstruction<OneRegisterInstruction>(getViewIndex + 1).registerA
// Early return the render for loop since the first item of the loop is the login button.
addInstructions(
getViewIndex + 2,
"""
invoke-static/range { p1 .. p1 }, $openLoginWebViewDescriptor
invoke-virtual { v$buttonRegister }, Landroid/view/View;->performClick()Z
return-void
"""
)
}
renderThirdLoginScreenFingerprint.method.apply {
val invokeSetListenerIndex = indexOfFirstInstructionOrThrow {
val reference = getReference<MethodReference>()
reference?.definingClass == "Landroid/view/View;" && reference.name == "setOnClickListener"
}
val buttonRegister = getInstruction<FiveRegisterInstruction>(invokeSetListenerIndex).registerC
addInstruction(
invokeSetListenerIndex + 1,
"invoke-virtual { v$buttonRegister }, Landroid/view/View;->performClick()Z"
)
}
thirdLoginScreenLoginOnClickFingerprint.method.apply {
// Use placeholder credentials to pass the login screen.
val loginActionIndex = indexOfFirstInstructionOrThrow(Opcode.RETURN_VOID) - 1
val loginActionInstruction = getInstruction<FiveRegisterInstruction>(loginActionIndex)
addInstructions(
loginActionIndex,
"""
const-string v${loginActionInstruction.registerD}, "placeholder"
const-string v${loginActionInstruction.registerE}, "placeholder"
"""
)
}
// endregion
// region Disable verdicts.
// Early return to block sending bad verdicts to the API.
runIntegrityVerificationFingerprint.method.returnEarly()