Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Unverified Commit b2f7f69f authored by Marvin W.'s avatar Marvin W. 🐿️
Browse files

Recaptcha: Initial version

parent 18c3a615
Loading
Loading
Loading
Loading
+11 −3
Original line number Diff line number Diff line
@@ -51,7 +51,11 @@ public class GoogleApiManager {
        boolean connecting = client.isConnecting();
        boolean connected = client.isConnected();
        if (connected) {
            try {
                apiCall.execute(client, completionSource);
            } catch (Exception e) {
                completionSource.setException(e);
            }
        } else {
            waitingApiCallMap.get(new ApiInstance(api)).add(new WaitingApiCall<R>((PendingGoogleApiCall<R, ApiClient>) apiCall, completionSource));
            if (!connecting) {
@@ -63,7 +67,11 @@ public class GoogleApiManager {
    private synchronized void onInstanceConnected(ApiInstance apiInstance, Bundle connectionHint) {
        List<WaitingApiCall<?>> waitingApiCalls = waitingApiCallMap.get(apiInstance);
        for (WaitingApiCall<?> waitingApiCall : waitingApiCalls) {
            try {
                waitingApiCall.execute(clientMap.get(apiInstance));
            } catch (Exception e) {
                waitingApiCall.failed(e);
            }
        }
        waitingApiCalls.clear();
    }
@@ -120,7 +128,7 @@ public class GoogleApiManager {
            this.completionSource = completionSource;
        }

        public void execute(ApiClient client) {
        public void execute(ApiClient client) throws Exception {
            apiCall.execute(client, completionSource);
        }

+1 −1
Original line number Diff line number Diff line
@@ -8,5 +8,5 @@ package org.microg.gms.common.api;
import com.google.android.gms.tasks.TaskCompletionSource;

public interface PendingGoogleApiCall<R, A extends ApiClient> {
    void execute(A client, TaskCompletionSource<R> completionSource);
    void execute(A client, TaskCompletionSource<R> completionSource) throws Exception;
}
+2 −0
Original line number Diff line number Diff line
@@ -31,6 +31,7 @@ dependencies {
    implementation project(':play-services-location-core')
    withNearbyImplementation project(':play-services-nearby-core')
    implementation project(':play-services-oss-licenses-core')
    implementation project(':play-services-recaptcha-core')
    implementation project(':play-services-safetynet-core')
    implementation project(':play-services-tapandpay-core')
    implementation project(':play-services-vision-core')
@@ -49,6 +50,7 @@ dependencies {
    implementation project(':play-services-maps')
    implementation project(':play-services-measurement-base')
    implementation project(':play-services-places')
    implementation project(':play-services-recaptcha')
    implementation project(':play-services-safetynet')
    implementation project(':play-services-tasks-ktx')

+0 −1
Original line number Diff line number Diff line
@@ -804,7 +804,6 @@
                <action android:name="com.google.android.gms.plus.service.START" />
                <action android:name="com.google.android.gms.pseudonymous.service.START" />
                <action android:name="com.google.android.gms.rcs.START" />
                <action android:name="com.google.android.gms.recaptcha.service.START" />
                <action android:name="com.google.android.gms.romanesco.MODULE_BACKUP_AGENT" />
                <action android:name="com.google.android.gms.romanesco.service.START" />
                <action android:name="com.google.android.gms.search.service.SEARCH_AUTH_START" />
+104 −23
Original line number Diff line number Diff line
@@ -9,27 +9,37 @@ import android.annotation.SuppressLint
import android.os.Bundle
import android.util.Base64
import android.util.Log
import androidx.core.content.ContextCompat
import androidx.core.os.bundleOf
import androidx.navigation.fragment.findNavController
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.preference.Preference
import androidx.preference.PreferenceCategory
import androidx.preference.PreferenceFragmentCompat
import com.android.volley.toolbox.JsonObjectRequest
import com.android.volley.toolbox.Volley
import com.google.android.gms.R
import com.google.android.gms.recaptcha.Recaptcha
import com.google.android.gms.recaptcha.RecaptchaAction
import com.google.android.gms.recaptcha.RecaptchaActionType
import com.google.android.gms.safetynet.SafetyNet
import com.google.android.gms.tasks.await
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.json.JSONObject
import org.microg.gms.safetynet.SafetyNetDatabase
import org.microg.gms.safetynet.SafetyNetRequestType
import org.microg.gms.safetynet.SafetyNetRequestType.ATTESTATION
import org.microg.gms.safetynet.SafetyNetRequestType.RECAPTCHA
import java.net.URLEncoder
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine
import kotlin.random.Random

class SafetyNetPreferencesFragment : PreferenceFragmentCompat() {
    private lateinit var runAttest: Preference
    private lateinit var runReCaptcha: Preference
    private lateinit var runReCaptchaEnterprise: Preference
    private lateinit var apps: PreferenceCategory
    private lateinit var appsAll: Preference
    private lateinit var appsNone: Preference
@@ -42,17 +52,24 @@ class SafetyNetPreferencesFragment : PreferenceFragmentCompat() {
    override fun onBindPreferences() {
        runAttest = preferenceScreen.findPreference("pref_safetynet_run_attest") ?: runAttest
        runReCaptcha = preferenceScreen.findPreference("pref_recaptcha_run_test") ?: runReCaptcha
        runReCaptchaEnterprise =
            preferenceScreen.findPreference("pref_recaptcha_enterprise_run_test") ?: runReCaptchaEnterprise
        apps = preferenceScreen.findPreference("prefcat_safetynet_apps") ?: apps
        appsAll = preferenceScreen.findPreference("pref_safetynet_apps_all") ?: appsAll
        appsNone = preferenceScreen.findPreference("pref_safetynet_apps_none") ?: appsNone

        runAttest.isVisible = SAFETYNET_API_KEY != null
        runReCaptcha.isVisible = RECAPTCHA_SITE_KEY != null
        runReCaptchaEnterprise.isVisible = RECAPTCHA_ENTERPRISE_SITE_KEY != null

        runAttest.setOnPreferenceClickListener {
            val context = context ?: return@setOnPreferenceClickListener false
            runAttest.setIcon(R.drawable.ic_circle_pending)
            runAttest.setSummary(R.string.pref_test_summary_running)
            lifecycleScope.launchWhenResumed {
                try {
                    val response = SafetyNet.getClient(requireActivity())
                    .attest(Random.nextBytes(32), "AIzaSyCcJO6IZiA5Or_AXw3LFdaTCmpnfL4pJ-Q").await()
                        .attest(Random.nextBytes(32), SAFETYNET_API_KEY).await()
                    val (_, payload, _) = try {
                        response.jwsResult.split(".")
                    } catch (e: Exception) {
@@ -68,6 +85,10 @@ class SafetyNetPreferencesFragment : PreferenceFragmentCompat() {
                            runAttest.summary = summary
                            runAttest.icon = icon
                        }
                } catch (e: Exception) {
                    runAttest.summary = getString(R.string.pref_test_summary_failed, e.message)
                    runAttest.icon = ContextCompat.getDrawable(context, R.drawable.ic_circle_warn)
                }
                updateContent()
            }
            true
@@ -77,13 +98,66 @@ class SafetyNetPreferencesFragment : PreferenceFragmentCompat() {
            runReCaptcha.setIcon(R.drawable.ic_circle_pending)
            runReCaptcha.setSummary(R.string.pref_test_summary_running)
            lifecycleScope.launchWhenResumed {
                try {
                    val response = SafetyNet.getClient(requireActivity())
                    .verifyWithRecaptcha("6Lc4TzgeAAAAAJnW7Jbo6UtQ0xGuTKjHAeyhINuq").await()
                formatSummaryForSafetyNetResult(context, response.tokenResult, response.result.status, RECAPTCHA)
                        .verifyWithRecaptcha(RECAPTCHA_SITE_KEY).await()
                    val result = if (response.tokenResult != null) {
                        val queue = Volley.newRequestQueue(context)
                        val json =
                            if (RECAPTCHA_SECRET != null) {
                                suspendCoroutine { continuation ->
                                    queue.add(object : JsonObjectRequest(
                                        Method.POST,
                                        "https://www.google.com/recaptcha/api/siteverify",
                                        null,
                                        { continuation.resume(it) },
                                        { continuation.resumeWithException(it) }
                                    ) {
                                        override fun getBodyContentType(): String = "application/x-www-form-urlencoded; charset=UTF-8"
                                        override fun getBody(): ByteArray =
                                            "secret=$RECAPTCHA_SECRET&response=${URLEncoder.encode(response.tokenResult, "UTF-8")}".encodeToByteArray()
                                    })
                                }
                            } else {
                                // Can't properly verify, everything becomes a success
                                JSONObject(mapOf("success" to true))
                            }
                        Log.d(TAG, "Result: $json")
                        json.toString()
                    } else {
                        null
                    }
                    formatSummaryForSafetyNetResult(context, result, response.result.status, RECAPTCHA)
                        .let { (summary, icon) ->
                            runReCaptcha.summary = summary
                            runReCaptcha.icon = icon
                        }
                } catch (e: Exception) {
                    runReCaptcha.summary = getString(R.string.pref_test_summary_failed, e.message)
                    runReCaptcha.icon = ContextCompat.getDrawable(context, R.drawable.ic_circle_warn)
                }
                updateContent()
            }
            true
        }
        runReCaptchaEnterprise.setOnPreferenceClickListener {
            val context = context ?: return@setOnPreferenceClickListener false
            runReCaptchaEnterprise.setIcon(R.drawable.ic_circle_pending)
            runReCaptchaEnterprise.setSummary(R.string.pref_test_summary_running)
            lifecycleScope.launchWhenResumed {
                try {
                    val client = Recaptcha.getClient(requireActivity())
                    val handle = client.init(RECAPTCHA_ENTERPRISE_SITE_KEY).await()
                    val result =
                        client.execute(handle, RecaptchaAction(RecaptchaActionType(RecaptchaActionType.SIGNUP))).await()
                    Log.d(TAG, "Recaptcha Token: " + result.tokenResult)
                    client.close(handle).await()
                    runReCaptchaEnterprise.summary = getString(R.string.pref_test_summary_warn, "Incomplete Test")
                    runReCaptchaEnterprise.icon = ContextCompat.getDrawable(context, R.drawable.ic_circle_warn)
                } catch (e: Exception) {
                    runReCaptchaEnterprise.summary = getString(R.string.pref_test_summary_failed, e.message)
                    runReCaptchaEnterprise.icon = ContextCompat.getDrawable(context, R.drawable.ic_circle_warn)
                }
                updateContent()
            }
            true
@@ -143,4 +217,11 @@ class SafetyNetPreferencesFragment : PreferenceFragmentCompat() {

        }
    }

    companion object {
        private val SAFETYNET_API_KEY: String? = "AIzaSyCcJO6IZiA5Or_AXw3LFdaTCmpnfL4pJ-Q"
        private val RECAPTCHA_SITE_KEY: String? = "6Lc4TzgeAAAAAJnW7Jbo6UtQ0xGuTKjHAeyhINuq"
        private val RECAPTCHA_SECRET: String? = "6Lc4TzgeAAA${"AAAjwSDqU-uG"}_Lcu2f23URMI8fq0I"
        private val RECAPTCHA_ENTERPRISE_SITE_KEY: String? = null
    }
}
Loading