Repo Created
This commit is contained in:
parent
eb305e2886
commit
a8c22c65db
4784 changed files with 329907 additions and 2 deletions
72
play-services-safetynet/core/build.gradle
Normal file
72
play-services-safetynet/core/build.gradle
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 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 {
|
||||
api project(':play-services-safetynet')
|
||||
|
||||
implementation project(':play-services-base-core')
|
||||
implementation project(':play-services-droidguard')
|
||||
implementation project(':play-services-droidguard-core')
|
||||
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.appcompat:appcompat:$appcompatVersion"
|
||||
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 {
|
||||
javaInterop = true
|
||||
}
|
||||
}
|
||||
|
||||
android {
|
||||
namespace "org.microg.gms.safetynet.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-safetynet'
|
||||
29
play-services-safetynet/core/src/main/AndroidManifest.xml
Normal file
29
play-services-safetynet/core/src/main/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
<?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>
|
||||
<service android:name="org.microg.gms.safetynet.SafetyNetClientService">
|
||||
<intent-filter>
|
||||
<action android:name="com.google.android.gms.safetynet.service.START" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
<activity
|
||||
android:name="org.microg.gms.safetynet.ReCaptchaActivity"
|
||||
android:autoRemoveFromRecents="true"
|
||||
android:icon="@drawable/ic_recaptcha"
|
||||
android:process=":ui"
|
||||
android:exported="false"
|
||||
android:theme="@style/Theme.AppCompat.Light.Dialog.NoActionBar">
|
||||
<intent-filter>
|
||||
<action android:name="org.microg.gms.safetynet.RECAPTCHA_ACTIVITY" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
||||
|
|
@ -0,0 +1,206 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 microG Project Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.microg.gms.safetynet;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.Signature;
|
||||
import android.util.Base64;
|
||||
import android.util.Log;
|
||||
|
||||
import org.microg.gms.common.Constants;
|
||||
import org.microg.gms.common.PackageUtils;
|
||||
import org.microg.gms.common.Utils;
|
||||
import org.microg.gms.profile.Build;
|
||||
import org.microg.gms.profile.ProfileManager;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.CharBuffer;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.CharsetDecoder;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.zip.GZIPInputStream;
|
||||
|
||||
import okio.ByteString;
|
||||
|
||||
public class Attestation {
|
||||
private static final String TAG = "GmsSafetyNetAttest";
|
||||
|
||||
private Context context;
|
||||
private String packageName;
|
||||
private byte[] payload;
|
||||
private String droidGuardResult;
|
||||
|
||||
public Attestation(Context context, String packageName) {
|
||||
this.context = context;
|
||||
this.packageName = packageName;
|
||||
}
|
||||
|
||||
public void setPayload(byte[] payload) {
|
||||
this.payload = payload;
|
||||
}
|
||||
|
||||
public SafetyNetData buildPayload(byte[] nonce) {
|
||||
this.droidGuardResult = null;
|
||||
SafetyNetData payload = new SafetyNetData.Builder()
|
||||
.nonce(ByteString.of(nonce))
|
||||
.currentTimeMs(System.currentTimeMillis())
|
||||
.packageName(packageName)
|
||||
.fileDigest(getPackageFileDigest())
|
||||
.signatureDigest(getPackageSignatures())
|
||||
.gmsVersionCode(Constants.GMS_VERSION_CODE)
|
||||
//.googleCn(false)
|
||||
.seLinuxState(new SELinuxState.Builder().enabled(true).supported(true).build())
|
||||
.suCandidates(Collections.<FileState>emptyList())
|
||||
.build();
|
||||
Log.d(TAG, "Payload: "+payload.toString());
|
||||
this.payload = payload.encode();
|
||||
return payload;
|
||||
}
|
||||
|
||||
public byte[] getPayload() {
|
||||
return payload;
|
||||
}
|
||||
|
||||
public String getPayloadHashBase64() {
|
||||
try {
|
||||
MessageDigest digest = getSha256Digest();
|
||||
return Base64.encodeToString(digest.digest(payload), Base64.NO_WRAP);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
Log.w(TAG, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static MessageDigest getSha256Digest() throws NoSuchAlgorithmException {
|
||||
return MessageDigest.getInstance("SHA-256");
|
||||
}
|
||||
|
||||
public void setDroidGuardResult(String droidGuardResult) {
|
||||
this.droidGuardResult = droidGuardResult;
|
||||
}
|
||||
|
||||
private ByteString getPackageFileDigest() {
|
||||
try {
|
||||
return ByteString.of(getPackageFileDigest(context, packageName));
|
||||
} catch (Exception e) {
|
||||
Log.w(TAG, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] getPackageFileDigest(Context context, String packageName) throws Exception {
|
||||
FileInputStream is = new FileInputStream(new File(context.getPackageManager().getApplicationInfo(packageName, 0).sourceDir));
|
||||
MessageDigest digest = getSha256Digest();
|
||||
byte[] data = new byte[4096];
|
||||
while (true) {
|
||||
int read = is.read(data);
|
||||
if (read < 0) break;
|
||||
digest.update(data, 0, read);
|
||||
}
|
||||
is.close();
|
||||
return digest.digest();
|
||||
}
|
||||
|
||||
@SuppressLint("PackageManagerGetSignatures")
|
||||
private List<ByteString> getPackageSignatures() {
|
||||
try {
|
||||
ArrayList<ByteString> res = new ArrayList<>();
|
||||
for (byte[] bytes : getPackageSignatures(context, packageName)) {
|
||||
res.add(ByteString.of(bytes));
|
||||
}
|
||||
return res;
|
||||
} catch (Exception e) {
|
||||
Log.w(TAG, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[][] getPackageSignatures(Context context, String packageName) throws Exception {
|
||||
PackageInfo pi = context.getPackageManager().getPackageInfo(packageName, PackageManager.GET_SIGNATURES);
|
||||
ArrayList<byte[]> res = new ArrayList<>();
|
||||
MessageDigest digest = getSha256Digest();
|
||||
for (Signature signature : pi.signatures) {
|
||||
res.add(digest.digest(signature.toByteArray()));
|
||||
}
|
||||
return res.toArray(new byte[][]{});
|
||||
}
|
||||
|
||||
public String attest(String apiKey) throws IOException {
|
||||
if (payload == null) {
|
||||
throw new IllegalStateException("missing payload");
|
||||
}
|
||||
return attest(new AttestRequest.Builder().safetyNetData(ByteString.of(payload)).droidGuardResult(droidGuardResult).build(), apiKey).result;
|
||||
}
|
||||
|
||||
private AttestResponse attest(AttestRequest request, String apiKey) throws IOException {
|
||||
ProfileManager.ensureInitialized(context);
|
||||
String requestUrl = "https://www.googleapis.com/androidcheck/v1/attestations/attest?alt=PROTO&key=" + apiKey;
|
||||
HttpURLConnection connection = (HttpURLConnection) new URL(requestUrl).openConnection();
|
||||
connection.setRequestMethod("POST");
|
||||
connection.setDoInput(true);
|
||||
connection.setDoOutput(true);
|
||||
connection.setRequestProperty("content-type", "application/x-protobuf");
|
||||
connection.setRequestProperty("Accept-Encoding", "gzip");
|
||||
connection.setRequestProperty("X-Android-Package", packageName);
|
||||
connection.setRequestProperty("X-Android-Cert", PackageUtils.firstSignatureDigest(context, packageName));
|
||||
connection.setRequestProperty("User-Agent", "SafetyNet/" + Constants.GMS_VERSION_CODE + " (" + Build.DEVICE + " " + Build.ID + "); gzip");
|
||||
|
||||
OutputStream os = connection.getOutputStream();
|
||||
os.write(request.encode());
|
||||
os.close();
|
||||
|
||||
if (connection.getResponseCode() != 200) {
|
||||
byte[] bytes = null;
|
||||
String ex = null;
|
||||
try {
|
||||
bytes = Utils.readStreamToEnd(connection.getErrorStream());
|
||||
ex = new String(Utils.readStreamToEnd(new GZIPInputStream(new ByteArrayInputStream(bytes))));
|
||||
} catch (Exception e) {
|
||||
if (bytes != null) {
|
||||
throw new IOException(getBytesAsString(bytes), e);
|
||||
}
|
||||
throw new IOException(connection.getResponseMessage(), e);
|
||||
}
|
||||
throw new IOException(ex);
|
||||
}
|
||||
|
||||
InputStream is = connection.getInputStream();
|
||||
byte[] bytes = Utils.readStreamToEnd(new GZIPInputStream(is));
|
||||
try {
|
||||
return AttestResponse.ADAPTER.decode(bytes);
|
||||
} catch (IOException e) {
|
||||
Log.d(TAG, Base64.encodeToString(bytes, 0));
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private String getBytesAsString(byte[] bytes) {
|
||||
if (bytes == null) return "null";
|
||||
try {
|
||||
CharsetDecoder d = Charset.forName("US-ASCII").newDecoder();
|
||||
CharBuffer r = d.decode(ByteBuffer.wrap(bytes));
|
||||
return r.toString();
|
||||
} catch (Exception e) {
|
||||
return Base64.encodeToString(bytes, Base64.NO_WRAP);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,240 @@
|
|||
/*
|
||||
* 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.droidguard.DroidGuardClient
|
||||
import com.google.android.gms.safetynet.SafetyNetStatusCodes.*
|
||||
import com.google.android.gms.tasks.await
|
||||
import org.microg.gms.profile.Build
|
||||
import org.microg.gms.profile.ProfileManager
|
||||
import org.microg.gms.safetynet.core.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
|
||||
ProfileManager.ensureInitialized(this@ReCaptchaActivity)
|
||||
userAgentString = Build.generateWebViewUserAgentString(userAgentString)
|
||||
}
|
||||
addJavascriptInterface(ReCaptchaEmbedder(this@ReCaptchaActivity), "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 {
|
||||
DroidGuardClient.getResults(this, flow, map).await()
|
||||
} 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 {
|
||||
DroidGuardClient.getResults(this, "recaptcha-android-frame", map).await()
|
||||
} 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"
|
||||
|
||||
class ReCaptchaEmbedder(val activity: ReCaptchaActivity) {
|
||||
@JavascriptInterface
|
||||
fun challengeReady() {
|
||||
Log.d(TAG, "challengeReady()")
|
||||
activity.runOnUiThread { activity.webView?.loadUrl("javascript: RecaptchaMFrame.show(${min(activity.widthPixels / activity.density, 400f)}, ${min(activity.heightPixels / activity.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 -> activity.sendErrorResult(ERROR, "Invalid Input Argument")
|
||||
2 -> activity.sendErrorResult(TIMEOUT, "Session Timeout")
|
||||
7 -> activity.sendErrorResult(RECAPTCHA_INVALID_SITEKEY, "Invalid Site Key")
|
||||
8 -> activity.sendErrorResult(RECAPTCHA_INVALID_KEYTYPE, "Invalid Type of Site Key")
|
||||
9 -> activity.sendErrorResult(RECAPTCHA_INVALID_PACKAGE_NAME, "Invalid Package Name for App")
|
||||
else -> activity.sendErrorResult(ERROR, "error")
|
||||
}
|
||||
if (finish) activity.finish()
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
fun onResize(width: Int, height: Int) {
|
||||
Log.d(TAG, "onResize($width, $height)")
|
||||
if (activity.webView?.visibility == View.VISIBLE) {
|
||||
activity.runOnUiThread { activity.setWebViewSize(width, height, true) }
|
||||
} else {
|
||||
activity.runOnUiThread { activity.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) {
|
||||
activity.runOnUiThread { activity.webView?.loadUrl("javascript: RecaptchaMFrame.shown($width, $height, $visible);") }
|
||||
} else {
|
||||
activity.runOnUiThread {
|
||||
activity.setWebViewSize(width, height, visible)
|
||||
activity.loading?.visibility = if (visible) View.GONE else View.VISIBLE
|
||||
activity.webView?.visibility = if (visible) View.VISIBLE else View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
fun requestToken(s: String, b: Boolean) {
|
||||
Log.d(TAG, "requestToken($s, $b)")
|
||||
activity.runOnUiThread {
|
||||
val cert = activity.webView?.certificate?.let { Base64.encodeToString(SslCertificate.saveState(it).getByteArray("x509-certificate"), Base64.URL_SAFE + Base64.NO_PADDING + Base64.NO_WRAP) }
|
||||
?: ""
|
||||
val params = StringBuilder(activity.params).appendUrlEncodedParam("c", s).appendUrlEncodedParam("sc", cert).appendUrlEncodedParam("mt", System.currentTimeMillis().toString()).toString()
|
||||
val flow = "recaptcha-android-${if (b) "verify" else "reload"}"
|
||||
activity.lifecycleScope.launchWhenResumed {
|
||||
activity.updateToken(flow, params)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
fun verifyCallback(token: String) {
|
||||
Log.d(TAG, "verifyCallback($token)")
|
||||
activity.sendResult(0) { putString("token", token) }
|
||||
activity.resultSent = true
|
||||
activity.finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,277 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 microG Project Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
package org.microg.gms.safetynet
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.database.Cursor
|
||||
import android.os.Build.VERSION.SDK_INT
|
||||
import android.os.Bundle
|
||||
import android.os.Parcel
|
||||
import android.os.ResultReceiver
|
||||
import android.util.Base64
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.google.android.gms.common.api.Status
|
||||
import com.google.android.gms.common.internal.GetServiceRequest
|
||||
import com.google.android.gms.common.internal.IGmsCallbacks
|
||||
import com.google.android.gms.droidguard.DroidGuardClient
|
||||
import com.google.android.gms.safetynet.AttestationData
|
||||
import com.google.android.gms.safetynet.HarmfulAppsInfo
|
||||
import com.google.android.gms.safetynet.RecaptchaResultData
|
||||
import com.google.android.gms.safetynet.SafeBrowsingData
|
||||
import com.google.android.gms.safetynet.SafetyNetStatusCodes
|
||||
import com.google.android.gms.safetynet.internal.ISafetyNetCallbacks
|
||||
import com.google.android.gms.safetynet.internal.ISafetyNetService
|
||||
import com.google.android.gms.tasks.await
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.microg.gms.BaseService
|
||||
import org.microg.gms.common.GmsService
|
||||
import org.microg.gms.common.GooglePackagePermission
|
||||
import org.microg.gms.common.PackageUtils
|
||||
import org.microg.gms.droidguard.core.DroidGuardPreferences
|
||||
import org.microg.gms.settings.SettingsContract
|
||||
import org.microg.gms.settings.SettingsContract.CheckIn.getContentUri
|
||||
import org.microg.gms.settings.SettingsContract.getSettings
|
||||
import org.microg.gms.utils.warnOnTransactionIssues
|
||||
import java.io.IOException
|
||||
import java.net.URLEncoder
|
||||
|
||||
private const val TAG = "GmsSafetyNet"
|
||||
private const val DEFAULT_API_KEY = "AIzaSyDqVnJBjE5ymo--oBJt3On7HQx9xNm1RHA"
|
||||
|
||||
class SafetyNetClientService : BaseService(TAG, GmsService.SAFETY_NET_CLIENT) {
|
||||
override fun handleServiceRequest(callback: IGmsCallbacks, request: GetServiceRequest, service: GmsService) {
|
||||
callback.onPostInitComplete(0, SafetyNetClientServiceImpl(this, request.packageName, lifecycle), null)
|
||||
}
|
||||
}
|
||||
|
||||
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 SafetyNetClientServiceImpl(
|
||||
private val context: Context,
|
||||
private val packageName: String,
|
||||
override val lifecycle: Lifecycle
|
||||
) : ISafetyNetService.Stub(), LifecycleOwner {
|
||||
|
||||
override fun attest(callbacks: ISafetyNetCallbacks, nonce: ByteArray) {
|
||||
attestWithApiKey(callbacks, nonce, DEFAULT_API_KEY)
|
||||
}
|
||||
|
||||
override fun attestWithApiKey(callbacks: ISafetyNetCallbacks, nonce: ByteArray?, apiKey: String) {
|
||||
if (nonce == null) {
|
||||
callbacks.onAttestationResult(Status(SafetyNetStatusCodes.DEVELOPER_ERROR, "Nonce missing"), null)
|
||||
return
|
||||
}
|
||||
|
||||
if (!SafetyNetPreferences.isEnabled(context)) {
|
||||
Log.d(TAG, "ignoring SafetyNet request, SafetyNet is disabled")
|
||||
callbacks.onAttestationResult(Status(SafetyNetStatusCodes.ERROR, "Disabled"), null)
|
||||
return
|
||||
}
|
||||
|
||||
if (!DroidGuardPreferences.isAvailable(context)) {
|
||||
Log.d(TAG, "ignoring SafetyNet request, DroidGuard is disabled")
|
||||
callbacks.onAttestationResult(Status(SafetyNetStatusCodes.ERROR, "Unsupported"), null)
|
||||
return
|
||||
}
|
||||
|
||||
lifecycleScope.launchWhenStarted {
|
||||
val db = SafetyNetDatabase(context)
|
||||
var requestID: Long = -1
|
||||
try {
|
||||
val attestation = Attestation(context, packageName)
|
||||
val safetyNetData = attestation.buildPayload(nonce)
|
||||
|
||||
requestID = db.insertRecentRequestStart(
|
||||
SafetyNetRequestType.ATTESTATION,
|
||||
safetyNetData.packageName,
|
||||
safetyNetData.nonce?.toByteArray(),
|
||||
safetyNetData.currentTimeMs ?: 0
|
||||
)
|
||||
|
||||
val data = mapOf("contentBinding" to attestation.payloadHashBase64)
|
||||
val dg = withContext(Dispatchers.IO) { DroidGuardClient.getResults(context, "attest", data).await() }
|
||||
attestation.setDroidGuardResult(dg)
|
||||
val jwsResult = withContext(Dispatchers.IO) { attestation.attest(apiKey) }
|
||||
|
||||
|
||||
val jsonData = try {
|
||||
requireNotNull(jwsResult)
|
||||
jwsResult.split(".").let {
|
||||
assert(it.size == 3)
|
||||
return@let Base64.decode(it[1], Base64.URL_SAFE).decodeToString()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Log.w(TAG, "An exception occurred when parsing the JWS token.")
|
||||
null
|
||||
}
|
||||
|
||||
db.insertRecentRequestEnd(requestID, Status.SUCCESS, jsonData)
|
||||
callbacks.onAttestationResult(Status.SUCCESS, AttestationData(jwsResult))
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Exception during attest: ${e.javaClass.name}", e)
|
||||
val code = when (e) {
|
||||
is IOException -> SafetyNetStatusCodes.NETWORK_ERROR
|
||||
else -> SafetyNetStatusCodes.ERROR
|
||||
}
|
||||
val status = Status(code, e.localizedMessage)
|
||||
|
||||
// This shouldn't happen, but do not update the database if it didn't insert the start of the request
|
||||
if (requestID != -1L) db.insertRecentRequestEnd(requestID, status, null)
|
||||
try {
|
||||
callbacks.onAttestationResult(Status(code, e.localizedMessage), null)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Exception while sending error", e)
|
||||
}
|
||||
}
|
||||
db.close()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getSharedUuid(callbacks: ISafetyNetCallbacks) {
|
||||
PackageUtils.checkPackageUid(context, packageName, getCallingUid())
|
||||
PackageUtils.assertGooglePackagePermission(context, GooglePackagePermission.SAFETYNET)
|
||||
|
||||
// TODO
|
||||
Log.d(TAG, "dummy Method: getSharedUuid")
|
||||
callbacks.onSharedUuid("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa")
|
||||
}
|
||||
|
||||
override fun lookupUri(callbacks: ISafetyNetCallbacks, apiKey: String, threatTypes: IntArray, i: Int, s2: String) {
|
||||
Log.d(TAG, "unimplemented Method: lookupUri")
|
||||
callbacks.onSafeBrowsingData(Status.SUCCESS, SafeBrowsingData())
|
||||
}
|
||||
|
||||
override fun enableVerifyApps(callbacks: ISafetyNetCallbacks) {
|
||||
Log.d(TAG, "dummy Method: enableVerifyApps")
|
||||
callbacks.onVerifyAppsUserResult(Status.SUCCESS, true)
|
||||
}
|
||||
|
||||
override fun listHarmfulApps(callbacks: ISafetyNetCallbacks) {
|
||||
Log.d(TAG, "dummy Method: listHarmfulApps")
|
||||
callbacks.onHarmfulAppsInfo(Status.SUCCESS, HarmfulAppsInfo().apply {
|
||||
lastScanTime = ((System.currentTimeMillis() - VERIFY_APPS_LAST_SCAN_DELAY) / VERIFY_APPS_LAST_SCAN_TIME_ROUNDING) * VERIFY_APPS_LAST_SCAN_TIME_ROUNDING + VERIFY_APPS_LAST_SCAN_OFFSET
|
||||
})
|
||||
}
|
||||
|
||||
override fun verifyWithRecaptcha(callbacks: ISafetyNetCallbacks, siteKey: String?) {
|
||||
if (siteKey == null) {
|
||||
callbacks.onRecaptchaResult(Status(SafetyNetStatusCodes.RECAPTCHA_INVALID_SITEKEY, "SiteKey missing"), null)
|
||||
return
|
||||
}
|
||||
|
||||
if (!SafetyNetPreferences.isEnabled(context)) {
|
||||
Log.d(TAG, "ignoring SafetyNet request, SafetyNet is disabled")
|
||||
callbacks.onRecaptchaResult(Status(SafetyNetStatusCodes.ERROR, "Disabled"), null)
|
||||
return
|
||||
}
|
||||
|
||||
val db = SafetyNetDatabase(context)
|
||||
val requestID = db.insertRecentRequestStart(
|
||||
SafetyNetRequestType.RECAPTCHA,
|
||||
context.packageName,
|
||||
null,
|
||||
System.currentTimeMillis()
|
||||
)
|
||||
|
||||
val intent = Intent("org.microg.gms.safetynet.RECAPTCHA_ACTIVITY")
|
||||
intent.`package` = context.packageName
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS)
|
||||
val androidId = getSettings(
|
||||
context,
|
||||
getContentUri(context),
|
||||
arrayOf(SettingsContract.CheckIn.ANDROID_ID)
|
||||
) { cursor: Cursor -> cursor.getLong(0) }
|
||||
val params = StringBuilder()
|
||||
|
||||
val (packageFileDigest, packageSignatures) = try {
|
||||
Pair(
|
||||
Base64.encodeToString(
|
||||
Attestation.getPackageFileDigest(context, packageName),
|
||||
Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING
|
||||
),
|
||||
Attestation.getPackageSignatures(context, packageName)
|
||||
.map { Base64.encodeToString(it, Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING) }
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
db.insertRecentRequestEnd(requestID, Status(SafetyNetStatusCodes.ERROR, e.localizedMessage), null)
|
||||
db.close()
|
||||
callbacks.onRecaptchaResult(Status(SafetyNetStatusCodes.ERROR, e.localizedMessage), null)
|
||||
return
|
||||
}
|
||||
|
||||
params.appendUrlEncodedParam("k", siteKey)
|
||||
.appendUrlEncodedParam("di", androidId.toString())
|
||||
.appendUrlEncodedParam("pk", packageName)
|
||||
.appendUrlEncodedParam("sv", SDK_INT.toString())
|
||||
.appendUrlEncodedParam("gv", "20.47.14 (040306-{{cl}})")
|
||||
.appendUrlEncodedParam("gm", "260")
|
||||
.appendUrlEncodedParam("as", packageFileDigest)
|
||||
for (signature in packageSignatures) {
|
||||
Log.d(TAG, "Sig: $signature")
|
||||
params.appendUrlEncodedParam("ac", signature)
|
||||
}
|
||||
params.appendUrlEncodedParam("ip", "com.android.vending")
|
||||
.appendUrlEncodedParam("av", false.toString())
|
||||
.appendUrlEncodedParam("si", null)
|
||||
intent.putExtra("params", params.toString())
|
||||
intent.putExtra("result", object : ResultReceiver(null) {
|
||||
override fun onReceiveResult(resultCode: Int, resultData: Bundle) {
|
||||
if (resultCode != 0) {
|
||||
db.insertRecentRequestEnd(
|
||||
requestID,
|
||||
Status(resultData.getInt("errorCode"), resultData.getString("error")),
|
||||
null
|
||||
)
|
||||
db.close()
|
||||
callbacks.onRecaptchaResult(
|
||||
Status(resultData.getInt("errorCode"), resultData.getString("error")),
|
||||
null
|
||||
)
|
||||
} else {
|
||||
db.insertRecentRequestEnd(requestID, Status.SUCCESS, resultData.getString("token"))
|
||||
db.close()
|
||||
callbacks.onRecaptchaResult(
|
||||
Status.SUCCESS,
|
||||
RecaptchaResultData().apply { token = resultData.getString("token") })
|
||||
}
|
||||
}
|
||||
})
|
||||
context.startActivity(intent)
|
||||
}
|
||||
|
||||
override fun initSafeBrowsing(callbacks: ISafetyNetCallbacks) {
|
||||
Log.d(TAG, "dummy: initSafeBrowsing")
|
||||
callbacks.onInitSafeBrowsingResult(Status.SUCCESS)
|
||||
}
|
||||
|
||||
override fun shutdownSafeBrowsing() {
|
||||
Log.d(TAG, "dummy: shutdownSafeBrowsing")
|
||||
}
|
||||
|
||||
override fun isVerifyAppsEnabled(callbacks: ISafetyNetCallbacks) {
|
||||
Log.d(TAG, "dummy: isVerifyAppsEnabled")
|
||||
callbacks.onVerifyAppsUserResult(Status.SUCCESS, true)
|
||||
}
|
||||
|
||||
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 {
|
||||
// We simulate one scan every day, which will happen at 03:12:02.121 and will be available 32 seconds later
|
||||
const val VERIFY_APPS_LAST_SCAN_DELAY = 32 * 1000L
|
||||
const val VERIFY_APPS_LAST_SCAN_OFFSET = ((3 * 60 + 12) * 60 + 2) * 1000L + 121
|
||||
const val VERIFY_APPS_LAST_SCAN_TIME_ROUNDING = 24 * 60 * 60 * 1000L
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,154 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2016, microG Project Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
package org.microg.gms.safetynet
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import android.database.sqlite.SQLiteOpenHelper
|
||||
import android.os.Build.VERSION.SDK_INT
|
||||
import android.util.Log
|
||||
import com.google.android.gms.common.api.Status
|
||||
|
||||
class SafetyNetDatabase(context: Context) : SQLiteOpenHelper(context, DB_NAME, null, DB_VERSION) {
|
||||
|
||||
init {
|
||||
if (SDK_INT >= 16) {
|
||||
setWriteAheadLoggingEnabled(true)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOpen(db: SQLiteDatabase) {
|
||||
if (!db.isReadOnly) clearOldRequests(db)
|
||||
}
|
||||
|
||||
private fun createSafetyNetSummary(cursor: Cursor): SafetyNetSummary {
|
||||
val summary = SafetyNetSummary(
|
||||
SafetyNetRequestType.valueOf(
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(FIELD_REQUEST_TYPE))
|
||||
),
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(FIELD_PACKAGE_NAME)),
|
||||
cursor.getBlob(cursor.getColumnIndexOrThrow(FIELD_NONCE)),
|
||||
cursor.getLong(cursor.getColumnIndexOrThrow(FIELD_TIMESTAMP))
|
||||
)
|
||||
summary.id = cursor.getInt(cursor.getColumnIndexOrThrow(FIELD_ID))
|
||||
if (cursor.isNull(cursor.getColumnIndexOrThrow(FIELD_RESULT_STATUS_CODE))) return summary
|
||||
summary.responseStatus = Status(
|
||||
cursor.getInt(cursor.getColumnIndexOrThrow(FIELD_RESULT_STATUS_CODE)),
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(FIELD_RESULT_STATUS_MSG))
|
||||
)
|
||||
summary.responseData = cursor.getString(cursor.getColumnIndexOrThrow(FIELD_RESULT_DATA))
|
||||
return summary
|
||||
}
|
||||
|
||||
val recentApps: List<Pair<String, Long>>
|
||||
get() {
|
||||
val db = readableDatabase
|
||||
val cursor = db.query(TABLE_RECENTS, arrayOf(FIELD_PACKAGE_NAME, "MAX($FIELD_TIMESTAMP)"), null, null, FIELD_PACKAGE_NAME, null, "MAX($FIELD_TIMESTAMP) DESC")
|
||||
if (cursor != null) {
|
||||
val result = ArrayList<Pair<String, Long>>()
|
||||
while (cursor.moveToNext()) {
|
||||
result.add(cursor.getString(0) to cursor.getLong(1))
|
||||
}
|
||||
cursor.close()
|
||||
return result
|
||||
}
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
fun getRecentRequests(packageName: String): List<SafetyNetSummary> {
|
||||
val db = readableDatabase
|
||||
val cursor = db.query(TABLE_RECENTS, null, "$FIELD_PACKAGE_NAME = ?", arrayOf(packageName), null, null, "$FIELD_TIMESTAMP DESC")
|
||||
if (cursor != null) {
|
||||
val result: MutableList<SafetyNetSummary> = ArrayList()
|
||||
while (cursor.moveToNext()) {
|
||||
result.add(createSafetyNetSummary(cursor))
|
||||
}
|
||||
cursor.close()
|
||||
return result
|
||||
}
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
fun insertRecentRequestStart(
|
||||
requestType: SafetyNetRequestType,
|
||||
packageName: String?,
|
||||
nonce: ByteArray?,
|
||||
timestamp: Long
|
||||
): Long {
|
||||
val db = writableDatabase
|
||||
val cv = ContentValues()
|
||||
cv.put(FIELD_REQUEST_TYPE, requestType.name)
|
||||
cv.put(FIELD_PACKAGE_NAME, packageName)
|
||||
cv.put(FIELD_NONCE, nonce)
|
||||
cv.put(FIELD_TIMESTAMP, timestamp)
|
||||
return db.insert(TABLE_RECENTS, null, cv)
|
||||
}
|
||||
|
||||
fun insertRecentRequestEnd(id: Long, status: Status, resultData: String?) {
|
||||
val db = writableDatabase
|
||||
val cv = ContentValues()
|
||||
cv.put(FIELD_RESULT_STATUS_CODE, status.statusCode)
|
||||
cv.put(FIELD_RESULT_STATUS_MSG, status.statusMessage)
|
||||
cv.put(FIELD_RESULT_DATA, resultData)
|
||||
db.update(TABLE_RECENTS, cv, "$FIELD_ID = ?", arrayOf(id.toString()))
|
||||
}
|
||||
|
||||
fun clearOldRequests(db: SQLiteDatabase) {
|
||||
val timeout = 1000 * 60 * 60 * 24 * 14 // 14 days
|
||||
val maxRequests = 150
|
||||
var rows = 0
|
||||
|
||||
rows += db.compileStatement(
|
||||
"DELETE FROM $TABLE_RECENTS WHERE $FIELD_ID NOT IN " +
|
||||
"(SELECT $FIELD_ID FROM $TABLE_RECENTS ORDER BY $FIELD_TIMESTAMP LIMIT $maxRequests)"
|
||||
).executeUpdateDelete()
|
||||
|
||||
val sqLiteStatement = db.compileStatement("DELETE FROM $TABLE_RECENTS WHERE $FIELD_TIMESTAMP + ? < ?")
|
||||
sqLiteStatement.bindLong(1, timeout.toLong())
|
||||
sqLiteStatement.bindLong(2, System.currentTimeMillis())
|
||||
rows += sqLiteStatement.executeUpdateDelete()
|
||||
|
||||
if (rows != 0) Log.d(TAG, "Cleared $rows old request(s)")
|
||||
}
|
||||
|
||||
fun clearAllRequests() {
|
||||
val db = writableDatabase
|
||||
db.execSQL("DELETE FROM $TABLE_RECENTS")
|
||||
}
|
||||
|
||||
override fun onCreate(db: SQLiteDatabase) {
|
||||
db.execSQL(CREATE_TABLE_RECENTS)
|
||||
}
|
||||
|
||||
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||
throw IllegalStateException("Upgrades not supported")
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = SafetyNetDatabase::class.java.simpleName
|
||||
private const val DB_NAME = "snet.db"
|
||||
private const val DB_VERSION = 1
|
||||
private const val CREATE_TABLE_RECENTS = "CREATE TABLE recents (" +
|
||||
"id INTEGER PRIMARY KEY AUTOINCREMENT ," +
|
||||
"request_type TEXT," +
|
||||
"package_name TEXT," +
|
||||
"nonce TEXT," +
|
||||
"timestamp INTEGER," +
|
||||
"result_status_code INTEGER DEFAULT NULL," +
|
||||
"result_status_msg TEXT DEFAULT NULL," +
|
||||
"result_data TEXT DEFAULT NULL)"
|
||||
private const val TABLE_RECENTS = "recents"
|
||||
private const val FIELD_ID = "id"
|
||||
private const val FIELD_REQUEST_TYPE = "request_type"
|
||||
private const val FIELD_PACKAGE_NAME = "package_name"
|
||||
private const val FIELD_NONCE = "nonce"
|
||||
private const val FIELD_TIMESTAMP = "timestamp"
|
||||
private const val FIELD_RESULT_STATUS_CODE = "result_status_code"
|
||||
private const val FIELD_RESULT_STATUS_MSG = "result_status_msg"
|
||||
private const val FIELD_RESULT_DATA = "result_data"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 microG Project Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.microg.gms.safetynet
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import org.microg.gms.settings.SettingsContract
|
||||
import org.microg.gms.settings.SettingsContract.SafetyNet.ENABLED
|
||||
|
||||
object SafetyNetPreferences {
|
||||
private fun <T> getSettings(context: Context, projection: String, def: T, f: (Cursor) -> T): T {
|
||||
return try {
|
||||
SettingsContract.getSettings(context, SettingsContract.SafetyNet.getContentUri(context), arrayOf(projection), f)
|
||||
} catch (e: Exception) {
|
||||
def
|
||||
}
|
||||
}
|
||||
|
||||
private fun setSettings(context: Context, f: ContentValues.() -> Unit) =
|
||||
SettingsContract.setSettings(context, SettingsContract.SafetyNet.getContentUri(context), f)
|
||||
|
||||
@JvmStatic
|
||||
fun isEnabled(context: Context): Boolean = getSettings(context, ENABLED, false) { it.getInt(0) != 0 }
|
||||
|
||||
@JvmStatic
|
||||
fun setEnabled(context: Context, enabled: Boolean) = setSettings(context) { put(ENABLED, enabled) }
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package org.microg.gms.safetynet
|
||||
|
||||
enum class SafetyNetRequestType {
|
||||
ATTESTATION,
|
||||
RECAPTCHA,
|
||||
RECAPTCHA_ENTERPRISE,
|
||||
;
|
||||
}
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
package org.microg.gms.safetynet
|
||||
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import com.google.android.gms.common.api.Status
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
data class SafetyNetSummary(
|
||||
val requestType: SafetyNetRequestType,
|
||||
|
||||
|
||||
// request data
|
||||
val packageName: String,
|
||||
val nonce: ByteArray?, // null with SafetyNetRequestType::RECAPTCHA
|
||||
val timestamp: Long,
|
||||
|
||||
) : Parcelable {
|
||||
|
||||
var id by Delegates.notNull<Int>()
|
||||
|
||||
// response data
|
||||
// note : responseStatus do not represent the actual status in case of an attestation, it will be in resultData
|
||||
var responseStatus: Status? = null
|
||||
var responseData: String? = null
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as SafetyNetSummary
|
||||
|
||||
if (requestType != other.requestType) return false
|
||||
if (packageName != other.packageName) return false
|
||||
if (!nonce.contentEquals(other.nonce)) return false
|
||||
if (responseStatus != other.responseStatus) return false
|
||||
if (responseData != other.responseData) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = requestType.hashCode()
|
||||
result = 31 * result + packageName.hashCode()
|
||||
result = 31 * result + nonce.hashCode()
|
||||
result = 31 * result + (responseStatus?.hashCode() ?: 0)
|
||||
result = 31 * result + (responseData?.hashCode() ?: 0)
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
// Parcelable implementation
|
||||
|
||||
constructor(parcel: Parcel) : this(
|
||||
SafetyNetRequestType.valueOf(parcel.readString()!!),
|
||||
parcel.readString()!!,
|
||||
parcel.createByteArray(),
|
||||
parcel.readLong()
|
||||
) {
|
||||
responseStatus = parcel.readParcelable(Status::class.java.classLoader)
|
||||
responseData = parcel.readString()
|
||||
}
|
||||
|
||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||
parcel.writeString(requestType.name)
|
||||
parcel.writeString(packageName)
|
||||
parcel.writeByteArray(nonce)
|
||||
parcel.writeLong(timestamp)
|
||||
parcel.writeParcelable(responseStatus, flags)
|
||||
parcel.writeString(responseData)
|
||||
}
|
||||
|
||||
override fun describeContents(): Int {
|
||||
return 0
|
||||
}
|
||||
|
||||
companion object CREATOR : Parcelable.Creator<SafetyNetSummary> {
|
||||
override fun createFromParcel(parcel: Parcel): SafetyNetSummary {
|
||||
return SafetyNetSummary(parcel)
|
||||
}
|
||||
|
||||
override fun newArray(size: Int): Array<SafetyNetSummary?> {
|
||||
return arrayOfNulls(size)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
34
play-services-safetynet/core/src/main/proto/safetynet.proto
Normal file
34
play-services-safetynet/core/src/main/proto/safetynet.proto
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
option java_package = "org.microg.gms.safetynet";
|
||||
|
||||
option java_outer_classname = "SafetyNetProto";
|
||||
|
||||
message SELinuxState {
|
||||
optional bool supported = 1;
|
||||
optional bool enabled = 2;
|
||||
}
|
||||
|
||||
message FileState {
|
||||
optional string fileName = 1;
|
||||
optional bytes digest = 2;
|
||||
}
|
||||
|
||||
message SafetyNetData {
|
||||
optional bytes nonce = 1;
|
||||
optional string packageName = 2;
|
||||
repeated bytes signatureDigest = 3;
|
||||
optional bytes fileDigest = 4;
|
||||
optional int32 gmsVersionCode = 5;
|
||||
repeated FileState suCandidates = 6;
|
||||
optional SELinuxState seLinuxState = 7;
|
||||
optional int64 currentTimeMs = 8;
|
||||
optional bool googleCn = 9;
|
||||
}
|
||||
|
||||
message AttestRequest {
|
||||
optional bytes safetyNetData = 1;
|
||||
optional string droidGuardResult = 2;
|
||||
}
|
||||
|
||||
message AttestResponse {
|
||||
optional string result = 2;
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
<!--
|
||||
~ SPDX-FileCopyrightText: 2021, microG Project Team
|
||||
~ SPDX-License-Identifier: Apache-2.0
|
||||
-->
|
||||
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="48dp"
|
||||
android:height="48dp"
|
||||
android:viewportWidth="48"
|
||||
android:viewportHeight="48">
|
||||
<path
|
||||
android:pathData="M2,24L2,40.5L5.9387,36.5616a22,22 45,0 0,9.6423 7.7639,22 22,0 0,0 23.9753,-4.7692L31.071,31.071A10,10 0,0 1,24 34,10 10,135 0,1 14.7361,27.7642L18.5,24L14,24Z"
|
||||
android:strokeWidth="0"
|
||||
android:fillColor="#bdbdbd"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:pathData="M7.5,2 L11.4386,5.9387A22,22 0,0 0,2 24L14,24A10,10 135,0 1,20.236 14.7361L24,18.5L24,14 24,2l-0.0682,0z"
|
||||
android:strokeWidth="0"
|
||||
android:strokeColor="#000000">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:startY="24"
|
||||
android:startX="14"
|
||||
android:endY="20.75"
|
||||
android:endX="14.000"
|
||||
android:type="linear">
|
||||
<item android:offset="0" android:color="#FF1E88E5"/>
|
||||
<item android:offset="1" android:color="#FF2196F3"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:pathData="M46,7.5 L42.0615,11.4386A22,22 0,0 0,24 2V14a10,10 0,0 1,9.264 6.2358l-3.7641,3.7641h4.5,12v-0.0682z"
|
||||
android:strokeWidth="0"
|
||||
android:strokeColor="#000000">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:startY="14"
|
||||
android:startX="24"
|
||||
android:endY="14"
|
||||
android:endX="27.25"
|
||||
android:type="linear">
|
||||
<item android:offset="0" android:color="#FF3949AB"/>
|
||||
<item android:offset="1" android:color="#FF3F51B5"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:pathData="M46,7.5 L42.0615,11.4386C37.9491,5.5255 31.2026,2 24,2L46,24v-0.0682z"
|
||||
android:strokeWidth="0">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:startY="2"
|
||||
android:startX="24"
|
||||
android:endY="24"
|
||||
android:endX="46"
|
||||
android:type="linear">
|
||||
<item android:offset="0" android:color="#18FFFFFF"/>
|
||||
<item android:offset="1" android:color="#00FFFFFF"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
</vector>
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ SPDX-FileCopyrightText: 2021, microG Project Team
|
||||
~ SPDX-License-Identifier: Apache-2.0
|
||||
-->
|
||||
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/recaptcha_loading"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@android:color/white"
|
||||
android:layout_gravity="center"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginLeft="20dip"
|
||||
android:layout_marginTop="20dip"
|
||||
android:layout_marginRight="20dip"
|
||||
android:layout_marginBottom="10sp"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:src="@drawable/ic_recaptcha" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginStart="10sp"
|
||||
android:layout_marginLeft="10sp"
|
||||
android:text="reCAPTCHA"
|
||||
android:textColor="#FF3949AB"
|
||||
android:textStyle="bold" />
|
||||
</LinearLayout>
|
||||
|
||||
<ProgressBar
|
||||
style="?android:progressBarStyleHorizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginLeft="20dip"
|
||||
android:layout_marginTop="10sp"
|
||||
android:layout_marginRight="20dip"
|
||||
android:layout_marginBottom="20dip"
|
||||
android:indeterminate="true"
|
||||
android:indeterminateTint="#FF3949AB" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<WebView
|
||||
android:id="@+id/recaptcha_webview"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:visibility="gone" />
|
||||
</FrameLayout>
|
||||
Loading…
Add table
Add a link
Reference in a new issue