Repo Created

This commit is contained in:
Fr4nz D13trich 2025-11-15 17:44:12 +01:00
parent eb305e2886
commit a8c22c65db
4784 changed files with 329907 additions and 2 deletions

View file

@ -0,0 +1,41 @@
/*
* SPDX-FileCopyrightText: 2022 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
apply plugin: 'com.android.library'
apply plugin: 'maven-publish'
apply plugin: 'signing'
android {
namespace "com.google.android.gms.recaptcha"
compileSdkVersion androidCompileSdk
buildToolsVersion "$androidBuildVersionTools"
buildFeatures {
aidl = true
}
defaultConfig {
versionName version
minSdkVersion androidMinSdk
targetSdkVersion androidTargetSdk
}
compileOptions {
sourceCompatibility = 1.8
targetCompatibility = 1.8
}
}
apply from: '../gradle/publish-android.gradle'
description = 'microG implementation of play-services-recaptcha'
dependencies {
// Dependencies from play-services-recaptcha:17.0.1
api project(':play-services-base')
api project(':play-services-basement')
api project(':play-services-tasks')
}

View 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'

View 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>

View file

@ -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()
}
}

View file

@ -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()
}
}
}

View file

@ -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"
}
}

View file

@ -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)
}
}
}
}

View 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;
}

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ SPDX-FileCopyrightText: 2022, microG Project Team
~ SPDX-License-Identifier: Apache-2.0
-->
<manifest />

View file

@ -0,0 +1,3 @@
package com.google.android.gms.recaptcha;
parcelable RecaptchaAction;

View file

@ -0,0 +1,3 @@
package com.google.android.gms.recaptcha;
parcelable RecaptchaHandle;

View file

@ -0,0 +1,3 @@
package com.google.android.gms.recaptcha;
parcelable RecaptchaResultData;

View file

@ -0,0 +1,3 @@
package com.google.android.gms.recaptcha.internal;
parcelable ExecuteParams;

View file

@ -0,0 +1,3 @@
package com.google.android.gms.recaptcha.internal;
parcelable ExecuteResults;

View file

@ -0,0 +1,7 @@
package com.google.android.gms.recaptcha.internal;
import com.google.android.gms.common.api.Status;
interface ICloseCallback {
oneway void onClosed(in Status status, boolean closed) = 0;
}

View file

@ -0,0 +1,11 @@
package com.google.android.gms.recaptcha.internal;
import com.google.android.gms.common.api.Status;
import com.google.android.gms.recaptcha.RecaptchaResultData;
import com.google.android.gms.recaptcha.internal.ExecuteResults;
interface IExecuteCallback {
oneway void onData(in Status status, in RecaptchaResultData data) = 0;
oneway void onResults(in Status status, in ExecuteResults results) = 1;
}

View file

@ -0,0 +1,11 @@
package com.google.android.gms.recaptcha.internal;
import com.google.android.gms.common.api.Status;
import com.google.android.gms.recaptcha.RecaptchaHandle;
import com.google.android.gms.recaptcha.internal.InitResults;
interface IInitCallback {
oneway void onHandle(in Status status, in RecaptchaHandle handle) = 0;
oneway void onResults(in Status status, in InitResults results) = 1;
}

View file

@ -0,0 +1,20 @@
package com.google.android.gms.recaptcha.internal;
import com.google.android.gms.recaptcha.RecaptchaAction;
import com.google.android.gms.recaptcha.RecaptchaHandle;
import com.google.android.gms.recaptcha.internal.ExecuteParams;
import com.google.android.gms.recaptcha.internal.InitParams;
import com.google.android.gms.recaptcha.internal.ICloseCallback;
import com.google.android.gms.recaptcha.internal.IExecuteCallback;
import com.google.android.gms.recaptcha.internal.IInitCallback;
interface IRecaptchaService {
void verifyWithRecaptcha(IExecuteCallback callback, String siteKey, String packageName) = 0;
void init(IInitCallback callback, String siteKey) = 1;
void execute(IExecuteCallback callback, in RecaptchaHandle handle, in RecaptchaAction action) = 2;
void close(ICloseCallback callback, in RecaptchaHandle handle) = 3;
void init2(IInitCallback callback, in InitParams params) = 4;
void execute2(IExecuteCallback callback, in ExecuteParams params) = 5;
}

View file

@ -0,0 +1,3 @@
package com.google.android.gms.recaptcha.internal;
parcelable InitParams;

View file

@ -0,0 +1,3 @@
package com.google.android.gms.recaptcha.internal;
parcelable InitResults;

View file

@ -0,0 +1,34 @@
/*
* SPDX-FileCopyrightText: 2022 microG Project Team
* SPDX-License-Identifier: Apache-2.0
* Notice: Portions of this file are reproduced from work created and shared by Google and used
* according to terms described in the Creative Commons 4.0 Attribution License.
* See https://developers.google.com/readme/policies for details.
*/
package com.google.android.gms.recaptcha;
/**
* Exception thrown when the server returns a non-200 response code.
*/
public class HttpStatusException extends Exception {
private int errorHttpStatus;
/**
* Constructs a {@link HttpStatusException} with the specified detail message and error code.
*
* @param msg The detail message (which is saved for later retrieval by the {@link #getMessage()} method)
* @param errorHttpStatus The status code of the failed HTTP request.
*/
public HttpStatusException(String msg, int errorHttpStatus) {
super(msg);
this.errorHttpStatus = errorHttpStatus;
}
/**
* Returns the status code of a failed HTTP request.
*/
public int getHttpErrorStatus() {
return errorHttpStatus;
}
}

View file

@ -0,0 +1,42 @@
/*
* SPDX-FileCopyrightText: 2022 microG Project Team
* SPDX-License-Identifier: Apache-2.0
* Notice: Portions of this file are reproduced from work created and shared by Google and used
* according to terms described in the Creative Commons 4.0 Attribution License.
* See https://developers.google.com/readme/policies for details.
*/
package com.google.android.gms.recaptcha;
import android.app.Activity;
import android.content.Context;
import org.microg.gms.recaptcha.RecaptchaClientImpl;
/**
* The reCAPTCHA API provides access to Google Cloud services that help you protect your app from spam and other
* abusive actions.
* <p>
* To instantiate a reCAPTCHA mobile client, call {@link #getClient(Context)} or {@link #getClient(Activity)}.
*/
public class Recaptcha {
/**
* Returns a {@link RecaptchaClient} that is used to access all APIs that are called when the app has a foreground
* {@link Activity}.
* <p>
* Use this method over {@link #getClient(Context)} to improve performance if you plan to make multiple API calls
* from your application's foreground {@link Activity}.
*/
public static RecaptchaClient getClient(Activity activity) {
return new RecaptchaClientImpl(activity);
}
/**
* Returns a {@link RecaptchaClient} that is used to access all APIs that are called without access to a foreground
* {@link Activity}.
*/
public static RecaptchaClient getClient(Context context) {
return new RecaptchaClientImpl(context);
}
}

View file

@ -0,0 +1,107 @@
/*
* SPDX-FileCopyrightText: 2022 microG Project Team
* SPDX-License-Identifier: Apache-2.0
* Notice: Portions of this file are reproduced from work created and shared by Google and used
* according to terms described in the Creative Commons 4.0 Attribution License.
* See https://developers.google.com/readme/policies for details.
*/
package com.google.android.gms.recaptcha;
import android.os.Bundle;
import androidx.annotation.NonNull;
import org.microg.safeparcel.AutoSafeParcelable;
/**
* Actions (e.g., login) intended to be protected by reCAPTCHA. An instance of this object should be passed to
* {@link RecaptchaClient#execute(RecaptchaHandle, RecaptchaAction)}.
*/
public class RecaptchaAction extends AutoSafeParcelable {
@Field(1)
private RecaptchaActionType action;
@Field(2)
private String customAction;
@Field(3)
private Bundle additionalArgs;
@Field(4)
private String verificationHistoryToken;
private RecaptchaAction() {
}
/**
* Creates a {@link RecaptchaAction} instance with a predefined reCAPTCHA action.
*/
public RecaptchaAction(RecaptchaActionType action) {
this(action, Bundle.EMPTY);
}
/**
* Creates a {@link RecaptchaAction} instance with a predefined reCAPTCHA action and additional arguments.
*/
public RecaptchaAction(RecaptchaActionType action, Bundle additionalArgs) {
this.action = action;
this.additionalArgs = additionalArgs;
}
/**
* Creates a {@link RecaptchaAction} instance with a custom action String.
*/
public RecaptchaAction(String customAction) {
this(customAction, Bundle.EMPTY);
}
/**
* Creates a {@link RecaptchaAction} instance with a custom action in the form of a String and additional arguments.
*/
public RecaptchaAction(String customAction, Bundle additionalArgs) {
this.action = new RecaptchaActionType(RecaptchaActionType.OTHER);
this.customAction = customAction;
this.additionalArgs = additionalArgs;
}
/**
* Gets {@code RecaptchaActionType}.
*/
public RecaptchaActionType getAction() {
return action;
}
/**
* Gets the additional arg map specified by this action.
*/
public Bundle getAdditionalArgs() {
return additionalArgs;
}
/**
* Gets custom action that user inputs.
*/
public String getCustomAction() {
return customAction;
}
/**
* Gets the verification history token specified by this action.
*/
public String getVerificationHistoryToken() {
return verificationHistoryToken;
}
/**
* Gets the String value of {@code RecaptchaAction}.
*/
@NonNull
@Override
public String toString() {
if (RecaptchaActionType.OTHER.equals(action.name) && customAction != null) {
return customAction;
} else {
return action.name;
}
}
public static final Creator<RecaptchaAction> CREATOR = new AutoCreator<>(RecaptchaAction.class);
}

View file

@ -0,0 +1,45 @@
/*
* SPDX-FileCopyrightText: 2022 microG Project Team
* SPDX-License-Identifier: Apache-2.0
* Notice: Portions of this file are reproduced from work created and shared by Google and used
* according to terms described in the Creative Commons 4.0 Attribution License.
* See https://developers.google.com/readme/policies for details.
*/
package com.google.android.gms.recaptcha;
import org.microg.safeparcel.AutoSafeParcelable;
/**
* Collection of predefined actions used by RecaptchaHandle.
*/
public class RecaptchaActionType extends AutoSafeParcelable {
@Field(1)
String name;
private RecaptchaActionType() {}
public RecaptchaActionType(String action) {
this.name = action;
}
public static final Creator<RecaptchaActionType> CREATOR = new AutoCreator<>(RecaptchaActionType.class);
/**
* User interaction that needs to be verified while the user is performing the workflow you would like to protect.
*/
public @interface Action {}
/**
* Indicates that the protected action is a login workflow.
*/
public static final String LOGIN = "login";
/**
* When a custom action is specified, reCAPTCHA uses this value automatically.
*/
public static final String OTHER = "other";
/**
* Indicates that the protected action is a signup workflow.
*/
public static final String SIGNUP = "signup";
}

View file

@ -0,0 +1,75 @@
/*
* SPDX-FileCopyrightText: 2022 microG Project Team
* SPDX-License-Identifier: Apache-2.0
* Notice: Portions of this file are reproduced from work created and shared by Google and used
* according to terms described in the Creative Commons 4.0 Attribution License.
* See https://developers.google.com/readme/policies for details.
*/
package com.google.android.gms.recaptcha;
import com.google.android.gms.common.api.Status;
import com.google.android.gms.tasks.Task;
/**
* The main entry point for interacting with the reCAPTCHA API.
*/
public interface RecaptchaClient {
/**
* Sends a challenge to an account in order to verify the identity of the user.
* <p>
* This method can be optionally called if you decide to perform a two factor authentication check on an account.
*
* @param recaptchaHandle RecaptchaHandle initialized through {@link #init(String)}.
* @param challengeRequestToken The challenge request token obtained through CreateAssessment().
* @return A VerificationHandle that can be used with {@link #verifyAccount(String, VerificationHandle)} calls. A
* handle is usually valid for a specific time after creation. If an expired handle is returned, meaning the
* operation was aborted, {@link VerificationHandle#getOperationAbortedToken()} will return a token that can be
* used with the reCAPTCHA Enterprise API CreateAssessment() to get more details.
*/
Task<VerificationHandle> challengeAccount(RecaptchaHandle recaptchaHandle, String challengeRequestToken);
/**
* Closes the initialized RecaptchaHandle.
* <p>
* Closes the handle if you will not be using it again to save resources.
*
* @param handle RecaptchaHandle initialized through {@link #init(String)}.
* @return true if the handle got closed successfully for the first time, false if there wasn't anything to close
* (e.g., the handle was already previously closed).
*/
Task<Boolean> close(RecaptchaHandle handle);
/**
* Returns a score indicating how likely the action was triggered by a real user. A score close to 0 indicates a likely bot, and a score close to 1 indicates a likely human.
* <p>
* This method should be called every time there is an action to be protected.
*
* @param handle {@link RecaptchaHandle} initialized through {@link #init(String)}.
* @param action User interaction that needs to be protected.
*/
Task<RecaptchaResultData> execute(RecaptchaHandle handle, RecaptchaAction action);
/**
* Prepares and initializes a RecaptchaHandle.
*
* @param siteKey A site key registered for this app
*/
Task<RecaptchaHandle> init(String siteKey);
/**
* Verifies a PIN against a verification handle obtained through a
* {@link #challengeAccount(RecaptchaHandle, String)} call.
* <p>
* The method should be called to verify a PIN submitted by the user. The returned {@link VerificationResult} will
* contain a Status and either a {@link VerificationHandle} or a new reCAPTCHA token.
*
* @param pin The fixed-length numerical PIN entered by the user. If a PIN with unexpected length or
* non numerical characters is entered, a
* {@link RecaptchaStatusCodes#RECAPTCHA_2FA_INVALID_PIN} error will be returned.
* @param verificationHandle A verification handle containing information required to preform the verification
* operation.
* @return A {@link VerificationResult} containing a {@link Status} indicating the status of the of the verification.
*/
Task<VerificationResult> verifyAccount(String pin, VerificationHandle verificationHandle);
}

View file

@ -0,0 +1,61 @@
/*
* SPDX-FileCopyrightText: 2022 microG Project Team
* SPDX-License-Identifier: Apache-2.0
* Notice: Portions of this file are reproduced from work created and shared by Google and used
* according to terms described in the Creative Commons 4.0 Attribution License.
* See https://developers.google.com/readme/policies for details.
*/
package com.google.android.gms.recaptcha;
import org.microg.gms.common.PublicApi;
import org.microg.safeparcel.AutoSafeParcelable;
import java.util.Collections;
import java.util.List;
/**
* Information pertaining to reCAPTCHA handle, which is used to identify the initialized reCAPTCHA service.
*/
public class RecaptchaHandle extends AutoSafeParcelable {
@Field(1)
private String siteKey;
@Field(2)
private String clientPackageName;
@Field(3)
private List<String> acceptableAdditionalArgs;
@PublicApi(exclude = true)
private RecaptchaHandle() {
}
@PublicApi(exclude = true)
public RecaptchaHandle(String siteKey, String clientPackageName, List<String> acceptableAdditionalArgs) {
this.siteKey = siteKey;
this.clientPackageName = clientPackageName;
this.acceptableAdditionalArgs = acceptableAdditionalArgs;
}
/**
* Returns a list of strings indicating the additional argument keys that reCAPTCHA server accepts.
*/
public List<String> getAcceptableAdditionalArgs() {
return Collections.unmodifiableList(acceptableAdditionalArgs);
}
/**
* Returns the package name of the app that calls reCAPTCHA API.
*/
public String getClientPackageName() {
return clientPackageName;
}
/**
* Returns the reCAPTCHA Site Key you registered to help protect your application.
*/
public String getSiteKey() {
return siteKey;
}
public static final Creator<RecaptchaHandle> CREATOR = new AutoCreator<>(RecaptchaHandle.class);
}

View file

@ -0,0 +1,26 @@
/*
* SPDX-FileCopyrightText: 2022 microG Project Team
* SPDX-License-Identifier: Apache-2.0
* Notice: Portions of this file are reproduced from work created and shared by Google and used
* according to terms described in the Creative Commons 4.0 Attribution License.
* See https://developers.google.com/readme/policies for details.
*/
package com.google.android.gms.recaptcha;
import java.io.IOException;
/**
* Exception thrown when the mobile client fails to connect to the reCAPTCHA server.
*/
public class RecaptchaNetworkException extends Exception {
/**
* Constructs a {@link RecaptchaNetworkException} with the specified detail message and {@link IOException}.
*
* @param msg The detail message (which is saved for later retrieval by the {@link #getMessage()} method)
* @param e The root {@link IOException} that causes the {@link RecaptchaNetworkException}.
*/
public RecaptchaNetworkException(String msg, IOException e) {
super(msg, e);
}
}

View file

@ -0,0 +1,35 @@
/*
* SPDX-FileCopyrightText: 2022 microG Project Team
* SPDX-License-Identifier: Apache-2.0
* Notice: Portions of this file are reproduced from work created and shared by Google and used
* according to terms described in the Creative Commons 4.0 Attribution License.
* See https://developers.google.com/readme/policies for details.
*/
package com.google.android.gms.recaptcha;
/**
* Class for wrapping an optional object (i.e., an immutable object that may contain a non-null reference to another
* object) to be used in {@link VerificationResult}.
*/
public class RecaptchaOptionalObject<T> {
private T object;
private RecaptchaOptionalObject(T object) {
this.object = object;
}
/**
* Returns a {@link RecaptchaOptionalObject} wrapping the specified object, which can be {@code null}.
*/
public static <T> RecaptchaOptionalObject<T> ofNullable(T object) {
return new RecaptchaOptionalObject<>(object);
}
/**
* Returns the wrapped object.
*/
public T orNull() {
return object;
}
}

View file

@ -0,0 +1,35 @@
/*
* SPDX-FileCopyrightText: 2022 microG Project Team
* SPDX-License-Identifier: Apache-2.0
* Notice: Portions of this file are reproduced from work created and shared by Google and used
* according to terms described in the Creative Commons 4.0 Attribution License.
* See https://developers.google.com/readme/policies for details.
*/
package com.google.android.gms.recaptcha;
import org.microg.safeparcel.AutoSafeParcelable;
/**
* Information pertaining to reCAPTCHA result data.
*/
public class RecaptchaResultData extends AutoSafeParcelable {
@Field(1)
private String tokenResult;
private RecaptchaResultData() {
}
public RecaptchaResultData(String token) {
this.tokenResult = token;
}
/**
* Returns a reCAPTCHA token.
*/
public String getTokenResult() {
return tokenResult;
}
public static final Creator<RecaptchaResultData> CREATOR = new AutoCreator<>(RecaptchaResultData.class);
}

View file

@ -0,0 +1,69 @@
/*
* SPDX-FileCopyrightText: 2022 microG Project Team
* SPDX-License-Identifier: Apache-2.0
* Notice: Portions of this file are reproduced from work created and shared by Google and used
* according to terms described in the Creative Commons 4.0 Attribution License.
* See https://developers.google.com/readme/policies for details.
*/
package com.google.android.gms.recaptcha;
import com.google.android.gms.common.api.CommonStatusCodes;
/**
* Status codes for the reCAPTCHA API.
*/
public final class RecaptchaStatusCodes extends CommonStatusCodes {
/**
* reCAPTCHA feature is disabled.
* <p>
* Please check if you update Google Play Services on your phone to get our latest reCAPTCHA module code that
* matches to the API versions you used in the SDK in your app.
*/
public static final int RECAPTCHA_FEATURE_OFF = 36004;
/**
* An internal error occurred during two factor authentication calls.
* <p>
* Please try again in a bit.
*/
public static final int RECAPTCHA_2FA_UNKNOWN = 36005;
/**
* The challenge account request token has expired.
* <p>
* Please obtain another request token from the reCAPTCHA Enterprise server.
*/
public static final int RECAPTCHA_2FA_CHALLENGE_EXPIRED = 36006;
/**
* The challenge account request token is invalid.
* <p>
* Please verify that you are using the correct token obtained from the reCAPTCHA Enterprise server.
*/
public static final int RECAPTCHA_2FA_INVALID_REQUEST_TOKEN = 36007;
/**
* The verification PIN has invalid format.
* <p>
* Please verify that the input PIN is of the right length and only contain numerical characters.
*/
public static final int RECAPTCHA_2FA_INVALID_PIN = 36008;
/**
* The verification PIN does not match the PIN sent to the challenged account.
* <p>
* Please try again using a new {@link VerificationHandle}.
*/
public static final int RECAPTCHA_2FA_PIN_MISMATCH = 36009;
/**
* All allowed verification attempts are exhausted.
* <p>
* Please restart the verification workflow by calling
* {@link RecaptchaClient#execute(RecaptchaHandle, RecaptchaAction)} again to fetch a new reCAPTCHA token, for
* retrieving a new challenge token via reCAPTCHA Enterprise API CreateAssessment(), then calling
* {@link RecaptchaClient#challengeAccount(RecaptchaHandle, String)}.
*/
public static final int RECAPTCHA_2FA_ATTEMPTS_EXHAUSTED = 36010;
/**
* The operation was aborted.
* <p>
* Please use the abortion token with the reCAPTCHA Enterprise server to obtain more information.
*/
public static final int RECAPTCHA_2FA_ABORTED = 36014;
}

View file

@ -0,0 +1,58 @@
/*
* SPDX-FileCopyrightText: 2022 microG Project Team
* SPDX-License-Identifier: Apache-2.0
* Notice: Portions of this file are reproduced from work created and shared by Google and used
* according to terms described in the Creative Commons 4.0 Attribution License.
* See https://developers.google.com/readme/policies for details.
*/
package com.google.android.gms.recaptcha;
import com.google.android.gms.common.internal.safeparcel.SafeParcelable;
/**
* Stores the information required to verify an account.
* <p>
* This object is only valid for a specific time after creation and it holds all the information needed to validate a
* PIN using {@link RecaptchaClient#verifyAccount(String, VerificationHandle)}. If an expired handle is returned by
* {@link RecaptchaClient#challengeAccount(RecaptchaHandle, String)}, then {@link #getOperationAbortedToken()} will
* return a token that can be used with the reCAPTCHA Enterprise API CreateAssessment() to get more details.
*/
public abstract class VerificationHandle implements SafeParcelable {
/**
* Returns the length of the PIN code.
*/
public abstract int getCodeLength();
/**
* Returns a reCAPTCHA token in the case {@link RecaptchaClient#challengeAccount(RecaptchaHandle, String)}
* operation was aborted and an expired {@link VerificationHandle} was returned, otherwise this returns an empty
* string. This can be used with the reCAPTCHA Enterprise API CreateAssessment() to retrieve more details.
*/
public abstract String getOperationAbortedToken();
/**
* Returns the site public key you registered for using reCAPTCHA.
*/
public abstract String getSiteKey();
/**
* Returns the validity duration of the object since its creation in minutes.
*/
public abstract long getTimeoutMinutes();
/**
* Returns an encrypted version of the internal verification token that will be used in
* {@link RecaptchaClient#verifyAccount(String, VerificationHandle)} call.
*/
public abstract String getVerificationToken();
/**
* Returns a boolean indicating if the {@link VerificationHandle} is valid for
* {@link RecaptchaClient#verifyAccount(String, VerificationHandle)} API calls. An invalid handle will cause
* {@link RecaptchaClient#verifyAccount(String, VerificationHandle)} calls to fail immediately.
*/
public boolean isValid() {
throw new UnsupportedOperationException("Not yet implemented");
}
}

View file

@ -0,0 +1,43 @@
/*
* SPDX-FileCopyrightText: 2022 microG Project Team
* SPDX-License-Identifier: Apache-2.0
* Notice: Portions of this file are reproduced from work created and shared by Google and used
* according to terms described in the Creative Commons 4.0 Attribution License.
* See https://developers.google.com/readme/policies for details.
*/
package com.google.android.gms.recaptcha;
import com.google.android.gms.common.api.CommonStatusCodes;
import com.google.android.gms.common.api.Status;
/**
* Immmutable object to hold the result of a verification operation.
*/
public abstract class VerificationResult {
/**
* Returns a status to provide more info on the result of the verification operation.
*
* @return {@link CommonStatusCodes#SUCCESS} or {@link RecaptchaStatusCodes#RECAPTCHA_2FA_ABORTED} status if the
* verification operation succeeded, and non-success status (e.g.
* {@link RecaptchaStatusCodes#RECAPTCHA_2FA_PIN_MISMATCH}) if the verification failed with more attempts
* available, i.e. user entered wrong pin.
*/
public abstract Status getVerificationStatus();
/**
* Returns an optional containing the reCAPTCHA token if the verification operation was successful or aborted.
*
* @return the reCAPTCHA token if {@code getVerificationStatus().equals(CommonStatusCodes.SUCCESS)} or
* {@code getVerificationStatus().equals RecaptchaStatusCodes.RECAPTCHA_2FA_ABORTED)}, otherwise an empty
* {@link RecaptchaOptionalObject}.
*/
public abstract RecaptchaOptionalObject<String> recaptchaToken();
/**
* Returns an optional containing a verification handle if the verification operation failed and the client is allowed to retry.
*
* @return a verification handle on a failure status, otherwise an empty {@link RecaptchaOptionalObject}.
*/
public abstract RecaptchaOptionalObject<VerificationHandle> verificationHandle();
}

View file

@ -0,0 +1,35 @@
/*
* SPDX-FileCopyrightText: 2022 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.recaptcha.internal;
import androidx.annotation.NonNull;
import com.google.android.gms.recaptcha.RecaptchaAction;
import com.google.android.gms.recaptcha.RecaptchaHandle;
import org.microg.gms.utils.ToStringHelper;
import org.microg.safeparcel.AutoSafeParcelable;
public class ExecuteParams extends AutoSafeParcelable {
@Field(1)
public RecaptchaHandle handle;
@Field(2)
public RecaptchaAction action;
@Field(3)
public String version;
@NonNull
@Override
public String toString() {
return ToStringHelper.name("ExecuteParams")
.field("handle", handle)
.field("action", action)
.field("version", version)
.end();
}
public static final Creator<ExecuteParams> CREATOR = new AutoCreator<>(ExecuteParams.class);
}

View file

@ -0,0 +1,17 @@
/*
* SPDX-FileCopyrightText: 2022 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.recaptcha.internal;
import com.google.android.gms.recaptcha.RecaptchaResultData;
import org.microg.safeparcel.AutoSafeParcelable;
public class ExecuteResults extends AutoSafeParcelable {
@Field(1)
public RecaptchaResultData data;
public static final Creator<ExecuteResults> CREATOR = new AutoCreator<>(ExecuteResults.class);
}

View file

@ -0,0 +1,29 @@
/*
* SPDX-FileCopyrightText: 2022 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.recaptcha.internal;
import androidx.annotation.NonNull;
import org.microg.gms.utils.ToStringHelper;
import org.microg.safeparcel.AutoSafeParcelable;
public class InitParams extends AutoSafeParcelable {
@Field(1)
public String siteKey;
@Field(2)
public String version;
@NonNull
@Override
public String toString() {
return ToStringHelper.name("InitParams")
.field("siteKey", siteKey)
.field("version", version)
.end();
}
public static final Creator<InitParams> CREATOR = new AutoCreator<>(InitParams.class);
}

View file

@ -0,0 +1,17 @@
/*
* SPDX-FileCopyrightText: 2022 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.recaptcha.internal;
import com.google.android.gms.recaptcha.RecaptchaHandle;
import org.microg.safeparcel.AutoSafeParcelable;
public class InitResults extends AutoSafeParcelable {
@Field(1)
public RecaptchaHandle handle;
public static final Creator<InitResults> CREATOR = new AutoCreator<>(InitResults.class);
}

View file

@ -0,0 +1,131 @@
/*
* SPDX-FileCopyrightText: 2022 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.recaptcha;
import android.content.Context;
import android.os.RemoteException;
import android.util.Log;
import com.google.android.gms.common.api.Api;
import com.google.android.gms.common.api.GoogleApi;
import com.google.android.gms.common.api.Status;
import com.google.android.gms.recaptcha.Recaptcha;
import com.google.android.gms.recaptcha.RecaptchaAction;
import com.google.android.gms.recaptcha.RecaptchaClient;
import com.google.android.gms.recaptcha.RecaptchaHandle;
import com.google.android.gms.recaptcha.RecaptchaResultData;
import com.google.android.gms.recaptcha.VerificationHandle;
import com.google.android.gms.recaptcha.VerificationResult;
import com.google.android.gms.recaptcha.internal.ExecuteParams;
import com.google.android.gms.recaptcha.internal.ExecuteResults;
import com.google.android.gms.recaptcha.internal.ICloseCallback;
import com.google.android.gms.recaptcha.internal.IExecuteCallback;
import com.google.android.gms.recaptcha.internal.IInitCallback;
import com.google.android.gms.recaptcha.internal.InitParams;
import com.google.android.gms.recaptcha.internal.InitResults;
import com.google.android.gms.tasks.Task;
import com.google.android.gms.tasks.Tasks;
import org.microg.gms.common.api.PendingGoogleApiCall;
import org.microg.gms.tasks.TaskImpl;
public class RecaptchaClientImpl extends GoogleApi<Api.ApiOptions.NoOptions> implements RecaptchaClient {
private int openHandles = 0;
public RecaptchaClientImpl(Context context) {
super(context, new Api<>((options, c, looper, clientSettings, callbacks, connectionFailedListener) -> new RecaptchaGmsClient(c, callbacks, connectionFailedListener)), Api.ApiOptions.NO_OPTIONS);
}
@Override
public Task<VerificationHandle> challengeAccount(RecaptchaHandle recaptchaHandle, String challengeRequestToken) {
return Tasks.forException(new UnsupportedOperationException());
}
@Override
public Task<Boolean> close(RecaptchaHandle handle) {
return scheduleTask((PendingGoogleApiCall<Boolean, RecaptchaGmsClient>) (client, completionSource) -> {
client.close(new ICloseCallback.Stub() {
@Override
public void onClosed(Status status, boolean closed) throws RemoteException {
if (status.isSuccess()) {
completionSource.trySetResult(closed);
} else {
completionSource.trySetException(new RuntimeException(status.getStatusMessage()));
}
if (openHandles == 0) {
Log.w("RecaptchaClient", "Can't mark handle closed if none is open");
return;
}
openHandles--;
if (openHandles == 0) client.disconnect();
}
}, handle);
});
}
@Override
public Task<RecaptchaResultData> execute(RecaptchaHandle handle, RecaptchaAction action) {
return scheduleTask((PendingGoogleApiCall<RecaptchaResultData, RecaptchaGmsClient>) (client, completionSource) -> {
ExecuteParams params = new ExecuteParams();
params.handle = handle;
params.action = action;
params.version = "18.1.1";
client.execute(new IExecuteCallback.Stub() {
@Override
public void onData(Status status, RecaptchaResultData data) throws RemoteException {
if (status.isSuccess()) {
completionSource.trySetResult(data);
} else {
completionSource.trySetException(new RuntimeException(status.getStatusMessage()));
}
}
@Override
public void onResults(Status status, ExecuteResults results) throws RemoteException {
if (status.isSuccess()) {
completionSource.trySetResult(results.data);
} else {
completionSource.trySetException(new RuntimeException(status.getStatusMessage()));
}
}
}, params);
});
}
@Override
public Task<RecaptchaHandle> init(String siteKey) {
openHandles++;
return scheduleTask((PendingGoogleApiCall<RecaptchaHandle, RecaptchaGmsClient>) (client, completionSource) -> {
InitParams params = new InitParams();
params.siteKey = siteKey;
params.version = "18.1.1";
client.init(new IInitCallback.Stub() {
@Override
public void onHandle(Status status, RecaptchaHandle handle) throws RemoteException {
if (status.isSuccess()) {
completionSource.trySetResult(handle);
} else {
completionSource.trySetException(new RuntimeException(status.getStatusMessage()));
}
}
@Override
public void onResults(Status status, InitResults results) throws RemoteException {
if (status.isSuccess()) {
completionSource.trySetResult(results.handle);
} else {
completionSource.trySetException(new RuntimeException(status.getStatusMessage()));
}
}
}, params);
});
}
@Override
public Task<VerificationResult> verifyAccount(String pin, VerificationHandle verificationHandle) {
return Tasks.forException(new UnsupportedOperationException());
}
}

View file

@ -0,0 +1,56 @@
/*
* SPDX-FileCopyrightText: 2022 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.recaptcha;
import android.content.Context;
import android.os.IBinder;
import android.os.RemoteException;
import org.microg.gms.common.GmsClient;
import org.microg.gms.common.GmsService;
import com.google.android.gms.common.api.internal.ConnectionCallbacks;
import com.google.android.gms.common.api.internal.OnConnectionFailedListener;
import com.google.android.gms.recaptcha.RecaptchaAction;
import com.google.android.gms.recaptcha.RecaptchaHandle;
import com.google.android.gms.recaptcha.internal.ExecuteParams;
import com.google.android.gms.recaptcha.internal.ICloseCallback;
import com.google.android.gms.recaptcha.internal.IExecuteCallback;
import com.google.android.gms.recaptcha.internal.IInitCallback;
import com.google.android.gms.recaptcha.internal.IRecaptchaService;
import com.google.android.gms.recaptcha.internal.InitParams;
public class RecaptchaGmsClient extends GmsClient<IRecaptchaService> {
public RecaptchaGmsClient(Context context, ConnectionCallbacks callbacks, OnConnectionFailedListener connectionFailedListener) {
super(context, callbacks, connectionFailedListener, GmsService.RECAPTCHA.ACTION);
serviceId = GmsService.RECAPTCHA.SERVICE_ID;
}
public void init(IInitCallback callback, String siteKey) throws RemoteException {
getServiceInterface().init(callback, siteKey);
}
public void init(IInitCallback callback, InitParams params) throws RemoteException {
getServiceInterface().init2(callback, params);
}
public void execute(IExecuteCallback callback, RecaptchaHandle handle, RecaptchaAction action) throws RemoteException {
getServiceInterface().execute(callback, handle, action);
}
public void execute(IExecuteCallback callback, ExecuteParams params) throws RemoteException {
getServiceInterface().execute2(callback, params);
}
public void close(ICloseCallback callback, RecaptchaHandle handle) throws RemoteException {
getServiceInterface().close(callback, handle);
}
@Override
protected IRecaptchaService interfaceFromBinder(IBinder binder) {
return IRecaptchaService.Stub.asInterface(binder);
}
}