mirror of https://github.com/YTVanced/VancedMicroG
234 lines
10 KiB
Kotlin
234 lines
10 KiB
Kotlin
/*
|
|
* SPDX-FileCopyrightText: 2021 microG Project Team
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
package org.microg.gms.safetynet
|
|
|
|
import android.annotation.SuppressLint
|
|
import android.content.Intent
|
|
import android.net.Uri
|
|
import android.net.http.SslCertificate
|
|
import android.os.Build.VERSION.SDK_INT
|
|
import android.os.Bundle
|
|
import android.os.ResultReceiver
|
|
import android.util.Base64
|
|
import android.util.Log
|
|
import android.view.View
|
|
import android.view.Window
|
|
import android.webkit.*
|
|
import androidx.appcompat.app.AppCompatActivity
|
|
import androidx.lifecycle.lifecycleScope
|
|
import androidx.webkit.WebViewClientCompat
|
|
import com.google.android.gms.safetynet.SafetyNetStatusCodes.*
|
|
import org.microg.gms.droidguard.core.DroidGuardResultCreator
|
|
import org.microg.gms.safetynet.core.ui.R
|
|
import java.io.ByteArrayInputStream
|
|
import java.net.URLEncoder
|
|
import java.security.MessageDigest
|
|
import kotlin.math.min
|
|
|
|
private const val TAG = "GmsReCAPTCHA"
|
|
|
|
private fun StringBuilder.appendUrlEncodedParam(key: String, value: String?) = append("&")
|
|
.append(URLEncoder.encode(key, "UTF-8"))
|
|
.append("=")
|
|
.append(value?.let { URLEncoder.encode(it, "UTF-8") } ?: "")
|
|
|
|
class ReCaptchaActivity : AppCompatActivity() {
|
|
private val receiver: ResultReceiver?
|
|
get() = intent?.getParcelableExtra("result") as ResultReceiver?
|
|
private val params: String?
|
|
get() = intent?.getStringExtra("params")
|
|
private val webView: WebView?
|
|
get() = findViewById(R.id.recaptcha_webview)
|
|
private val loading: View?
|
|
get() = findViewById(R.id.recaptcha_loading)
|
|
private val density: Float
|
|
get() = resources.displayMetrics.density
|
|
private val widthPixels: Int
|
|
get() = resources.displayMetrics.widthPixels
|
|
private val heightPixels: Int
|
|
get() {
|
|
val base = resources.displayMetrics.heightPixels
|
|
val statusBarHeightId = resources.getIdentifier("status_bar_height", "dimen", "android")
|
|
val statusBarHeight = if (statusBarHeightId > 0) resources.getDimensionPixelSize(statusBarHeightId) else 0
|
|
return base - statusBarHeight - (density * 20.0).toInt()
|
|
}
|
|
private var resultSent: Boolean = false
|
|
|
|
@SuppressLint("SetJavaScriptEnabled", "AddJavascriptInterface")
|
|
override fun onCreate(savedInstanceState: Bundle?) {
|
|
super.onCreate(savedInstanceState)
|
|
if (receiver == null || params == null) {
|
|
finish()
|
|
return
|
|
}
|
|
requestWindowFeature(Window.FEATURE_NO_TITLE)
|
|
setContentView(R.layout.recaptcha_window)
|
|
webView?.apply {
|
|
webViewClient = object : WebViewClientCompat() {
|
|
fun String.isRecaptchaUrl() = startsWith("https://www.gstatic.com/recaptcha/") || startsWith("https://www.google.com/recaptcha/") || startsWith("https://www.google.com/js/bg/")
|
|
|
|
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 {
|
|
if (url.startsWith("https://support.google.com/recaptcha")) {
|
|
startActivity(Intent("android.intent.action.VIEW", Uri.parse(url)))
|
|
finish()
|
|
return true
|
|
}
|
|
return !url.isRecaptchaUrl()
|
|
}
|
|
}
|
|
settings.apply {
|
|
javaScriptEnabled = true
|
|
useWideViewPort = true
|
|
displayZoomControls = false
|
|
setSupportZoom(false)
|
|
cacheMode = WebSettings.LOAD_NO_CACHE
|
|
}
|
|
addJavascriptInterface(object {
|
|
@JavascriptInterface
|
|
fun challengeReady() {
|
|
Log.d(TAG, "challengeReady()")
|
|
runOnUiThread { webView?.loadUrl("javascript: RecaptchaMFrame.show(${min(widthPixels / density, 400f)}, ${min(heightPixels / density, 400f)});") }
|
|
}
|
|
|
|
@JavascriptInterface
|
|
fun getClientAPIVersion() = 1
|
|
|
|
@JavascriptInterface
|
|
fun onChallengeExpired() {
|
|
Log.d(TAG, "onChallengeExpired()")
|
|
}
|
|
|
|
@JavascriptInterface
|
|
fun onError(errorCode: Int, finish: Boolean) {
|
|
Log.d(TAG, "onError($errorCode, $finish)")
|
|
when (errorCode) {
|
|
1 -> sendErrorResult(ERROR, "Invalid Input Argument")
|
|
2 -> sendErrorResult(TIMEOUT, "Session Timeout")
|
|
7 -> sendErrorResult(RECAPTCHA_INVALID_SITEKEY, "Invalid Site Key")
|
|
8 -> sendErrorResult(RECAPTCHA_INVALID_KEYTYPE, "Invalid Type of Site Key")
|
|
9 -> sendErrorResult(RECAPTCHA_INVALID_PACKAGE_NAME, "Invalid Package Name for App")
|
|
else -> sendErrorResult(ERROR, "error")
|
|
}
|
|
if (finish) this@ReCaptchaActivity.finish()
|
|
}
|
|
|
|
@JavascriptInterface
|
|
fun onResize(width: Int, height: Int) {
|
|
Log.d(TAG, "onResize($width, $height)")
|
|
if (webView?.visibility == View.VISIBLE) {
|
|
runOnUiThread { setWebViewSize(width, height, true) }
|
|
} else {
|
|
runOnUiThread { webView?.loadUrl("javascript: RecaptchaMFrame.shown($width, $height, true);") }
|
|
}
|
|
}
|
|
|
|
@JavascriptInterface
|
|
fun onShow(visible: Boolean, width: Int, height: Int) {
|
|
Log.d(TAG, "onShow($visible, $width, $height)")
|
|
if (width <= 0 && height <= 0) {
|
|
runOnUiThread { webView?.loadUrl("javascript: RecaptchaMFrame.shown($width, $height, $visible);") }
|
|
} else {
|
|
runOnUiThread {
|
|
setWebViewSize(width, height, visible)
|
|
loading?.visibility = if (visible) View.GONE else View.VISIBLE
|
|
webView?.visibility = if (visible) View.VISIBLE else View.GONE
|
|
}
|
|
}
|
|
}
|
|
|
|
@JavascriptInterface
|
|
fun requestToken(s: String, b: Boolean) {
|
|
Log.d(TAG, "requestToken($s, $b)")
|
|
runOnUiThread {
|
|
val cert = webView?.certificate?.let { Base64.encodeToString(SslCertificate.saveState(it).getByteArray("x509-certificate"), Base64.URL_SAFE + Base64.NO_PADDING + Base64.NO_WRAP) }
|
|
?: ""
|
|
val params = StringBuilder(params).appendUrlEncodedParam("c", s).appendUrlEncodedParam("sc", cert).appendUrlEncodedParam("mt", System.currentTimeMillis().toString()).toString()
|
|
val flow = "recaptcha-android-${if (b) "verify" else "reload"}"
|
|
lifecycleScope.launchWhenResumed {
|
|
updateToken(flow, params)
|
|
}
|
|
}
|
|
}
|
|
|
|
@JavascriptInterface
|
|
fun verifyCallback(token: String) {
|
|
Log.d(TAG, "verifyCallback($token)")
|
|
sendResult(0) { putString("token", token) }
|
|
resultSent = true
|
|
finish()
|
|
}
|
|
}, "RecaptchaEmbedder")
|
|
}
|
|
lifecycleScope.launchWhenResumed {
|
|
open()
|
|
}
|
|
}
|
|
|
|
fun sendErrorResult(errorCode: Int, error: String) = sendResult(errorCode) { putString("error", error); putInt("errorCode", errorCode) }
|
|
|
|
fun sendResult(resultCode: Int, v: Bundle.() -> Unit) {
|
|
receiver?.send(resultCode, Bundle().also(v))
|
|
resultSent = true
|
|
}
|
|
|
|
override fun finish() {
|
|
lifecycleScope.launchWhenResumed {
|
|
webView?.loadUrl("javascript: RecaptchaMFrame.shown(0, 0, false);")
|
|
}
|
|
if (!resultSent) {
|
|
sendErrorResult(CANCELED, "Cancelled")
|
|
}
|
|
super.finish()
|
|
}
|
|
|
|
fun setWebViewSize(width: Int, height: Int, visible: Boolean) {
|
|
webView?.apply {
|
|
layoutParams.width = min(widthPixels, (width * density).toInt())
|
|
layoutParams.height = min(heightPixels, (height * density).toInt())
|
|
requestLayout()
|
|
loadUrl("javascript: RecaptchaMFrame.shown(${(layoutParams.width / density).toInt()}, ${(layoutParams.height / density).toInt()}, $visible);")
|
|
}
|
|
}
|
|
|
|
suspend fun updateToken(flow: String, params: String) {
|
|
val map = mapOf("contentBinding" to Base64.encodeToString(MessageDigest.getInstance("SHA-256").digest(params.toByteArray()), Base64.NO_WRAP))
|
|
val dg = try {
|
|
Base64.encodeToString(DroidGuardResultCreator.getResult(this, flow, map), Base64.NO_WRAP + Base64.URL_SAFE + Base64.NO_PADDING)
|
|
} catch (e: Exception) {
|
|
Log.w(TAG, e)
|
|
Base64.encodeToString("ERROR : IOException".toByteArray(), Base64.NO_WRAP + Base64.URL_SAFE + Base64.NO_PADDING)
|
|
}
|
|
if (SDK_INT >= 19) {
|
|
webView?.evaluateJavascript("RecaptchaMFrame.token('${URLEncoder.encode(dg, "UTF-8")}', '$params');", null)
|
|
} else {
|
|
webView?.loadUrl("javascript: RecaptchaMFrame.token('${URLEncoder.encode(dg, "UTF-8")}', '$params');")
|
|
}
|
|
}
|
|
|
|
suspend fun open() {
|
|
val params = StringBuilder(params).appendUrlEncodedParam("mt", System.currentTimeMillis().toString()).toString()
|
|
val map = mapOf("contentBinding" to Base64.encodeToString(MessageDigest.getInstance("SHA-256").digest(params.toByteArray()), Base64.NO_WRAP))
|
|
val dg = try {
|
|
Base64.encodeToString(DroidGuardResultCreator.getResult(this, "recaptcha-android-frame", map), Base64.NO_WRAP + Base64.URL_SAFE + Base64.NO_PADDING)
|
|
} catch (e: Exception) {
|
|
Log.w(TAG, e)
|
|
Base64.encodeToString("ERROR : IOException".toByteArray(), Base64.NO_WRAP + Base64.URL_SAFE + Base64.NO_PADDING)
|
|
}
|
|
webView?.postUrl(MFRAME_URL, "mav=1&dg=${URLEncoder.encode(dg, "UTF-8")}&mp=${URLEncoder.encode(params, "UTF-8")}".toByteArray())
|
|
}
|
|
|
|
companion object {
|
|
private const val MFRAME_URL = "https://www.google.com/recaptcha/api2/mframe"
|
|
}
|
|
}
|