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.safetynet"
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-safetynet'
dependencies {
// Dependencies from play-services-safetynet:18.0.1
api project(':play-services-base')
api project(':play-services-basement')
api project(':play-services-tasks')
}

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,8 @@
package org.microg.gms.safetynet
enum class SafetyNetRequestType {
ATTESTATION,
RECAPTCHA,
RECAPTCHA_ENTERPRISE,
;
}

View file

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

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

View file

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

View file

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

View file

@ -0,0 +1,6 @@
<?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.safetynet;
parcelable AttestationData;

View file

@ -0,0 +1,3 @@
package com.google.android.gms.safetynet;
parcelable HarmfulAppsData;

View file

@ -0,0 +1,3 @@
package com.google.android.gms.safetynet;
parcelable HarmfulAppsInfo;

View file

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

View file

@ -0,0 +1,3 @@
package com.google.android.gms.safetynet;
parcelable RemoveHarmfulAppData;

View file

@ -0,0 +1,3 @@
package com.google.android.gms.safetynet;
parcelable SafeBrowsingData;

View file

@ -0,0 +1,21 @@
package com.google.android.gms.safetynet.internal;
import com.google.android.gms.common.api.Status;
import com.google.android.gms.safetynet.AttestationData;
import com.google.android.gms.safetynet.HarmfulAppsData;
import com.google.android.gms.safetynet.HarmfulAppsInfo;
import com.google.android.gms.safetynet.RecaptchaResultData;
import com.google.android.gms.safetynet.RemoveHarmfulAppData;
import com.google.android.gms.safetynet.SafeBrowsingData;
interface ISafetyNetCallbacks {
oneway void onAttestationResult(in Status status, in AttestationData attestationData) = 0;
oneway void onSharedUuid(String s) = 1;
oneway void onSafeBrowsingData(in Status status, in SafeBrowsingData safeBrowsingData) = 2;
oneway void onVerifyAppsUserResult(in Status status, boolean enabled) = 3;
oneway void onHarmfulAppsData(in Status status, in List<HarmfulAppsData> harmfulAppsData) = 4;
oneway void onRecaptchaResult(in Status status, in RecaptchaResultData recaptchaResultData) = 5;
oneway void onHarmfulAppsInfo(in Status status, in HarmfulAppsInfo harmfulAppsInfo) = 7;
oneway void onInitSafeBrowsingResult(in Status status) = 10;
oneway void onRemoveHarmfulAppData(in Status status, in RemoveHarmfulAppData removeHarmfulAppData) = 14;
}

View file

@ -0,0 +1,25 @@
package com.google.android.gms.safetynet.internal;
import com.google.android.gms.safetynet.internal.ISafetyNetCallbacks;
interface ISafetyNetService {
void attest(ISafetyNetCallbacks callbacks, in byte[] nonce) = 0;
void attestWithApiKey(ISafetyNetCallbacks callbacks, in byte[] nonce, String apiKey) = 6;
void getSharedUuid(ISafetyNetCallbacks callbacks) = 1;
void lookupUri(ISafetyNetCallbacks callbacks, String apiKey, in int[] threatTypes, int version, String uri) = 2;
void enableVerifyApps(ISafetyNetCallbacks callbacks) = 3;
void listHarmfulApps(ISafetyNetCallbacks callbacks) = 4;
void verifyWithRecaptcha(ISafetyNetCallbacks callbacks, String siteKey) = 5;
// void fun9(ISafetyNetCallbacks callbacks) = 8;
// void fun10(ISafetyNetCallbacks callbacks, String s1, int i1, in byte[] b1) = 9;
// void fun11(int i1, in Bundle b1) = 10;
void initSafeBrowsing(ISafetyNetCallbacks callbacks) = 11;
void shutdownSafeBrowsing() = 12;
void isVerifyAppsEnabled(ISafetyNetCallbacks callbacks) = 13;
//
// void fun18(ISafetyNetCallbacks callbacks, int i1, String s1) = 17;
// void fun19(ISafetyNetCallbacks callbacks, int i1) = 18;
// void removeHarmfulApp(ISafetyNetCallbacks callbacks, String packageName, in byte[] digest) = 19;
// void fun21(ISafetyNetCallbacks callbacks, in Bundle b1) = 20;
}

View file

@ -0,0 +1,29 @@
/*
* SPDX-FileCopyrightText: 2017 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.safetynet;
import org.microg.safeparcel.AutoSafeParcelable;
import org.microg.safeparcel.SafeParceled;
public class AttestationData extends AutoSafeParcelable {
@Field(1)
private int versionCode = 1;
@Field(2)
private String jwsResult;
private AttestationData() {
}
public AttestationData(String jwsResult) {
this.jwsResult = jwsResult;
}
public String getJwsResult() {
return jwsResult;
}
public static final Creator<AttestationData> CREATOR = new AutoCreator<AttestationData>(AttestationData.class);
}

View file

@ -0,0 +1,49 @@
/*
* SPDX-FileCopyrightText: 2021 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.safetynet;
import org.microg.gms.common.PublicApi;
import org.microg.safeparcel.AutoSafeParcelable;
/**
* APK information pertaining to one potentially harmful app.
*/
@PublicApi
public class HarmfulAppsData extends AutoSafeParcelable {
/**
* The package name of the potentially harmful app.
*/
@Field(2)
public final String apkPackageName;
/**
* The SHA-256 of the potentially harmful app APK file.
*/
@Field(3)
public final byte[] apkSha256;
/**
* The potentially harmful app category defined in {@link VerifyAppsConstants}.
*/
@Field(4)
public final int apkCategory;
private HarmfulAppsData() {
apkPackageName = null;
apkSha256 = null;
apkCategory = 0;
}
@PublicApi(exclude = true)
public HarmfulAppsData(String apkPackageName, byte[] apkSha256, int apkCategory) {
this.apkPackageName = apkPackageName;
this.apkSha256 = apkSha256;
this.apkCategory = apkCategory;
}
public static final Creator<HarmfulAppsData> CREATOR = new AutoCreator<HarmfulAppsData>(HarmfulAppsData.class);
}

View file

@ -0,0 +1,21 @@
/*
* SPDX-FileCopyrightText: 2017 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.safetynet;
import org.microg.safeparcel.AutoSafeParcelable;
public class HarmfulAppsInfo extends AutoSafeParcelable {
@Field(2)
public long lastScanTime;
@Field(3)
public HarmfulAppsData[] harmfulApps = new HarmfulAppsData[0];
@Field(4)
public int hoursSinceLastScanWithHarmfulApp = -1;
@Field(5)
public boolean harmfulAppInOtherProfile = false;
public static final Creator<HarmfulAppsInfo> CREATOR = new AutoCreator<HarmfulAppsInfo>(HarmfulAppsInfo.class);
}

View file

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

View file

@ -0,0 +1,17 @@
/*
* SPDX-FileCopyrightText: 2017 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.safetynet;
import org.microg.safeparcel.AutoSafeParcelable;
public class RemoveHarmfulAppData extends AutoSafeParcelable {
@Field(2)
public int field2;
@Field(3)
public boolean field3;
public static final Creator<RemoveHarmfulAppData> CREATOR = new AutoCreator<RemoveHarmfulAppData>(RemoveHarmfulAppData.class);
}

View file

@ -0,0 +1,33 @@
/*
* SPDX-FileCopyrightText: 2017 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.safetynet;
import android.os.ParcelFileDescriptor;
import com.google.android.gms.common.data.DataHolder;
import org.microg.safeparcel.AutoSafeParcelable;
import org.microg.safeparcel.SafeParceled;
import java.io.File;
public class SafeBrowsingData extends AutoSafeParcelable {
@Field(1)
public int versionCode = 1;
@Field(2)
public String metadata;
@Field(3)
public DataHolder data;
@Field(4)
public ParcelFileDescriptor fileDescriptor;
public byte[] fileContents;
@Field(5)
public long lastUpdateTime;
@Field(6)
public byte[] state;
public static final Creator<SafeBrowsingData> CREATOR = new AutoCreator<SafeBrowsingData>(SafeBrowsingData.class);
}

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.safetynet;
import android.app.Activity;
import android.content.Context;
import com.google.android.gms.common.api.Api;
import org.microg.gms.common.PublicApi;
import org.microg.gms.safetynet.SafetyNetApiImpl;
import org.microg.gms.safetynet.SafetyNetGmsClient;
/**
* The SafetyNet API provides access to Google services that help you assess the health and safety of an Android device.
* <p>
* To use SafetyNet, call {@link #getClient(Context)} or {@link #getClient(Activity)}.
*/
@PublicApi
public class SafetyNet {
/**
* The API necessary to use SafetyNet.
*
* @deprecated use {@link #getClient(Context)} or {@link #getClient(Activity)}.
*/
@Deprecated
public static final Api<Api.ApiOptions.NoOptions> API = new Api<>((options, context, looper, clientSettings, callbacks, connectionFailedListener) -> new SafetyNetGmsClient(context, callbacks, connectionFailedListener));;
/**
* The entry point for interacting with the SafetyNet APIs which help assess the health and safety of an Android device.
*
* @deprecated use {@link #getClient(Context)} or {@link #getClient(Activity)}.
*/
@Deprecated
public static final SafetyNetApi SafetyNetApi = new SafetyNetApiImpl();
/**
* Returns a {@link SafetyNetClient} 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)} if your app has a foreground Activity and you will be making
* multiple API calls to improve performance.
*/
public static SafetyNetClient getClient(Activity activity) {
return new SafetyNetClient(activity);
}
/**
* Returns a {@link SafetyNetClient} that is used to access all APIs that are called without access to a
* foreground {@link Activity}.
*/
public static SafetyNetClient getClient(Context context) {
return new SafetyNetClient(context);
}
}

View file

@ -0,0 +1,129 @@
/*
* 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.safetynet;
import com.google.android.gms.common.api.GoogleApiClient;
import com.google.android.gms.common.api.PendingResult;
import com.google.android.gms.common.api.Response;
import com.google.android.gms.common.api.Result;
import com.google.android.gms.common.api.Status;
import org.microg.gms.common.PublicApi;
/**
* The main entry point for interacting with SafetyNet.
*/
public interface SafetyNetApi {
/**
* Provides user attestation with reCAPTCHA.
* <p>
* If reCAPTCHA is confident that this is a real user on a real device it will return a token with no challenge.
* Otherwise it will provide a visual/audio challenge to attest the humanness of the user before returning a token.
* <p>
* When you make a request with this API, you must provide your client {@link GoogleApiClient} and site public key
* as parameters, and after the request completes, you can get the {@link RecaptchaTokenResult}
* from the response.
*
* @param client The {@link GoogleApiClient} to service the call. The client must be connected using
* {@link GoogleApiClient#connect()} before invoking this method.
* @param siteKey A site public key registered for this app
* @deprecated use {@link SafetyNetClient#verifyWithRecaptcha(String)}
*/
@Deprecated
PendingResult<RecaptchaTokenResult> verifyWithRecaptcha(GoogleApiClient client, String siteKey);
/**
* Response from {@link SafetyNetClient#attest(byte[], String)} that contains a Compatibility Test Suite
* attestation result.
* <p>
* Use the {@link Result#getStatus()} method to obtain a {@link Status} object. Calling the Status object's
* {@link Status#isSuccess} indicates whether or not communication with the service was successful, but does not
* indicate if the device has passed the compatibility check. If the request was successful,
* {@link AttestationResponse#getJwsResult()} may be used to determine whether the device has passed the
* compatibility check.
*/
class AttestationResponse extends Response<AttestationResult> {
/**
* Gets the JSON Web Signature attestation result.
* <p>
* Result is in JSON Web Signature format.
*
* @return JSON Web signature formatted attestation response or {@code null} if an error occurred.
*/
public String getJwsResult() {
return getResult().getJwsResult();
}
}
@PublicApi(exclude = true)
@Deprecated
interface AttestationResult extends Result {
String getJwsResult();
}
/**
* {@link Response} from {@link SafetyNetClient#verifyWithRecaptcha(String)}.
* <p>
* This Result contains a reCAPTCHA user response token and third party clients should use this token to verify
* the user. The token must be validated on the server side to determine whether the user has passed the challenge.
*/
class RecaptchaTokenResponse extends Response<RecaptchaTokenResult> {
/**
* Gets the reCAPTCHA user response token which must be validated by calling the siteverify method.
*
* @return A user response token.
*/
public String getTokenResult() {
return getResult().getTokenResult();
}
}
/**
* A Result from {@link #verifyWithRecaptcha(GoogleApiClient, String)}.
* <p>
* This Result contains a reCAPTCHA user response token and third party clients should use this token to verify
* the user. Calling the {@link Status#isSuccess()} will indicate whether or not communication with the service was
* successful, but does not indicate if the user has passed the reCAPTCHA challenge. The token must be validated on
* the server side to determine whether the user has passed the challenge.
*
* @deprecated use {@link RecaptchaTokenResponse} returned from {@link SafetyNetClient#verifyWithRecaptcha(String)}.
*/
@Deprecated
interface RecaptchaTokenResult extends Result {
String getTokenResult();
}
/**
* A {@link Response} to get user decisions for the Verify Apps API.
*/
class VerifyAppsUserResponse extends Response<VerifyAppsUserResult> {
/**
* Returns whether the user has enabled Verify Apps when prompted.
* <p>
* This method is only meaningful when used with
* {@link SafetyNetClient#enableVerifyApps()} or {@link SafetyNetClient#isVerifyAppsEnabled()}.
*/
public boolean isVerifyAppsEnabled() {
return getResult().isVerifyAppsEnabled();
}
}
/**
* A {@link Result} to get user decisions for the Verify Apps API.
*
* @deprecated use {@link VerifyAppsUserResponse} instead.
*/
@Deprecated
interface VerifyAppsUserResult extends Result {
/**
* Returns whether the user has enabled Verify Apps when prompted.
*/
boolean isVerifyAppsEnabled();
}
}

View file

@ -0,0 +1,180 @@
/*
* 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.safetynet;
import android.content.Context;
import android.os.RemoteException;
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.safetynet.internal.ISafetyNetCallbacks;
import com.google.android.gms.tasks.Task;
import org.microg.gms.common.PublicApi;
import org.microg.gms.common.api.PendingGoogleApiCall;
import org.microg.gms.safetynet.ISafetyNetCallbacksDefaultStub;
import org.microg.gms.safetynet.SafetyNetGmsClient;
/**
* The main entry point for SafetyNet.
*/
@PublicApi
public class SafetyNetClient extends GoogleApi<Api.ApiOptions.NoOptions> {
@PublicApi(exclude = true)
SafetyNetClient(Context context) {
super(context, SafetyNet.API, Api.ApiOptions.NO_OPTIONS);
}
/**
* Provides attestation results for the device.
* <p>
* An attestation result states whether the device where it is running matches the profile of a device that has
* passed Android compatibility testing.
* <p>
* When you request a compatibility check, you must provide a nonce, which is a random token generated in a
* cryptographically secure manner. You can obtain a nonce by generating one within your app each time you make a
* compatibility check request. As a more secure option, you can obtain a nonce from your own server, using a
* secure connection.
* <p>
* A nonce used with an attestation request should be at least 16 bytes in length. After you make a request, the
* response {@link SafetyNetApi.AttestationResponse} includes your nonce, so you can verify it against the one you
* sent. You should only use a nonce value once, for a single request. Use a different nonce for any subsequent
* attestation requests.
*
* @param nonce A cryptographic nonce used for anti-replay and tracking of requests.
* @param apiKey An Android API key obtained through the developer console.
*/
public Task<SafetyNetApi.AttestationResponse> attest(byte[] nonce, String apiKey) {
return scheduleTask((PendingGoogleApiCall<SafetyNetApi.AttestationResponse, SafetyNetGmsClient>) (client, completionSource) -> {
try {
client.attest(new ISafetyNetCallbacksDefaultStub() {
@Override
public void onAttestationResult(Status status, AttestationData attestationData) throws RemoteException {
SafetyNetApi.AttestationResponse response = new SafetyNetApi.AttestationResponse();
response.setResult(new SafetyNetApi.AttestationResult() {
@Override
public String getJwsResult() {
return attestationData.getJwsResult();
}
@Override
public Status getStatus() {
return status;
}
});
completionSource.setResult(response);
}
}, nonce, apiKey);
} catch (Exception e) {
completionSource.setException(e);
}
});
}
/**
* Provides user attestation with reCAPTCHA.
* <p>
* If reCAPTCHA is confident that this is a real user on a real device it will return a token with no challenge.
* Otherwise it will provide a visual/audio challenge to attest the humanness of the user before returning a token.
*
* @param siteKey A site public key registered for this app
*/
public Task<SafetyNetApi.RecaptchaTokenResponse> verifyWithRecaptcha(String siteKey) {
return scheduleTask((PendingGoogleApiCall<SafetyNetApi.RecaptchaTokenResponse, SafetyNetGmsClient>) (client, completionSource) -> {
try {
client.verifyWithRecaptcha(new ISafetyNetCallbacksDefaultStub() {
@Override
public void onRecaptchaResult(Status status, RecaptchaResultData recaptchaResultData) throws RemoteException {
SafetyNetApi.RecaptchaTokenResponse response = new SafetyNetApi.RecaptchaTokenResponse();
response.setResult(new SafetyNetApi.RecaptchaTokenResult() {
@Override
public String getTokenResult() {
if (recaptchaResultData != null) {
return recaptchaResultData.token;
} else {
return null;
}
}
@Override
public Status getStatus() {
return status;
}
});
completionSource.setResult(response);
}
}, siteKey);
} catch (Exception e) {
completionSource.setException(e);
}
});
}
/**
* Prompts the user to enable Verify Apps if it is currently turned off.
*/
public Task<SafetyNetApi.VerifyAppsUserResponse> enableVerifyApps() {
return scheduleTask((PendingGoogleApiCall<SafetyNetApi.VerifyAppsUserResponse, SafetyNetGmsClient>) (client, completionSource) -> {
try {
client.enableVerifyApps(new ISafetyNetCallbacksDefaultStub() {
@Override
public void onVerifyAppsUserResult(Status status, boolean enabled) throws RemoteException {
SafetyNetApi.VerifyAppsUserResponse response = new SafetyNetApi.VerifyAppsUserResponse();
response.setResult(new SafetyNetApi.VerifyAppsUserResult() {
@Override
public boolean isVerifyAppsEnabled() {
return enabled;
}
@Override
public Status getStatus() {
return status;
}
});
completionSource.setResult(response);
}
});
} catch (Exception e) {
completionSource.setException(e);
}
});
}
/**
* Determines whether Verify Apps is enabled.
*/
public Task<SafetyNetApi.VerifyAppsUserResponse> isVerifyAppsEnabled() {
return scheduleTask((PendingGoogleApiCall<SafetyNetApi.VerifyAppsUserResponse, SafetyNetGmsClient>) (client, completionSource) -> {
try {
client.isVerifyAppsEnabled(new ISafetyNetCallbacksDefaultStub() {
@Override
public void onVerifyAppsUserResult(Status status, boolean enabled) throws RemoteException {
SafetyNetApi.VerifyAppsUserResponse response = new SafetyNetApi.VerifyAppsUserResponse();
response.setResult(new SafetyNetApi.VerifyAppsUserResult() {
@Override
public boolean isVerifyAppsEnabled() {
return enabled;
}
@Override
public Status getStatus() {
return status;
}
});
completionSource.setResult(response);
}
});
} catch (Exception e) {
completionSource.setException(e);
}
});
}
}

View file

@ -0,0 +1,71 @@
/*
* SPDX-FileCopyrightText: 2021 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.safetynet;
import com.google.android.gms.common.api.CommonStatusCodes;
/**
* Status codes for the SafetyNet API.
*/
public class SafetyNetStatusCodes extends CommonStatusCodes {
/**
* None of the input threat types to {@code lookupUri(String, String, int...)} are supported.
*/
public static final int SAFE_BROWSING_UNSUPPORTED_THREAT_TYPES = 12000;
/**
* The API key required for calling {@code lookupUri(String, String, int...)} is missing in the manifest.
* <p>
* A meta-data name-value pair in the app manifest with the name "com.google.android.safetynet.API_KEY" and a value
* consisting of the API key from the Google Developers Console is not present.
*/
public static final int SAFE_BROWSING_MISSING_API_KEY = 12001;
/**
* An internal error occurred causing the call to {@code lookupUri(String, String, int...)} to be unavailable.
*/
public static final int SAFE_BROWSING_API_NOT_AVAILABLE = 12002;
/**
* Verify Apps is not supported on this device.
*/
public static final int VERIFY_APPS_NOT_AVAILABLE = 12003;
/**
* An internal error occurred while using the Verify Apps API.
*/
public static final int VERIFY_APPS_INTERNAL_ERROR = 12004;
/**
* Cannot list potentially harmful apps because Verify Apps is not enabled.
* <p>
* The developer may call {@code enableVerifyApps()} to request the user turn on Verify Apps.
*/
public static final int VERIFY_APPS_NOT_ENABLED = 12005;
/**
* User device SDK version is not supported.
*/
public static final int UNSUPPORTED_SDK_VERSION = 12006;
/**
* Cannot start the reCAPTCHA service because site key parameter is not valid.
*/
public static final int RECAPTCHA_INVALID_SITEKEY = 12007;
/**
* Cannot start the reCAPTCHA service because type of site key is not valid.
* <p>
* Please register new site key with the key type set to "reCAPTCHA Android"
*/
public static final int RECAPTCHA_INVALID_KEYTYPE = 12008;
/**
* {@code lookupUri(String, String, int...)} called without first calling {@code initSafeBrowsing()}.
*/
public static final int SAFE_BROWSING_API_NOT_INITIALIZED = 12009;
/**
* Cannot start the reCAPTCHA service because calling package name is not matched with site key.
* <p>
* Please add the new package name to your site key via reCAPTCHA Admin Console or choose to disable the package
* name validation for your key.
*/
public static final int RECAPTCHA_INVALID_PACKAGE_NAME = 12013;
}

View file

@ -0,0 +1,52 @@
/*
* SPDX-FileCopyrightText: 2021 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.safetynet;
import org.microg.gms.common.PublicApi;
/**
* Constants pertaining to the Verify Apps SafetyNet API.
*/
@PublicApi
public class VerifyAppsConstants {
/**
* An action that is broadcasted when harmful apps are discovered.
*/
public static final String ACTION_HARMFUL_APPS_FOUND = "com.google.android.gms.safetynet.action.HARMFUL_APPS_FOUND";
/**
* An action that is broadcasted when a harmful app is blocked from installation.
*/
public static final String ACTION_HARMFUL_APP_BLOCKED = "com.google.android.gms.safetynet.action.HARMFUL_APP_BLOCKED";
/**
* An action that is broadcasted when a harmful app is installed.
*/
public static final String ACTION_HARMFUL_APP_INSTALLED = "com.google.android.gms.safetynet.action.HARMFUL_APP_INSTALLED";
public static final int HARMFUL_CATEGORY_RANSOMWARE = 1;
public static final int HARMFUL_CATEGORY_PHISHING = 2;
public static final int HARMFUL_CATEGORY_TROJAN = 3;
public static final int HARMFUL_CATEGORY_UNCOMMON = 4;
public static final int HARMFUL_CATEGORY_FRAUDWARE = 5;
public static final int HARMFUL_CATEGORY_TOLL_FRAUD = 6;
public static final int HARMFUL_CATEGORY_WAP_FRAUD = 7;
public static final int HARMFUL_CATEGORY_CALL_FRAUD = 8;
public static final int HARMFUL_CATEGORY_BACKDOOR = 9;
public static final int HARMFUL_CATEGORY_SPYWARE = 10;
public static final int HARMFUL_CATEGORY_GENERIC_MALWARE = 11;
public static final int HARMFUL_CATEGORY_HARMFUL_SITE = 12;
public static final int HARMFUL_CATEGORY_WINDOWS_MALWARE = 13;
public static final int HARMFUL_CATEGORY_HOSTILE_DOWNLOADER = 14;
public static final int HARMFUL_CATEGORY_NON_ANDROID_THREAT = 15;
public static final int HARMFUL_CATEGORY_ROOTING = 16;
public static final int HARMFUL_CATEGORY_PRIVILEGE_ESCALATION = 17;
public static final int HARMFUL_CATEGORY_TRACKING = 18;
public static final int HARMFUL_CATEGORY_SPAM = 19;
public static final int HARMFUL_CATEGORY_DENIAL_OF_SERVICE = 20;
public static final int HARMFUL_CATEGORY_DATA_COLLECTION = 21;
}

View file

@ -0,0 +1,58 @@
/*
* SPDX-FileCopyrightText: 2022 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.safetynet;
import android.os.RemoteException;
import com.google.android.gms.common.api.Status;
import com.google.android.gms.safetynet.AttestationData;
import com.google.android.gms.safetynet.HarmfulAppsData;
import com.google.android.gms.safetynet.HarmfulAppsInfo;
import com.google.android.gms.safetynet.RecaptchaResultData;
import com.google.android.gms.safetynet.RemoveHarmfulAppData;
import com.google.android.gms.safetynet.SafeBrowsingData;
import com.google.android.gms.safetynet.internal.ISafetyNetCallbacks;
import java.util.List;
public class ISafetyNetCallbacksDefaultStub extends ISafetyNetCallbacks.Stub {
@Override
public void onAttestationResult(Status status, AttestationData attestationData) throws RemoteException {
}
@Override
public void onSharedUuid(String s) throws RemoteException {
}
@Override
public void onSafeBrowsingData(Status status, SafeBrowsingData safeBrowsingData) throws RemoteException {
}
@Override
public void onVerifyAppsUserResult(Status status, boolean enabled) throws RemoteException {
}
@Override
public void onHarmfulAppsData(Status status, List<HarmfulAppsData> harmfulAppsData) throws RemoteException {
}
@Override
public void onRecaptchaResult(Status status, RecaptchaResultData recaptchaResultData) throws RemoteException {
}
@Override
public void onHarmfulAppsInfo(Status status, HarmfulAppsInfo harmfulAppsInfo) throws RemoteException {
}
@Override
public void onInitSafeBrowsingResult(Status status) throws RemoteException {
}
@Override
public void onRemoveHarmfulAppData(Status status, RemoveHarmfulAppData removeHarmfulAppData) throws RemoteException {
}
}

View file

@ -0,0 +1,40 @@
/*
* SPDX-FileCopyrightText: 2022 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.safetynet;
import android.os.RemoteException;
import com.google.android.gms.common.api.GoogleApiClient;
import com.google.android.gms.common.api.PendingResult;
import com.google.android.gms.common.api.Status;
import com.google.android.gms.safetynet.RecaptchaResultData;
import com.google.android.gms.safetynet.SafetyNet;
import com.google.android.gms.safetynet.SafetyNetApi;
import com.google.android.gms.safetynet.internal.ISafetyNetCallbacks;
import org.microg.gms.common.GmsConnector;
public class SafetyNetApiImpl implements SafetyNetApi {
@Override
public PendingResult<RecaptchaTokenResult> verifyWithRecaptcha(GoogleApiClient apiClient, String siteKey) {
return GmsConnector.call(apiClient, SafetyNet.API, (GmsConnector.Callback<SafetyNetGmsClient, RecaptchaTokenResult>) (client, resultProvider) -> client.verifyWithRecaptcha(new ISafetyNetCallbacksDefaultStub() {
@Override
public void onRecaptchaResult(Status status, RecaptchaResultData recaptchaResultData) throws RemoteException {
resultProvider.onResultAvailable(new RecaptchaTokenResult() {
@Override
public String getTokenResult() {
return recaptchaResultData.token;
}
@Override
public Status getStatus() {
return status;
}
});
}
}, siteKey));
}
}

View file

@ -0,0 +1,46 @@
/*
* SPDX-FileCopyrightText: 2022 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.safetynet;
import android.content.Context;
import android.os.IBinder;
import android.os.RemoteException;
import com.google.android.gms.safetynet.internal.ISafetyNetCallbacks;
import com.google.android.gms.safetynet.internal.ISafetyNetService;
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;
public class SafetyNetGmsClient extends GmsClient<ISafetyNetService> {
public SafetyNetGmsClient(Context context, ConnectionCallbacks callbacks, OnConnectionFailedListener connectionFailedListener) {
super(context, callbacks, connectionFailedListener, GmsService.SAFETY_NET_CLIENT.ACTION);
serviceId = GmsService.SAFETY_NET_CLIENT.SERVICE_ID;
}
public void attest(ISafetyNetCallbacks callbacks, byte[] nonce, String apiKey) throws RemoteException {
getServiceInterface().attestWithApiKey(callbacks, nonce, apiKey);
}
public void verifyWithRecaptcha(ISafetyNetCallbacks callbacks, String siteKey) throws RemoteException {
getServiceInterface().verifyWithRecaptcha(callbacks, siteKey);
}
public void enableVerifyApps(ISafetyNetCallbacks callbacks) throws RemoteException {
getServiceInterface().enableVerifyApps(callbacks);
}
public void isVerifyAppsEnabled(ISafetyNetCallbacks callbacks) throws RemoteException {
getServiceInterface().isVerifyAppsEnabled(callbacks);
}
@Override
protected ISafetyNetService interfaceFromBinder(IBinder binder) {
return ISafetyNetService.Stub.asInterface(binder);
}
}