Repo Created
This commit is contained in:
parent
eb305e2886
commit
a8c22c65db
4784 changed files with 329907 additions and 2 deletions
68
play-services-recaptcha/core/build.gradle
Normal file
68
play-services-recaptcha/core/build.gradle
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 microG Project Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
apply plugin: 'com.android.library'
|
||||
apply plugin: 'com.squareup.wire'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'maven-publish'
|
||||
apply plugin: 'signing'
|
||||
|
||||
dependencies {
|
||||
implementation project(':play-services-base-core')
|
||||
implementation project(':play-services-droidguard-core')
|
||||
implementation project(':play-services-safetynet-core')
|
||||
implementation project(':play-services-recaptcha')
|
||||
implementation project(':play-services-tasks-ktx')
|
||||
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutineVersion"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutineVersion"
|
||||
|
||||
implementation "androidx.core:core-ktx:$coreVersion"
|
||||
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion"
|
||||
implementation "androidx.lifecycle:lifecycle-service:$lifecycleVersion"
|
||||
implementation "androidx.webkit:webkit:$webkitVersion"
|
||||
|
||||
implementation "com.android.volley:volley:$volleyVersion"
|
||||
implementation "com.squareup.wire:wire-runtime:$wireVersion"
|
||||
}
|
||||
|
||||
wire {
|
||||
kotlin {}
|
||||
}
|
||||
|
||||
android {
|
||||
namespace "org.microg.gms.recaptcha.core"
|
||||
|
||||
compileSdkVersion androidCompileSdk
|
||||
buildToolsVersion "$androidBuildVersionTools"
|
||||
|
||||
defaultConfig {
|
||||
versionName version
|
||||
minSdkVersion androidMinSdk
|
||||
targetSdkVersion androidTargetSdk
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main.java.srcDirs += 'src/main/kotlin'
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
disable 'MissingTranslation'
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = 1.8
|
||||
targetCompatibility = 1.8
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = 1.8
|
||||
}
|
||||
}
|
||||
|
||||
apply from: '../../gradle/publish-android.gradle'
|
||||
|
||||
description = 'microG service implementation for play-services-recaptcha'
|
||||
18
play-services-recaptcha/core/src/main/AndroidManifest.xml
Normal file
18
play-services-recaptcha/core/src/main/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ SPDX-FileCopyrightText: 2021 microG Project Team
|
||||
~ SPDX-License-Identifier: Apache-2.0
|
||||
-->
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<application>
|
||||
<!-- This service is in :ui process because it may spawn a web view. See https://crbug.com/558377 -->
|
||||
<service android:name="org.microg.gms.recaptcha.RecaptchaService"
|
||||
android:process=":ui">
|
||||
<intent-filter>
|
||||
<action android:name="com.google.android.gms.recaptcha.service.START"/>
|
||||
</intent-filter>
|
||||
</service>
|
||||
</application>
|
||||
</manifest>
|
||||
|
|
@ -0,0 +1,133 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 microG Project Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.microg.gms.recaptcha
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build.VERSION.SDK_INT
|
||||
import android.os.LocaleList
|
||||
import android.util.Log
|
||||
import com.android.volley.*
|
||||
import com.android.volley.toolbox.Volley
|
||||
import com.google.android.gms.droidguard.DroidGuardClient
|
||||
import com.google.android.gms.recaptcha.RecaptchaHandle
|
||||
import com.google.android.gms.recaptcha.RecaptchaResultData
|
||||
import com.google.android.gms.recaptcha.internal.ExecuteParams
|
||||
import com.google.android.gms.recaptcha.internal.InitParams
|
||||
import com.google.android.gms.tasks.await
|
||||
import com.squareup.wire.Message
|
||||
import com.squareup.wire.ProtoAdapter
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import org.microg.gms.droidguard.core.VersionUtil
|
||||
import org.microg.gms.utils.singleInstanceOf
|
||||
import java.util.*
|
||||
import kotlin.collections.HashMap
|
||||
|
||||
private const val TAG = "RecaptchaGuard"
|
||||
|
||||
class RecaptchaGuardImpl(private val context: Context, private val packageName: String) : RecaptchaImpl {
|
||||
private val queue = singleInstanceOf { Volley.newRequestQueue(context.applicationContext) }
|
||||
private var lastToken: String? = null
|
||||
|
||||
override suspend fun init(params: InitParams): RecaptchaHandle {
|
||||
val response = ProtobufPostRequest(
|
||||
"https://www.recaptcha.net/recaptcha/api3/ac", RecaptchaInitRequest(
|
||||
data_ = RecaptchaInitRequest.Data(
|
||||
siteKey = params.siteKey,
|
||||
packageName = packageName,
|
||||
version = "${VersionUtil(context).versionCode};${params.version}"
|
||||
)
|
||||
), RecaptchaInitResponse.ADAPTER
|
||||
).sendAndAwait(queue)
|
||||
lastToken = response.token
|
||||
return RecaptchaHandle(params.siteKey, packageName, response.acceptableAdditionalArgs.toList())
|
||||
}
|
||||
|
||||
override suspend fun execute(params: ExecuteParams): RecaptchaResultData {
|
||||
if (params.handle.clientPackageName != null && params.handle.clientPackageName != packageName) throw IllegalArgumentException("invalid handle")
|
||||
val timestamp = System.currentTimeMillis()
|
||||
val additionalArgs = mutableMapOf<String, String>()
|
||||
val guardMap = mutableMapOf<String, String>()
|
||||
for (key in params.action.additionalArgs.keySet()) {
|
||||
val value = params.action.additionalArgs.getString(key)
|
||||
?: throw Exception("Only string values are allowed as an additional arg in RecaptchaAction")
|
||||
if (key !in params.handle.acceptableAdditionalArgs)
|
||||
throw Exception("AdditionalArgs key[ \"$key\" ] is not accepted by reCATPCHA server")
|
||||
additionalArgs.put(key, value)
|
||||
}
|
||||
Log.d(TAG, "Additional arguments: $additionalArgs")
|
||||
if (lastToken == null) {
|
||||
init(InitParams().apply { siteKey = params.handle.siteKey; version = params.version })
|
||||
}
|
||||
val token = lastToken!!
|
||||
guardMap["token"] = token
|
||||
guardMap["action"] = params.action.toString()
|
||||
guardMap["timestamp_millis"] to timestamp.toString()
|
||||
guardMap.putAll(additionalArgs)
|
||||
if (params.action.verificationHistoryToken != null)
|
||||
guardMap["verification_history_token"] = params.action.verificationHistoryToken
|
||||
val dg = DroidGuardClient.getResults(context, "recaptcha-android", guardMap).await()
|
||||
val response = ProtobufPostRequest(
|
||||
"https://www.recaptcha.net/recaptcha/api3/ae", RecaptchaExecuteRequest(
|
||||
token = token,
|
||||
action = params.action.toString(),
|
||||
timestamp = timestamp,
|
||||
dg = dg,
|
||||
additionalArgs = additionalArgs,
|
||||
verificationHistoryToken = params.action.verificationHistoryToken
|
||||
), RecaptchaExecuteResponse.ADAPTER
|
||||
).sendAndAwait(queue)
|
||||
return RecaptchaResultData(response.token)
|
||||
}
|
||||
|
||||
override suspend fun close(handle: RecaptchaHandle): Boolean {
|
||||
if (handle.clientPackageName != null && handle.clientPackageName != packageName) throw IllegalArgumentException("invalid handle")
|
||||
val closed = lastToken != null
|
||||
lastToken = null
|
||||
return closed
|
||||
}
|
||||
}
|
||||
|
||||
class ProtobufPostRequest<I : Message<I, *>, O>(url: String, private val i: I, private val oAdapter: ProtoAdapter<O>) :
|
||||
Request<O>(Request.Method.POST, url, null) {
|
||||
private val deferred = CompletableDeferred<O>()
|
||||
|
||||
override fun getHeaders(): Map<String, String> {
|
||||
val headers = HashMap(super.getHeaders())
|
||||
headers["Accept-Language"] = if (SDK_INT >= 24) LocaleList.getDefault().toLanguageTags() else Locale.getDefault().language
|
||||
return headers
|
||||
}
|
||||
|
||||
override fun getBody(): ByteArray = i.encode()
|
||||
|
||||
override fun getBodyContentType(): String = "application/x-protobuf"
|
||||
|
||||
override fun parseNetworkResponse(response: NetworkResponse): Response<O> {
|
||||
try {
|
||||
return Response.success(oAdapter.decode(response.data), null)
|
||||
} catch (e: VolleyError) {
|
||||
return Response.error(e)
|
||||
} catch (e: Exception) {
|
||||
return Response.error(VolleyError())
|
||||
}
|
||||
}
|
||||
|
||||
override fun deliverResponse(response: O) {
|
||||
Log.d(TAG, "Got response: $response")
|
||||
deferred.complete(response)
|
||||
}
|
||||
|
||||
override fun deliverError(error: VolleyError) {
|
||||
deferred.completeExceptionally(error)
|
||||
}
|
||||
|
||||
suspend fun await(): O = deferred.await()
|
||||
|
||||
suspend fun sendAndAwait(queue: RequestQueue): O {
|
||||
Log.d(TAG, "Sending request: $i")
|
||||
queue.add(this)
|
||||
return await()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 microG Project Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.microg.gms.recaptcha
|
||||
|
||||
import com.google.android.gms.recaptcha.RecaptchaHandle
|
||||
import com.google.android.gms.recaptcha.RecaptchaResultData
|
||||
import com.google.android.gms.recaptcha.internal.ExecuteParams
|
||||
import com.google.android.gms.recaptcha.internal.InitParams
|
||||
|
||||
interface RecaptchaImpl {
|
||||
suspend fun init(params: InitParams): RecaptchaHandle
|
||||
suspend fun execute(params: ExecuteParams): RecaptchaResultData
|
||||
suspend fun close(handle: RecaptchaHandle): Boolean
|
||||
|
||||
object Unsupported : RecaptchaImpl {
|
||||
override suspend fun init(params: InitParams): RecaptchaHandle {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override suspend fun execute(params: ExecuteParams): RecaptchaResultData {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override suspend fun close(handle: RecaptchaHandle): Boolean {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,182 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 microG Project Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
package org.microg.gms.recaptcha
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build.VERSION.SDK_INT
|
||||
import android.os.Parcel
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.google.android.gms.common.Feature
|
||||
import com.google.android.gms.common.api.CommonStatusCodes
|
||||
import com.google.android.gms.common.api.Status
|
||||
import com.google.android.gms.common.internal.ConnectionInfo
|
||||
import com.google.android.gms.common.internal.GetServiceRequest
|
||||
import com.google.android.gms.common.internal.IGmsCallbacks
|
||||
import com.google.android.gms.recaptcha.RecaptchaAction
|
||||
import com.google.android.gms.recaptcha.RecaptchaHandle
|
||||
import com.google.android.gms.recaptcha.internal.*
|
||||
import kotlinx.coroutines.launch
|
||||
import org.microg.gms.BaseService
|
||||
import org.microg.gms.common.GmsService
|
||||
import org.microg.gms.common.PackageUtils
|
||||
import org.microg.gms.droidguard.core.DroidGuardPreferences
|
||||
import org.microg.gms.safetynet.SafetyNetPreferences
|
||||
import org.microg.gms.utils.warnOnTransactionIssues
|
||||
|
||||
private const val TAG = "RecaptchaService"
|
||||
|
||||
class RecaptchaService : BaseService(TAG, GmsService.RECAPTCHA) {
|
||||
private fun getRecaptchaImpl(packageName: String): ArrayList<RecaptchaImpl> {
|
||||
val list = ArrayList<RecaptchaImpl>()
|
||||
if (SafetyNetPreferences.isEnabled(this) && SDK_INT >= 19) {
|
||||
list.add(RecaptchaWebImpl(this, packageName, lifecycle))
|
||||
}
|
||||
if (DroidGuardPreferences.isAvailable(this)) {
|
||||
list.add(RecaptchaGuardImpl(this, packageName))
|
||||
}
|
||||
if (list.isEmpty()) {
|
||||
list.add(RecaptchaImpl.Unsupported)
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
override fun handleServiceRequest(callback: IGmsCallbacks, request: GetServiceRequest, service: GmsService) {
|
||||
val packageName = PackageUtils.getAndCheckCallingPackage(this, request.packageName)!!
|
||||
val imps = getRecaptchaImpl(packageName)
|
||||
callback.onPostInitCompleteWithConnectionInfo(
|
||||
CommonStatusCodes.SUCCESS,
|
||||
RecaptchaServiceImpl(this, packageName, lifecycle, imps),
|
||||
ConnectionInfo().apply {
|
||||
features = arrayOf(
|
||||
Feature("verify_with_recaptcha_v2_internal", 1),
|
||||
Feature("init", 3),
|
||||
Feature("execute", 5),
|
||||
Feature("close", 2)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class RecaptchaServiceImpl(
|
||||
private val context: Context,
|
||||
private val packageName: String,
|
||||
override val lifecycle: Lifecycle,
|
||||
private val imps: List<RecaptchaImpl>
|
||||
) : IRecaptchaService.Stub(), LifecycleOwner {
|
||||
|
||||
private var realRecaptchaImpl: RecaptchaImpl? = null
|
||||
|
||||
override fun verifyWithRecaptcha(callback: IExecuteCallback, siteKey: String, packageName: String) {
|
||||
Log.d(TAG, "Not yet implemented: verifyWithRecaptcha($siteKey, $packageName)")
|
||||
}
|
||||
|
||||
override fun init(callback: IInitCallback, siteKey: String) {
|
||||
init2(callback, InitParams().also {
|
||||
it.siteKey = siteKey
|
||||
it.version = LEGACY_VERSION
|
||||
})
|
||||
}
|
||||
|
||||
override fun execute(callback: IExecuteCallback, handle: RecaptchaHandle, action: RecaptchaAction) {
|
||||
execute2(callback, ExecuteParams().also {
|
||||
it.handle = handle
|
||||
it.action = action
|
||||
it.version = LEGACY_VERSION
|
||||
})
|
||||
}
|
||||
|
||||
override fun close(callback: ICloseCallback, handle: RecaptchaHandle) {
|
||||
lifecycleScope.launch {
|
||||
Log.d(TAG, "close($handle)")
|
||||
try {
|
||||
if (realRecaptchaImpl == null) {
|
||||
throw UnsupportedOperationException("Method <close> realRecaptchaImpl is null")
|
||||
}
|
||||
Log.d(TAG, "close realRecaptchaImpl:${realRecaptchaImpl}")
|
||||
callback.onClosed(Status.SUCCESS, realRecaptchaImpl!!.close(handle))
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun init2(callback: IInitCallback, params: InitParams) {
|
||||
lifecycleScope.launch {
|
||||
Log.d(TAG, "init($params)")
|
||||
try {
|
||||
Log.d(TAG, "imps size: ${imps.size}")
|
||||
for (recaptchaImpl in imps) {
|
||||
Log.d(TAG, "recaptchaImpl:${recaptchaImpl}")
|
||||
val recaptchaHandle = runCatching { recaptchaImpl.init(params) }.getOrNull() ?: continue
|
||||
realRecaptchaImpl = recaptchaImpl
|
||||
if (params.version == LEGACY_VERSION) {
|
||||
callback.onHandle(Status.SUCCESS, recaptchaHandle)
|
||||
} else {
|
||||
callback.onResults(Status.SUCCESS, InitResults().also { it.handle = recaptchaHandle })
|
||||
}
|
||||
Log.d(TAG, "realRecaptchaImpl:${realRecaptchaImpl}")
|
||||
return@launch
|
||||
}
|
||||
if (realRecaptchaImpl == null) {
|
||||
throw UnsupportedOperationException("Method <init2> realRecaptchaImpl is null")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, e)
|
||||
try {
|
||||
if (params.version == LEGACY_VERSION) {
|
||||
callback.onHandle(Status.INTERNAL_ERROR, null)
|
||||
} else {
|
||||
callback.onResults(Status.INTERNAL_ERROR, InitResults())
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// Ignored
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun execute2(callback: IExecuteCallback, params: ExecuteParams) {
|
||||
Log.d(TAG, "execute($params)")
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
if (realRecaptchaImpl == null) {
|
||||
throw UnsupportedOperationException("Method <execute2> realRecaptchaImpl is null")
|
||||
}
|
||||
Log.d(TAG, "execute2 realRecaptchaImpl:${realRecaptchaImpl}")
|
||||
val data = realRecaptchaImpl!!.execute(params)
|
||||
if (params.version == LEGACY_VERSION) {
|
||||
callback.onData(Status.SUCCESS, data)
|
||||
} else {
|
||||
callback.onResults(Status.SUCCESS, ExecuteResults().also { it.data = data })
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, e)
|
||||
try {
|
||||
if (params.version == LEGACY_VERSION) {
|
||||
callback.onData(Status.INTERNAL_ERROR, null)
|
||||
} else {
|
||||
callback.onResults(Status.INTERNAL_ERROR, ExecuteResults())
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// Ignored
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean =
|
||||
warnOnTransactionIssues(code, reply, flags, TAG) {
|
||||
super.onTransact(code, data, reply, flags)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val LEGACY_VERSION = "16.0.0"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,736 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 microG Project Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.microg.gms.recaptcha
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.media.AudioManager
|
||||
import android.os.BatteryManager
|
||||
import android.os.Handler
|
||||
import android.provider.Settings
|
||||
import android.text.format.DateFormat
|
||||
import android.util.Base64
|
||||
import android.util.Log
|
||||
import android.webkit.JavascriptInterface
|
||||
import android.webkit.WebResourceResponse
|
||||
import android.webkit.WebView
|
||||
import androidx.annotation.Keep
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.webkit.WebViewClientCompat
|
||||
import com.google.android.gms.recaptcha.RecaptchaHandle
|
||||
import com.google.android.gms.recaptcha.RecaptchaResultData
|
||||
import com.google.android.gms.recaptcha.internal.ExecuteParams
|
||||
import com.google.android.gms.recaptcha.internal.InitParams
|
||||
import com.google.android.gms.tasks.Task
|
||||
import com.google.android.gms.tasks.Tasks
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import okio.ByteString
|
||||
import org.microg.gms.profile.Build
|
||||
import org.microg.gms.profile.ProfileManager
|
||||
import org.microg.gms.tasks.TaskImpl
|
||||
import org.microg.gms.utils.toBase64
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.lang.reflect.Array
|
||||
import java.lang.reflect.Constructor
|
||||
import java.lang.reflect.Field
|
||||
import java.lang.reflect.Method
|
||||
import java.lang.reflect.Modifier
|
||||
import java.lang.reflect.Proxy
|
||||
import java.net.URLEncoder
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util.ArrayDeque
|
||||
import java.util.Locale
|
||||
import java.util.Queue
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import kotlin.coroutines.Continuation
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
private const val TAG = "RecaptchaWeb"
|
||||
|
||||
@RequiresApi(19)
|
||||
class RecaptchaWebImpl(private val context: Context, private val packageName: String, override val lifecycle: Lifecycle) : RecaptchaImpl, LifecycleOwner {
|
||||
private var webView: WebView? = null
|
||||
private var lastRequestToken: String? = null
|
||||
private var initFinished = AtomicBoolean(true)
|
||||
private var initContinuation: Continuation<Int>? = null
|
||||
private var executeFinished = AtomicBoolean(true)
|
||||
private var executeContinuation: Continuation<String>? = null
|
||||
|
||||
override suspend fun init(params: InitParams): RecaptchaHandle {
|
||||
lastRequestToken = UUID.randomUUID().toString()
|
||||
ProfileManager.ensureInitialized(context)
|
||||
FakeHandler.setDecryptKeyPrefix(IntArray(0))
|
||||
FakeApplication.context = context
|
||||
FakeApplication.packageNameOverride = packageName
|
||||
suspendCoroutine { continuation ->
|
||||
initFinished.set(false)
|
||||
initContinuation = continuation
|
||||
webView = WebView(context).apply {
|
||||
settings.javaScriptEnabled = true
|
||||
addJavascriptInterface(RNJavaScriptInterface(this@RecaptchaWebImpl, CodeInterpreter(this@RecaptchaWebImpl)), "RN")
|
||||
webViewClient = object : WebViewClientCompat() {
|
||||
fun String.isRecaptchaUrl() = startsWith("https://www.recaptcha.net/") || startsWith("https://www.gstatic.com/recaptcha/")
|
||||
|
||||
override fun shouldInterceptRequest(view: WebView, url: String): WebResourceResponse? {
|
||||
if (url.isRecaptchaUrl()) {
|
||||
return null
|
||||
}
|
||||
return WebResourceResponse("text/plain", "UTF-8", ByteArrayInputStream(byteArrayOf()))
|
||||
}
|
||||
|
||||
override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean {
|
||||
return !url.isRecaptchaUrl()
|
||||
}
|
||||
|
||||
override fun onPageFinished(view: WebView?, url: String?) {
|
||||
}
|
||||
}
|
||||
postUrl(
|
||||
MWV_URL, ("" +
|
||||
"k=${URLEncoder.encode(params.siteKey, "UTF-8")}&" +
|
||||
"pk=${URLEncoder.encode(packageName, "UTF-8")}&" +
|
||||
"mst=ANDROID_ONPLAY&" +
|
||||
"msv=18.1.1&" +
|
||||
"msi=${URLEncoder.encode(lastRequestToken, "UTF-8")}&" +
|
||||
"mov=${Build.VERSION.SDK_INT}"
|
||||
).toByteArray()
|
||||
)
|
||||
}
|
||||
lifecycleScope.launch {
|
||||
delay(10000)
|
||||
if (!initFinished.getAndSet(true)) {
|
||||
try {
|
||||
continuation.resumeWithException(RuntimeException("Timeout reached"))
|
||||
} catch (_: Exception) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
initContinuation = null
|
||||
return RecaptchaHandle(params.siteKey, packageName, emptyList())
|
||||
}
|
||||
|
||||
override suspend fun execute(params: ExecuteParams): RecaptchaResultData {
|
||||
if (webView == null) {
|
||||
init(InitParams().apply { siteKey = params.handle.siteKey; version = params.version })
|
||||
}
|
||||
val additionalArgs = mutableMapOf<String, String>()
|
||||
for (key in params.action.additionalArgs.keySet()) {
|
||||
additionalArgs[key] = params.action.additionalArgs.getString(key)!!
|
||||
}
|
||||
val request = RecaptchaExecuteRequest(token = lastRequestToken, action = params.action.toString(), additionalArgs = additionalArgs).encode().toBase64(Base64.URL_SAFE, Base64.NO_WRAP)
|
||||
val token = suspendCoroutine { continuation ->
|
||||
executeFinished.set(false)
|
||||
executeContinuation = continuation
|
||||
eval("recaptcha.m.Main.execute(\"${request}\")")
|
||||
lifecycleScope.launch {
|
||||
delay(10000)
|
||||
if (!executeFinished.getAndSet(true)) {
|
||||
try {
|
||||
continuation.resumeWithException(RuntimeException("Timeout reached"))
|
||||
} catch (_: Exception) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
return RecaptchaResultData(token)
|
||||
}
|
||||
|
||||
override suspend fun close(handle: RecaptchaHandle): Boolean {
|
||||
if (handle.clientPackageName != null && handle.clientPackageName != packageName) throw IllegalArgumentException("invalid handle")
|
||||
val closed = webView != null
|
||||
webView?.stopLoading()
|
||||
webView?.loadUrl("about:blank")
|
||||
webView = null
|
||||
return closed
|
||||
}
|
||||
|
||||
private fun eval(script: String) {
|
||||
Log.d(TAG, "eval: $script")
|
||||
webView?.let {
|
||||
Handler(context.mainLooper).post {
|
||||
it.evaluateJavascript(script, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected fun finalize() {
|
||||
FakeApplication.packageNameOverride = ""
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val MWV_URL = "https://www.recaptcha.net/recaptcha/api3/mwv"
|
||||
private const val DEBUG = false
|
||||
object FakeApplication : Application() {
|
||||
var context: Context
|
||||
get() = baseContext
|
||||
set(value) { try { attachBaseContext(value.applicationContext) } catch (_: Exception) { } }
|
||||
var packageNameOverride: String = ""
|
||||
override fun getPackageName(): String {
|
||||
return packageNameOverride
|
||||
}
|
||||
}
|
||||
var codeDecryptKeyPrefix = emptyList<Int>()
|
||||
private set
|
||||
|
||||
class FakeHandler : Exception() {
|
||||
private var cloudProjectNumber: Long? = 0
|
||||
private var nonce: String? = null
|
||||
|
||||
@Keep
|
||||
fun requestIntegrityToken(request: FakeHandler): Task<FakeHandler> {
|
||||
return Tasks.forException(FakeHandler())
|
||||
}
|
||||
|
||||
@Keep
|
||||
fun setCloudProjectNumber(cloudProjectNumber: Long): FakeHandler {
|
||||
this.cloudProjectNumber = cloudProjectNumber
|
||||
return this
|
||||
}
|
||||
|
||||
@Keep
|
||||
fun setNonce(nonce: String): FakeHandler {
|
||||
this.nonce = nonce
|
||||
return this
|
||||
}
|
||||
|
||||
@Keep
|
||||
fun build(): FakeHandler {
|
||||
return this
|
||||
}
|
||||
|
||||
@Keep
|
||||
fun cloudProjectNumber(): Long? {
|
||||
return cloudProjectNumber
|
||||
}
|
||||
|
||||
@Keep
|
||||
fun nonce(): String? {
|
||||
return nonce
|
||||
}
|
||||
|
||||
@Keep
|
||||
fun getErrorCode(): Int = -1
|
||||
|
||||
companion object {
|
||||
@Keep
|
||||
@JvmStatic
|
||||
fun setDecryptKeyPrefix(newKeyPrefix: IntArray) {
|
||||
codeDecryptKeyPrefix = newKeyPrefix.asList()
|
||||
}
|
||||
|
||||
@Keep
|
||||
@JvmStatic
|
||||
fun getFakeApplication(): Application = FakeApplication
|
||||
|
||||
@Keep
|
||||
@JvmStatic
|
||||
fun createFakeIntegrityManager(context: Context): FakeHandler {
|
||||
return FakeHandler()
|
||||
}
|
||||
@Keep
|
||||
@JvmStatic
|
||||
fun createFakeIntegrityTokenRequestBuilder(): FakeHandler {
|
||||
return FakeHandler()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class CodeInterpreter(private val impl: RecaptchaWebImpl) {
|
||||
val dict = mutableMapOf<Int, Any?>()
|
||||
var errorHandler = ""
|
||||
var xorSecret = IntRange(0, 127).random().toByte()
|
||||
|
||||
private val intToClassMap = mapOf(
|
||||
1 to java.lang.Integer.TYPE,
|
||||
2 to java.lang.Short.TYPE,
|
||||
3 to java.lang.Byte.TYPE,
|
||||
4 to java.lang.Long.TYPE,
|
||||
5 to java.lang.Character.TYPE,
|
||||
6 to java.lang.Float.TYPE,
|
||||
7 to java.lang.Double.TYPE,
|
||||
8 to java.lang.Boolean.TYPE,
|
||||
9 to FakeHandler::class.java
|
||||
)
|
||||
|
||||
private fun getClass(name: String): Class<*>? = when (name) {
|
||||
"[I" -> IntArray::class.java
|
||||
"[B" -> ByteArray::class.java
|
||||
"android.os.Build" -> Build::class.java
|
||||
"android.os.Build\$VERSION" -> Build.VERSION::class.java
|
||||
"android.app.ActivityThread" -> FakeHandler::class.java
|
||||
"com.google.android.play.core.integrity.IntegrityManager" -> FakeHandler::class.java
|
||||
"com.google.android.play.core.integrity.IntegrityManagerFactory" -> FakeHandler::class.java
|
||||
"com.google.android.play.core.integrity.IntegrityTokenRequest" -> FakeHandler::class.java
|
||||
"com.google.android.play.core.integrity.IntegrityTokenResponse" -> FakeHandler::class.java
|
||||
"android.content.Intent", "android.content.IntentFilter", "android.content.BroadcastReceiver",
|
||||
"android.content.Context", "android.content.pm.PackageManager", "android.content.ContentResolver",
|
||||
"java.lang.String", "java.lang.CharSequence", "java.lang.Long",
|
||||
"java.nio.charset.Charset", "java.nio.charset.StandardCharsets",
|
||||
"android.text.format.DateFormat", "java.util.Date", "java.util.Locale", "java.nio.ByteBuffer",
|
||||
"android.os.BatteryManager", "android.media.AudioManager",
|
||||
"com.google.android.gms.tasks.OnCompleteListener",
|
||||
"android.provider.Settings\$System" -> Class.forName(name)
|
||||
|
||||
else -> {
|
||||
Log.w(TAG, "Not providing class $name", Exception())
|
||||
if (DEBUG) Class.forName(name) else null
|
||||
}
|
||||
}
|
||||
|
||||
private fun getMethod(cls: Class<*>, name: String, params: kotlin.Array<Class<*>?>): Method? = when {
|
||||
cls == FakeHandler::class.java && name == "acx" -> FakeHandler::class.java.getMethod("setDecryptKeyPrefix", *params)
|
||||
cls == FakeHandler::class.java && name == "currentApplication" -> FakeHandler::class.java.getMethod("getFakeApplication", *params)
|
||||
cls == FakeHandler::class.java && name == "create" -> FakeHandler::class.java.getMethod("createFakeIntegrityManager", *params)
|
||||
cls == FakeHandler::class.java && name == "builder" -> FakeHandler::class.java.getMethod("createFakeIntegrityTokenRequestBuilder", *params)
|
||||
cls == FakeHandler::class.java -> cls.getMethod(name, *params)
|
||||
cls == FakeApplication.javaClass && name == "getContentResolver" -> cls.getMethod(name, *params)
|
||||
cls == FakeApplication.javaClass && name == "getSystemService" -> cls.getMethod(name, *params)
|
||||
cls == FakeApplication.javaClass && name == "registerReceiver" -> cls.getMethod(name, *params)
|
||||
cls == PackageManager::class.java && name == "checkPermission" -> cls.getMethod(name, *params)
|
||||
cls == Context::class.java && name == "checkSelfPermission" -> cls.getMethod(name, *params)
|
||||
cls == Context::class.java && name == "getPackageManager" -> cls.getMethod(name, *params)
|
||||
cls == Context::class.java && name == "getPackageName" -> cls.getMethod(name, *params)
|
||||
cls == AudioManager::class.java && name == "getStreamVolume" -> cls.getMethod(name, *params)
|
||||
cls == Settings.System::class.java && name == "getInt" -> cls.getMethod(name, *params)
|
||||
cls == DateFormat::class.java -> cls.getMethod(name, *params)
|
||||
cls == Locale::class.java -> cls.getMethod(name, *params)
|
||||
cls == Intent::class.java -> cls.getMethod(name, *params)
|
||||
cls == String::class.java -> cls.getMethod(name, *params)
|
||||
cls == ByteBuffer::class.java -> cls.getMethod(name, *params)
|
||||
cls == TaskImpl::class.java -> cls.getMethod(name, *params)
|
||||
name == "toString" -> cls.getMethod(name, *params)
|
||||
name == "parseLong" -> cls.getMethod(name, *params)
|
||||
else -> {
|
||||
Log.w(TAG, "Not providing method $name in ${cls.display()}", Exception())
|
||||
if (DEBUG) cls.getMethod(name, *params) else null
|
||||
}
|
||||
}
|
||||
|
||||
private fun getField(cls: Class<*>, name: String): Field? = when {
|
||||
cls == Build::class.java -> cls.getField(name)
|
||||
cls == Build.VERSION::class.java -> cls.getField(name)
|
||||
cls == Settings.System::class.java && cls.getField(name).modifiers.and(Modifier.STATIC) > 0 -> cls.getField(name)
|
||||
cls == BatteryManager::class.java && cls.getField(name).modifiers.and(Modifier.STATIC) > 0 -> cls.getField(name)
|
||||
cls == AudioManager::class.java && cls.getField(name).modifiers.and(Modifier.STATIC) > 0 -> cls.getField(name)
|
||||
cls == StandardCharsets::class.java && cls.getField(name).modifiers.and(Modifier.STATIC) > 0 -> cls.getField(name)
|
||||
else -> {
|
||||
Log.w(TAG, "Not providing field $name in ${cls.display()}", Exception())
|
||||
if (DEBUG) cls.getField(name) else null
|
||||
}
|
||||
}
|
||||
|
||||
private operator fun Any?.rem(other: Any?): Any? = when {
|
||||
this is IntArray && other is Int -> map { it % other }.toIntArray()
|
||||
else -> throw UnsupportedOperationException("rem ${this?.javaClass} % ${other?.javaClass}")
|
||||
}
|
||||
|
||||
private infix fun Any?.xor(other: Any?): Any? = when {
|
||||
this is String && other is Int -> map { it.code xor other }.toIntArray()
|
||||
this is String && other is Byte -> encodeToByteArray().map { (it.toInt() xor other.toInt()).toByte() }.toByteArray()
|
||||
this is Long && other is Long -> this xor other
|
||||
else -> throw UnsupportedOperationException("xor ${this?.javaClass} ^ ${other?.javaClass}")
|
||||
}
|
||||
|
||||
private fun Any?.join(): Any? = when (this) {
|
||||
is ByteArray -> decodeToString()
|
||||
is CharArray -> concatToString()
|
||||
is IntArray -> joinToString(",", "[", "]")
|
||||
is LongArray -> joinToString(",", "[", "]")
|
||||
is ShortArray -> joinToString(",", "[", "]")
|
||||
is FloatArray -> joinToString(",", "[", "]")
|
||||
is DoubleArray -> joinToString(",", "[", "]")
|
||||
is kotlin.Array<*> -> joinToString(",", "[", "]")
|
||||
is Iterable<*> -> joinToString(",", "[", "]")
|
||||
else -> this
|
||||
}
|
||||
|
||||
private fun String.deXor(): String = map { Char(it.code xor xorSecret.toInt()) }.toCharArray().concatToString()
|
||||
|
||||
private fun Any?.deXor(): Any? = when {
|
||||
this is RecaptchaWebCode.Arg && this.asObject() is String -> this.asObject()!!.deXor()
|
||||
this is String -> this.deXor()
|
||||
else -> this
|
||||
}
|
||||
|
||||
private fun Any.asClass(): Class<*>? = when (this) {
|
||||
is RecaptchaWebCode.Arg -> asObject()!!.asClass()
|
||||
is Int -> intToClassMap[this]!!
|
||||
is String -> getClass(this)
|
||||
is Class<*> -> this
|
||||
else -> throw UnsupportedOperationException("$this.asClass()")
|
||||
}
|
||||
|
||||
private fun Any?.getClass(): Class<*> = when (this) {
|
||||
is RecaptchaWebCode.Arg -> asObject().getClass()
|
||||
is Class<*> -> this
|
||||
null -> Unit.javaClass
|
||||
else -> this.javaClass
|
||||
}
|
||||
|
||||
private fun Any?.display(): String = when (this) {
|
||||
is RecaptchaWebCode.Arg -> asObject().display() + if (index != null) " (d[$index])" else ""
|
||||
is Int, is Boolean -> "${this}"
|
||||
is Byte -> "${this}b"
|
||||
is Short -> "${this}s"
|
||||
is Long -> "${this}l"
|
||||
is Double -> "${this}d"
|
||||
is Float -> "${this}f"
|
||||
is String -> if (any { !it.isLetterOrDigit() && it !in listOf('.', '=', '-', '_') }) "<string with complex chars>" else "\"${this}\""
|
||||
is Class<*> -> name
|
||||
is Constructor<*> -> "{new ${declaringClass.name}(${parameterTypes.joinToString { it.name }})}"
|
||||
is Method -> "{${declaringClass.name}.$name(${parameterTypes.joinToString { it.name }})}"
|
||||
is Field -> "{${declaringClass.name}.$name}"
|
||||
is IntArray -> joinToString(prefix = "[", postfix = "]")
|
||||
is ByteArray -> joinToString(prefix = "[", postfix = "]b")
|
||||
is ShortArray -> joinToString(prefix = "[", postfix = "]s")
|
||||
is LongArray -> joinToString(prefix = "[", postfix = "]l")
|
||||
is FloatArray -> joinToString(prefix = "[", postfix = "]f")
|
||||
is DoubleArray -> joinToString(prefix = "[", postfix = "]d")
|
||||
is BooleanArray -> joinToString(prefix = "[", postfix = "]")
|
||||
null -> "null"
|
||||
else -> "@{${this.javaClass.name}}"
|
||||
}
|
||||
|
||||
private fun RecaptchaWebCode.Arg.asObject(): Any? = when {
|
||||
index != null -> dict[index]
|
||||
bol != null -> bol
|
||||
bt != null -> bt[0]
|
||||
chr != null -> chr[0]
|
||||
sht != null -> sht.toShort()
|
||||
i != null -> i
|
||||
l != null -> l
|
||||
flt != null -> flt
|
||||
dbl != null -> dbl
|
||||
str != null -> str
|
||||
else -> null
|
||||
}
|
||||
|
||||
private fun Any.asListValue(): RecaptchaWebList.Value = when(this) {
|
||||
is Int -> RecaptchaWebList.Value(i = this)
|
||||
is Short -> RecaptchaWebList.Value(sht = this.toInt())
|
||||
is Byte -> RecaptchaWebList.Value(bt = ByteString.of(this))
|
||||
is Long -> RecaptchaWebList.Value(l = this)
|
||||
is Double -> RecaptchaWebList.Value(dbl = this)
|
||||
is Float -> RecaptchaWebList.Value(flt = this)
|
||||
is Boolean -> RecaptchaWebList.Value(bol = this)
|
||||
is Char -> RecaptchaWebList.Value(chr = this.toString())
|
||||
is String -> RecaptchaWebList.Value(str = this)
|
||||
else -> RecaptchaWebList.Value(str = toString())
|
||||
}
|
||||
|
||||
fun execute(code: RecaptchaWebCode) {
|
||||
for (op in code.ops) {
|
||||
when (op.code) {
|
||||
1 -> {
|
||||
// d[i] = a0
|
||||
if (DEBUG) Log.d(TAG, "d[${op.arg1}] = ${op.args[0].display()}")
|
||||
dict[op.arg1!!] = op.args[0].asObject()
|
||||
}
|
||||
|
||||
2 -> {
|
||||
// d[i] = a0 .. a1
|
||||
if (DEBUG) Log.d(TAG, "d[${op.arg1}] = \"${op.args[0].display()}${op.args[1].display()}\"")
|
||||
dict[op.arg1!!] = "${op.args[0].asObject()}${op.args[1].asObject()}"
|
||||
}
|
||||
|
||||
3 -> {
|
||||
// d[i] = Class(a0)
|
||||
val cls = op.args[0].asObject().deXor()?.asClass()
|
||||
if (DEBUG) Log.d(TAG, "d[${op.arg1}] = $cls")
|
||||
dict[op.arg1!!] = cls
|
||||
}
|
||||
|
||||
4 -> {
|
||||
// d[i] = Class(a0).getConstructor(a1 ...)
|
||||
val constructor = op.args[0].asClass()!!.getConstructor(*op.args.subList(1, op.args.size).map { it.asClass() }.toTypedArray())
|
||||
if (DEBUG) Log.d(TAG, "d[${op.arg1}] = ${constructor.display()}")
|
||||
dict[op.arg1!!] = constructor
|
||||
}
|
||||
|
||||
5 -> {
|
||||
// d[i] = Class(a0).getMethod(a1, a2 ...)
|
||||
val methodName = (op.args[1].asObject().deXor() as String)
|
||||
val cls = op.args[0].getClass()
|
||||
val method = getMethod(cls, methodName, op.args.subList(2, op.args.size).map { it.asClass() }.toTypedArray())
|
||||
if (DEBUG) Log.d(TAG, "d[${op.arg1}] = ${method.display()}")
|
||||
dict[op.arg1!!] = method
|
||||
}
|
||||
|
||||
6 -> {
|
||||
// d[i] = Class(a0).getField(a1)
|
||||
val fieldName = (op.args[1].asObject().deXor() as String)
|
||||
val cls = op.args[0].getClass()
|
||||
val field = getField(cls, fieldName)
|
||||
if (DEBUG) Log.d(TAG, "d[${op.arg1}] = ${field.display()}")
|
||||
dict[op.arg1!!] = field
|
||||
}
|
||||
|
||||
7 -> {
|
||||
// d[i] = Constructor(a0).newInstance(a1 ...)
|
||||
if (DEBUG) Log.d(
|
||||
TAG,
|
||||
"d[${op.arg1}] = new ${(op.args[0].asObject() as Constructor<*>).name}(${
|
||||
op.args.subList(1, op.args.size).joinToString { it.display() }
|
||||
})"
|
||||
)
|
||||
dict[op.arg1!!] =
|
||||
(op.args[0].asObject() as Constructor<*>).newInstance(*op.args.subList(1, op.args.size).map { it.asObject() }.toTypedArray())
|
||||
}
|
||||
|
||||
8 -> {
|
||||
// d[i] = Method(a0).invoke(a1, a2 ...)
|
||||
if (DEBUG) Log.d(
|
||||
TAG,
|
||||
"d[${op.arg1}] = (${op.args[1].display()}).${(op.args[0].asObject() as Method).name}(${
|
||||
op.args.subList(2, op.args.size).joinToString { it.display() }
|
||||
})"
|
||||
)
|
||||
dict[op.arg1!!] = (op.args[0].asObject() as Method).invoke(
|
||||
op.args[1].asObject(),
|
||||
*op.args.subList(2, op.args.size).map { it.asObject() }.toTypedArray()
|
||||
)
|
||||
}
|
||||
|
||||
9 -> {
|
||||
// d[i] = Method(a0).invoke(null, a1 ...)
|
||||
if (DEBUG) Log.d(
|
||||
TAG,
|
||||
"d[${op.arg1}] = ${(op.args[0].asObject() as Method).declaringClass.name}.${(op.args[0].asObject() as Method).name}(${
|
||||
op.args.subList(
|
||||
1,
|
||||
op.args.size
|
||||
).joinToString { it.display() }
|
||||
})"
|
||||
)
|
||||
dict[op.arg1!!] =
|
||||
(op.args[0].asObject() as Method).invoke(null, *op.args.subList(1, op.args.size).map { it.asObject() }.toTypedArray())
|
||||
}
|
||||
|
||||
10 -> {
|
||||
// d[i] = Field(a0).get(a1)
|
||||
if (DEBUG) Log.d(TAG, "d[${op.arg1}] = (${op.args[1].display()}).${(op.args[0].asObject() as Field).name}")
|
||||
dict[op.arg1!!] = (op.args[0].asObject() as Field).get(op.args[1].asObject())
|
||||
}
|
||||
|
||||
11 -> {
|
||||
// d[i] = Field(a0).get(null)
|
||||
if (DEBUG) Log.d(TAG, "d[${op.arg1}] = ${(op.args[0].asObject() as Field).declaringClass.name}.${(op.args[0].asObject() as Field).name}")
|
||||
dict[op.arg1!!] = (op.args[0].asObject() as Field).get(null)
|
||||
}
|
||||
|
||||
12 -> {
|
||||
// Field(a0).set(a1, a2)
|
||||
if (DEBUG) Log.d(TAG, "(${op.args[1].display()}).${(op.args[0].asObject() as Field).name} = ${op.args[2].display()}")
|
||||
(op.args[0].asObject() as Field).set(op.args[1].asObject(), op.args[2].asObject())
|
||||
}
|
||||
|
||||
13 -> {
|
||||
// Field(a0).set(null, a1)
|
||||
if (DEBUG) Log.d(
|
||||
TAG,
|
||||
"(${(op.args[0].asObject() as Field).declaringClass.name}).${(op.args[0].asObject() as Field).name} = ${op.args[1].display()}"
|
||||
)
|
||||
(op.args[0].asObject() as Field).set(null, op.args[1].asObject())
|
||||
}
|
||||
|
||||
15 -> {
|
||||
// eval(a0(a1))
|
||||
impl.eval("${op.args[0].str}(\"${op.args[1].asObject()}\")")
|
||||
}
|
||||
|
||||
17 -> {
|
||||
// d[i] = new a0[a1]
|
||||
if (DEBUG) Log.d(TAG, "d[${op.arg1}] = new ${op.args[0].asClass()!!.name}[${op.args[1].display()}]")
|
||||
dict[op.arg1!!] = Array.newInstance(op.args[0].asClass(), op.args[1].asObject() as Int)
|
||||
}
|
||||
|
||||
18 -> {
|
||||
// d[i] = new a1() { * a2(args) { eval(a0(args)); return a3; } }
|
||||
val callbackName = op.args[0].asObject() as String
|
||||
val methodName = (op.args[2].asObject() as String).deXor()
|
||||
val cls = op.args[1].asObject().deXor()?.asClass()
|
||||
val returnValue = op.args[3].asObject()
|
||||
val argsTarget = (if (op.args.size == 5) op.args[4].asObject() as? Int else null) ?: -1
|
||||
if (DEBUG) Log.d(TAG, "d[${op.arg1}] = new ${cls?.name}() { * ${methodName}(*) { js:$callbackName(*); return ${returnValue.display()}; } }")
|
||||
dict[op.arg1!!] =
|
||||
Proxy.newProxyInstance(cls!!.classLoader, arrayOf(cls)) { obj: Any, method: Method, args: kotlin.Array<Any>? ->
|
||||
if (method.name == methodName) {
|
||||
if (argsTarget != -1) dict[argsTarget] = args
|
||||
val encoded = RecaptchaWebList(args.orEmpty().map { it.asListValue() }).encode().toBase64(Base64.URL_SAFE, Base64.NO_WRAP)
|
||||
impl.eval("${callbackName}(\"$encoded\")")
|
||||
returnValue
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
19 -> {
|
||||
// d[i] = new Queue(a1)
|
||||
// d[a0] = new a2() { * a3(args) { d[i].add(args); return a4; } }
|
||||
val methodName = (op.args[3].asObject() as String).deXor()
|
||||
val maxSize = op.args[1].asObject() as Int
|
||||
val queue = ArrayDeque<List<Any>>(maxSize)
|
||||
val limitedQueue = object : Queue<List<Any>> by queue {
|
||||
override fun add(element: List<Any>?): Boolean {
|
||||
if (maxSize == 0) return true
|
||||
if (size == maxSize) remove()
|
||||
queue.add(element)
|
||||
return true
|
||||
}
|
||||
}
|
||||
val returnValue = if (op.args.size == 5) op.args[4].asObject() else null
|
||||
val cls = op.args[2].asObject().deXor()?.asClass()
|
||||
dict[op.arg1!!] = limitedQueue
|
||||
dict[op.args[0].asObject() as Int] = Proxy.newProxyInstance(cls!!.classLoader, arrayOf(cls)) { obj: Any, method: Method, args: kotlin.Array<Any>? ->
|
||||
if (method.name == methodName) {
|
||||
limitedQueue.add(args?.asList().orEmpty())
|
||||
returnValue
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
20 -> {
|
||||
// unset(d, a0 ...)
|
||||
if (DEBUG) Log.d(TAG, "d[${op.args.joinToString { it.index.toString() }}] = @@@")
|
||||
for (arg in op.args) {
|
||||
dict.remove(arg.index)
|
||||
}
|
||||
}
|
||||
|
||||
26 -> {
|
||||
// e = a0
|
||||
errorHandler = op.args[0].str!!
|
||||
}
|
||||
|
||||
27 -> {
|
||||
// clear(d)
|
||||
dict.clear()
|
||||
}
|
||||
|
||||
30 -> {
|
||||
// d[i] = encode(a0 ...)
|
||||
val res = RecaptchaWebList(op.args.map { it.asObject()!!.asListValue() }).encode().toBase64(Base64.URL_SAFE, Base64.NO_WRAP)
|
||||
if (DEBUG) Log.d(TAG, "d[${op.arg1}] = ${res.display()}")
|
||||
dict[op.arg1!!] = res
|
||||
}
|
||||
|
||||
31 -> {
|
||||
// a0[a1] = a2
|
||||
if (DEBUG) Log.d(TAG, "d[${op.args[0].index}][${op.args[1].display()}] = ${op.args[2].display()}")
|
||||
Array.set(op.args[0].asObject()!!, op.args[1].asObject() as Int, op.args[2].asObject())
|
||||
}
|
||||
|
||||
32 -> {
|
||||
// d[i] = a0[a1]
|
||||
if (DEBUG) Log.d(TAG, "d[${op.arg1}] = ${op.args[0].display()}[${op.args[1].display()}]")
|
||||
val arr = op.args[0].asObject()
|
||||
val idx = op.args[1].asObject() as Int
|
||||
val res = when (arr) {
|
||||
is String -> arr[idx]
|
||||
is List<*> -> arr[idx]
|
||||
else -> Array.get(arr, idx)
|
||||
}
|
||||
dict[op.arg1!!] = res
|
||||
}
|
||||
|
||||
34 -> {
|
||||
// d[i] = a0 % a1
|
||||
if (DEBUG) Log.d(TAG, "d[${op.arg1}] = ${op.args[0].display()} % ${op.args[1].display()}")
|
||||
dict[op.arg1!!] = op.args[0].asObject() % op.args[1].asObject()
|
||||
}
|
||||
|
||||
35 -> {
|
||||
// d[i] = a0 ^ a1
|
||||
if (DEBUG) Log.d(TAG, "d[${op.arg1}] = ${op.args[0].display()} ^ ${op.args[1].display()}")
|
||||
dict[op.arg1!!] = op.args[0].asObject() xor op.args[1].asObject()
|
||||
}
|
||||
|
||||
37 -> {
|
||||
// d[i] = String(a1[*a0])
|
||||
val str = op.args[1].asObject() as String
|
||||
val res = (op.args[0].asObject() as IntArray).map { str[it] }.toCharArray().concatToString()
|
||||
if (DEBUG) Log.d(TAG, "d[${op.arg1}] = ${res.display()}")
|
||||
dict[op.arg1!!] = res
|
||||
}
|
||||
|
||||
38 -> {
|
||||
// x = a0
|
||||
xorSecret = op.args[0].asObject() as Byte
|
||||
}
|
||||
|
||||
39 -> {
|
||||
// d[i] = join(a0)
|
||||
val res = op.args[0].asObject().join()
|
||||
if (DEBUG) Log.d(TAG, "d[${op.arg1}] = ${res.display()}")
|
||||
dict[op.arg1!!] = res
|
||||
}
|
||||
|
||||
else -> {
|
||||
Log.w(TAG, "Op ${op.encode().toBase64(Base64.URL_SAFE, Base64.NO_WRAP)} not implemented (code=${op.code})")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class RNJavaScriptInterface(private val impl: RecaptchaWebImpl, private val interpreter: CodeInterpreter) {
|
||||
|
||||
@JavascriptInterface
|
||||
fun zzoed(input: String) {
|
||||
val result = RecaptchaWebResult.ADAPTER.decode(Base64.decode(input, Base64.URL_SAFE))
|
||||
if (DEBUG) Log.d(TAG, "zzoed: $result")
|
||||
if (!impl.executeFinished.getAndSet(true) && impl.lastRequestToken == result.requestToken) {
|
||||
if (result.code == 1 && result.token != null) {
|
||||
impl.executeContinuation?.resume(result.token)
|
||||
} else {
|
||||
impl.executeContinuation?.resumeWithException(RuntimeException("Status ${result.code}"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
fun zzoid(input: String) {
|
||||
val status = RecaptchaWebStatusCode.ADAPTER.decode(Base64.decode(input, Base64.URL_SAFE))
|
||||
if (DEBUG) Log.d(TAG, "zzoid: $status")
|
||||
if (!impl.initFinished.getAndSet(true)) {
|
||||
if (status.code != null) {
|
||||
impl.initContinuation?.resume(status.code)
|
||||
} else {
|
||||
impl.initContinuation?.resumeWithException(RuntimeException("Status is null"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
fun zzrp(input: String) {
|
||||
val callback = RecaptchaWebEncryptedCallback.ADAPTER.decode(Base64.decode(input, Base64.URL_SAFE))
|
||||
var key = (codeDecryptKeyPrefix + callback.key).reduce { a, b -> a xor b }
|
||||
fun next(): Int {
|
||||
key = ((key * 4391) + 277) % 32779
|
||||
return key % 255
|
||||
}
|
||||
|
||||
val decrypted = callback.data_?.map { Char(it.code xor next()) }?.toCharArray()?.concatToString()
|
||||
if (DEBUG) Log.d(TAG, "zzrp: $decrypted")
|
||||
val code = RecaptchaWebCode.ADAPTER.decode(Base64.decode(decrypted, Base64.URL_SAFE + Base64.NO_PADDING))
|
||||
interpreter.execute(code)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
90
play-services-recaptcha/core/src/main/proto/recaptcha.proto
Normal file
90
play-services-recaptcha/core/src/main/proto/recaptcha.proto
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
syntax = "proto3";
|
||||
|
||||
option java_package = "org.microg.gms.recaptcha";
|
||||
|
||||
message RecaptchaInitRequest {
|
||||
message Data {
|
||||
optional string siteKey = 1;
|
||||
optional string packageName = 2;
|
||||
optional string version = 3;
|
||||
}
|
||||
optional Data data = 2;
|
||||
}
|
||||
|
||||
message RecaptchaInitResponse {
|
||||
optional string token = 1;
|
||||
repeated string acceptableAdditionalArgs = 3;
|
||||
}
|
||||
|
||||
message RecaptchaExecuteRequest {
|
||||
optional string token = 1;
|
||||
optional string action = 2;
|
||||
optional uint64 timestamp = 3;
|
||||
optional string dg = 4;
|
||||
map<string, string> additionalArgs = 5;
|
||||
optional string verificationHistoryToken = 6;
|
||||
// map<int32, bytes> unknown = 7;
|
||||
}
|
||||
|
||||
message RecaptchaExecuteResponse {
|
||||
optional string token = 1;
|
||||
}
|
||||
|
||||
message RecaptchaWebEncryptedCallback {
|
||||
optional string data = 1;
|
||||
repeated int32 key = 2;
|
||||
}
|
||||
|
||||
message RecaptchaWebInvokeMultiParameter {
|
||||
repeated string args = 1;
|
||||
}
|
||||
|
||||
message RecaptchaWebStatusCode {
|
||||
optional int32 code = 1;
|
||||
}
|
||||
|
||||
message RecaptchaWebResult {
|
||||
optional string requestToken = 1;
|
||||
optional string token = 2;
|
||||
optional int32 code = 3;
|
||||
}
|
||||
|
||||
message RecaptchaWebList {
|
||||
message Value {
|
||||
oneof typed {
|
||||
bool bol = 1;
|
||||
bytes bt = 2;
|
||||
string chr = 3;
|
||||
sint32 sht = 4;
|
||||
sint32 i = 5;
|
||||
sint64 l = 7;
|
||||
float flt = 9;
|
||||
double dbl = 10;
|
||||
string str = 11;
|
||||
}
|
||||
}
|
||||
repeated Value values = 1;
|
||||
}
|
||||
|
||||
message RecaptchaWebCode {
|
||||
message Arg {
|
||||
oneof typed {
|
||||
int32 index = 1;
|
||||
bool bol = 2;
|
||||
bytes bt = 3;
|
||||
string chr = 4;
|
||||
sint32 sht = 5;
|
||||
sint32 i = 6;
|
||||
sint64 l = 8;
|
||||
float flt = 10;
|
||||
double dbl = 11;
|
||||
string str = 12;
|
||||
}
|
||||
}
|
||||
message Op {
|
||||
optional int32 code = 1;
|
||||
optional int32 arg1 = 2;
|
||||
repeated Arg args = 3;
|
||||
}
|
||||
repeated Op ops = 1;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue