diff --git a/build.gradle b/build.gradle index 07307bbf5c..d4c1a8cefa 100644 --- a/build.gradle +++ b/build.gradle @@ -28,6 +28,7 @@ buildscript { ext.slf4jVersion = '1.7.36' ext.volleyVersion = '1.2.1' ext.wireVersion = '4.8.0' + ext.tinkVersion = '1.13.0' ext.androidBuildGradleVersion = '8.2.2' diff --git a/play-services-droidguard/build.gradle b/play-services-droidguard/build.gradle index a253811a4c..78d9af9a3a 100644 --- a/play-services-droidguard/build.gradle +++ b/play-services-droidguard/build.gradle @@ -5,6 +5,7 @@ apply plugin: 'com.android.library' apply plugin: 'maven-publish' +apply plugin: 'kotlin-android' apply plugin: 'signing' android { @@ -31,6 +32,10 @@ android { sourceCompatibility = 1.8 targetCompatibility = 1.8 } + + kotlinOptions { + jvmTarget = 1.8 + } } apply from: '../gradle/publish-android.gradle' diff --git a/play-services-droidguard/core/src/main/java/com/google/android/gms/droidguard/DroidGuardChimeraService.java b/play-services-droidguard/core/src/main/java/com/google/android/gms/droidguard/DroidGuardChimeraService.java index a25cbd87aa..ed15575a30 100644 --- a/play-services-droidguard/core/src/main/java/com/google/android/gms/droidguard/DroidGuardChimeraService.java +++ b/play-services-droidguard/core/src/main/java/com/google/android/gms/droidguard/DroidGuardChimeraService.java @@ -17,7 +17,7 @@ import org.microg.gms.droidguard.core.DroidGuardServiceBroker; import org.microg.gms.droidguard.GuardCallback; -import org.microg.gms.droidguard.core.HandleProxyFactory; +import org.microg.gms.droidguard.core.NetworkHandleProxyFactory; import org.microg.gms.droidguard.PingData; import org.microg.gms.droidguard.Request; @@ -30,7 +30,7 @@ public class DroidGuardChimeraService extends TracingIntentService { public static final Object a = new Object(); // factory - public HandleProxyFactory b; + public NetworkHandleProxyFactory b; // widevine public Object c; // executor @@ -51,7 +51,7 @@ public DroidGuardChimeraService() { setIntentRedelivery(true); } - public DroidGuardChimeraService(HandleProxyFactory factory, Object ping, Object database) { + public DroidGuardChimeraService(NetworkHandleProxyFactory factory, Object ping, Object database) { super("DG"); setIntentRedelivery(true); this.b = factory; @@ -120,7 +120,7 @@ public final IBinder onBind(Intent intent) { @Override public void onCreate() { this.e = new Object(); - this.b = new HandleProxyFactory(this); + this.b = new NetworkHandleProxyFactory(this); this.g = new Object(); this.h = new Handler(); this.c = new Object(); diff --git a/play-services-droidguard/core/src/main/kotlin/org/microg/gms/droidguard/core/DroidGuardHandleImpl.kt b/play-services-droidguard/core/src/main/kotlin/org/microg/gms/droidguard/core/DroidGuardHandleImpl.kt index 5ce8116564..9dfa0ab9a4 100644 --- a/play-services-droidguard/core/src/main/kotlin/org/microg/gms/droidguard/core/DroidGuardHandleImpl.kt +++ b/play-services-droidguard/core/src/main/kotlin/org/microg/gms/droidguard/core/DroidGuardHandleImpl.kt @@ -14,10 +14,12 @@ import android.util.Log import com.google.android.gms.droidguard.internal.DroidGuardInitReply import com.google.android.gms.droidguard.internal.DroidGuardResultsRequest import com.google.android.gms.droidguard.internal.IDroidGuardHandle +import org.microg.gms.droidguard.BytesException import org.microg.gms.droidguard.GuardCallback +import org.microg.gms.droidguard.HandleProxy import java.io.FileNotFoundException -class DroidGuardHandleImpl(private val context: Context, private val packageName: String, private val factory: HandleProxyFactory, private val callback: GuardCallback) : IDroidGuardHandle.Stub() { +class DroidGuardHandleImpl(private val context: Context, private val packageName: String, private val factory: NetworkHandleProxyFactory, private val callback: GuardCallback) : IDroidGuardHandle.Stub() { private val condition = ConditionVariable() private var flow: String? = null diff --git a/play-services-droidguard/core/src/main/kotlin/org/microg/gms/droidguard/core/DroidGuardPreferences.kt b/play-services-droidguard/core/src/main/kotlin/org/microg/gms/droidguard/core/DroidGuardPreferences.kt index cb0e5eff07..bb96747df2 100644 --- a/play-services-droidguard/core/src/main/kotlin/org/microg/gms/droidguard/core/DroidGuardPreferences.kt +++ b/play-services-droidguard/core/src/main/kotlin/org/microg/gms/droidguard/core/DroidGuardPreferences.kt @@ -38,7 +38,7 @@ object DroidGuardPreferences { fun isAvailable(context: Context): Boolean = isEnabled(context) && (!isForcedLocalDisabled(context) || getMode(context) != Mode.Embedded) @JvmStatic - fun isLocalAvailable(context: Context): Boolean = isEnabled(context) && !isForcedLocalDisabled(context) + fun isLocalAvailable(context: Context): Boolean = isEnabled(context) && !isForcedLocalDisabled(context) && getMode(context) == Mode.Embedded @JvmStatic fun setEnabled(context: Context, enabled: Boolean) = setSettings(context) { put(ENABLED, enabled) } diff --git a/play-services-droidguard/core/src/main/kotlin/org/microg/gms/droidguard/core/DroidGuardResultCreator.kt b/play-services-droidguard/core/src/main/kotlin/org/microg/gms/droidguard/core/DroidGuardResultCreator.kt index a2e7d4d928..f3ae3dc7b6 100644 --- a/play-services-droidguard/core/src/main/kotlin/org/microg/gms/droidguard/core/DroidGuardResultCreator.kt +++ b/play-services-droidguard/core/src/main/kotlin/org/microg/gms/droidguard/core/DroidGuardResultCreator.kt @@ -41,7 +41,7 @@ private class NetworkDroidGuardResultCreator(private val context: Context) : Dro get() = DroidGuardPreferences.getNetworkServerUrl(context) ?: throw IllegalStateException("Network URL required") override suspend fun getResults(flow: String, data: Map): String = suspendCoroutine { continuation -> - queue.add(PostParamsStringRequest("$url?flow=$flow", data, { + queue.add(PostParamsStringRequest("$url?flow=$flow&source=${context.packageName}", data, { continuation.resume(it) }, { continuation.resumeWithException(it.cause ?: it) diff --git a/play-services-droidguard/core/src/main/kotlin/org/microg/gms/droidguard/core/HandleProxyFactory.kt b/play-services-droidguard/core/src/main/kotlin/org/microg/gms/droidguard/core/NetworkHandleProxyFactory.kt similarity index 71% rename from play-services-droidguard/core/src/main/kotlin/org/microg/gms/droidguard/core/HandleProxyFactory.kt rename to play-services-droidguard/core/src/main/kotlin/org/microg/gms/droidguard/core/NetworkHandleProxyFactory.kt index b254866478..4e685de383 100644 --- a/play-services-droidguard/core/src/main/kotlin/org/microg/gms/droidguard/core/HandleProxyFactory.kt +++ b/play-services-droidguard/core/src/main/kotlin/org/microg/gms/droidguard/core/NetworkHandleProxyFactory.kt @@ -6,13 +6,11 @@ package org.microg.gms.droidguard.core import android.content.Context -import androidx.annotation.GuardedBy import com.android.volley.NetworkResponse import com.android.volley.VolleyError import com.android.volley.toolbox.RequestFuture import com.android.volley.toolbox.Volley import com.google.android.gms.droidguard.internal.DroidGuardResultsRequest -import dalvik.system.DexClassLoader import okio.ByteString.Companion.decodeHex import okio.ByteString.Companion.of import org.microg.gms.droidguard.* @@ -20,16 +18,11 @@ import org.microg.gms.profile.Build import org.microg.gms.profile.ProfileManager import org.microg.gms.utils.singleInstanceOf import java.io.File -import java.io.IOException -import java.security.MessageDigest -import java.security.cert.Certificate import java.util.* import com.android.volley.Request as VolleyRequest import com.android.volley.Response as VolleyResponse -class HandleProxyFactory(private val context: Context) { - @GuardedBy("CLASS_LOCK") - private val classMap = hashMapOf>() +class NetworkHandleProxyFactory(private val context: Context) : HandleProxyFactory(context) { private val dgDb: DgDatabaseHelper = DgDatabaseHelper(context) private val version = VersionUtil(context) private val queue = singleInstanceOf { Volley.newRequestQueue(context.applicationContext) } @@ -136,7 +129,7 @@ class HandleProxyFactory(private val context: Context) { override fun getHeaders(): Map { return mapOf( - "User-Agent" to "DroidGuard/${version.versionCode}" + "User-Agent" to "DroidGuard/${version.versionCode}" ) } }) @@ -178,68 +171,7 @@ class HandleProxyFactory(private val context: Context) { return HandleProxy(clazz, context, flow, byteCode, callback, vmKey, extra, request?.bundle) } - fun getTheApkFile(vmKey: String) = File(getCacheDir(vmKey), "the.apk") - private fun getCacheDir() = context.getDir(CACHE_FOLDER_NAME, Context.MODE_PRIVATE) - private fun getCacheDir(vmKey: String) = File(getCacheDir(), vmKey) - private fun getOptDir(vmKey: String) = File(getCacheDir(vmKey), "opt") - private fun isValidCache(vmKey: String) = getTheApkFile(vmKey).isFile && getOptDir(vmKey).isDirectory - - private fun updateCacheTimestamp(vmKey: String) { - try { - val timestampFile = File(getCacheDir(vmKey), "t") - if (!timestampFile.exists() && !timestampFile.createNewFile()) { - throw Exception("Failed to touch last-used file for $vmKey.") - } - if (!timestampFile.setLastModified(System.currentTimeMillis())) { - throw Exception("Failed to update last-used timestamp for $vmKey.") - } - } catch (e: IOException) { - throw Exception("Failed to touch last-used file for $vmKey.") - } - } - - private fun verifyApkSignature(apk: File): Boolean { - return true - val certificates: Array = TODO() - if (certificates.size != 1) return false - return Arrays.equals(MessageDigest.getInstance("SHA-256").digest(certificates[0].encoded), PROD_CERT_HASH) - } - - private fun loadClass(vmKey: String, bytes: ByteArray): Class<*> { - synchronized(CLASS_LOCK) { - val cachedClass = classMap[vmKey] - if (cachedClass != null) { - updateCacheTimestamp(vmKey) - return cachedClass - } - val weakClass = weakClassMap[vmKey] - if (weakClass != null) { - classMap[vmKey] = weakClass - updateCacheTimestamp(vmKey) - return weakClass - } - if (!isValidCache(vmKey)) { - throw BytesException(bytes, "VM key $vmKey not found in cache") - } - if (!verifyApkSignature(getTheApkFile(vmKey))) { - getCacheDir(vmKey).deleteRecursively() - throw ClassNotFoundException("APK signature verification failed") - } - val loader = DexClassLoader(getTheApkFile(vmKey).absolutePath, getOptDir(vmKey).absolutePath, null, context.classLoader) - val clazz = loader.loadClass(CLASS_NAME) - classMap[vmKey] = clazz - weakClassMap[vmKey] = clazz - return clazz - } - } - companion object { - const val CLASS_NAME = "com.google.ccc.abuse.droidguard.DroidGuard" const val SERVER_URL = "https://www.googleapis.com/androidantiabuse/v1/x/create?alt=PROTO&key=AIzaSyBofcZsgLSS7BOnBjZPEkk4rYwzOIz-lTI" - const val CACHE_FOLDER_NAME = "cache_dg" - val CLASS_LOCK = Object() - @GuardedBy("CLASS_LOCK") - val weakClassMap = WeakHashMap>() - val PROD_CERT_HASH = byteArrayOf(61, 122, 18, 35, 1, -102, -93, -99, -98, -96, -29, 67, 106, -73, -64, -119, 107, -5, 79, -74, 121, -12, -34, 95, -25, -62, 63, 50, 108, -113, -103, 74) } } diff --git a/play-services-droidguard/src/main/java/org/microg/gms/droidguard/DroidGuardApiClient.java b/play-services-droidguard/src/main/java/org/microg/gms/droidguard/DroidGuardApiClient.java index 5399c347fc..b2baf6b833 100644 --- a/play-services-droidguard/src/main/java/org/microg/gms/droidguard/DroidGuardApiClient.java +++ b/play-services-droidguard/src/main/java/org/microg/gms/droidguard/DroidGuardApiClient.java @@ -11,6 +11,7 @@ import android.os.HandlerThread; import android.os.IBinder; import android.os.Looper; +import android.os.ParcelFileDescriptor; import android.util.Log; import com.google.android.gms.droidguard.DroidGuardHandle; @@ -29,6 +30,7 @@ public class DroidGuardApiClient extends GmsClient { private final Context context; private int openHandles = 0; private Handler handler; + private HandleProxyFactory factory; public DroidGuardApiClient(Context context, ConnectionCallbacks callbacks, OnConnectionFailedListener connectionFailedListener) { super(context, callbacks, connectionFailedListener, GmsService.DROIDGUARD.ACTION); @@ -38,6 +40,8 @@ public DroidGuardApiClient(Context context, ConnectionCallbacks callbacks, OnCon HandlerThread thread = new HandlerThread("DG"); thread.start(); handler = new Handler(thread.getLooper()); + + factory = new HandleProxyFactory(context); } public void setPackageName(String packageName) { @@ -60,6 +64,7 @@ public DroidGuardHandle openHandle(String flow, DroidGuardResultsRequest request for (String key : bundle.keySet()) { Log.d(TAG, "reply.object[" + key + "] = " + bundle.get(key)); } + handleDroidGuardData(reply.pfd, (Bundle) reply.object); } } } @@ -70,6 +75,16 @@ public DroidGuardHandle openHandle(String flow, DroidGuardResultsRequest request } } + private void handleDroidGuardData(ParcelFileDescriptor pfd, Bundle bundle) { + String vmKey = bundle.getString("h"); + if (vmKey == null) { + throw new RuntimeException("Missing vmKey"); + } + HandleProxy proxy = factory.createHandle(vmKey, pfd, bundle); + proxy.init(); + proxy.close(); + } + public void markHandleClosed() { if (openHandles == 0) { Log.w(TAG, "Can't mark handle closed if none is open"); diff --git a/play-services-droidguard/core/src/main/kotlin/org/microg/gms/droidguard/core/BytesException.kt b/play-services-droidguard/src/main/kotlin/org/microg/gms/droidguard/BytesException.kt similarity index 92% rename from play-services-droidguard/core/src/main/kotlin/org/microg/gms/droidguard/core/BytesException.kt rename to play-services-droidguard/src/main/kotlin/org/microg/gms/droidguard/BytesException.kt index 71ef914228..32721ee211 100644 --- a/play-services-droidguard/core/src/main/kotlin/org/microg/gms/droidguard/core/BytesException.kt +++ b/play-services-droidguard/src/main/kotlin/org/microg/gms/droidguard/BytesException.kt @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package org.microg.gms.droidguard.core +package org.microg.gms.droidguard class BytesException : Exception { val bytes: ByteArray diff --git a/play-services-droidguard/core/src/main/kotlin/org/microg/gms/droidguard/core/HandleProxy.kt b/play-services-droidguard/src/main/kotlin/org/microg/gms/droidguard/HandleProxy.kt similarity index 62% rename from play-services-droidguard/core/src/main/kotlin/org/microg/gms/droidguard/core/HandleProxy.kt rename to play-services-droidguard/src/main/kotlin/org/microg/gms/droidguard/HandleProxy.kt index 871468027d..34d1545c68 100644 --- a/play-services-droidguard/core/src/main/kotlin/org/microg/gms/droidguard/core/HandleProxy.kt +++ b/play-services-droidguard/src/main/kotlin/org/microg/gms/droidguard/HandleProxy.kt @@ -1,9 +1,9 @@ /* - * SPDX-FileCopyrightText: 2021 microG Project Team + * SPDX-FileCopyrightText: 2022 microG Project Team * SPDX-License-Identifier: Apache-2.0 */ -package org.microg.gms.droidguard.core +package org.microg.gms.droidguard import android.content.Context import android.os.Bundle @@ -11,20 +11,20 @@ import android.os.Parcelable class HandleProxy(val handle: Any, val vmKey: String, val extra: ByteArray = ByteArray(0)) { constructor(clazz: Class<*>, context: Context, vmKey: String, data: Parcelable) : this( - kotlin.runCatching { - clazz.getDeclaredConstructor(Context::class.java, Parcelable::class.java).newInstance(context, data) - }.getOrElse { - throw BytesException(ByteArray(0), it) - }, - vmKey + kotlin.runCatching { + clazz.getDeclaredConstructor(Context::class.java, Parcelable::class.java).newInstance(context, data) + }.getOrElse { + throw BytesException(ByteArray(0), it) + }, + vmKey ) constructor(clazz: Class<*>, context: Context, flow: String?, byteCode: ByteArray, callback: Any, vmKey: String, extra: ByteArray, bundle: Bundle?) : this( - kotlin.runCatching { - clazz.getDeclaredConstructor(Context::class.java, String::class.java, ByteArray::class.java, Object::class.java, Bundle::class.java).newInstance(context, flow, byteCode, callback, bundle) - }.getOrElse { - throw BytesException(extra, it) - }, vmKey, extra) + kotlin.runCatching { + clazz.getDeclaredConstructor(Context::class.java, String::class.java, ByteArray::class.java, Object::class.java, Bundle::class.java).newInstance(context, flow, byteCode, callback, bundle) + }.getOrElse { + throw BytesException(extra, it) + }, vmKey, extra) fun run(data: Map): ByteArray { try { diff --git a/play-services-droidguard/src/main/kotlin/org/microg/gms/droidguard/HandleProxyFactory.kt b/play-services-droidguard/src/main/kotlin/org/microg/gms/droidguard/HandleProxyFactory.kt new file mode 100644 index 0000000000..e6fc1707b1 --- /dev/null +++ b/play-services-droidguard/src/main/kotlin/org/microg/gms/droidguard/HandleProxyFactory.kt @@ -0,0 +1,117 @@ +/* + * SPDX-FileCopyrightText: 2021 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.droidguard + +import android.content.Context +import android.os.Bundle +import android.os.ParcelFileDescriptor +import android.os.Parcelable +import androidx.annotation.GuardedBy +import dalvik.system.DexClassLoader +import java.io.File +import java.io.IOException +import java.security.MessageDigest +import java.security.cert.Certificate +import java.util.* + +open class HandleProxyFactory(private val context: Context) { + @GuardedBy("CLASS_LOCK") + protected val classMap = hashMapOf>() + + fun createHandle(vmKey: String, pfd: ParcelFileDescriptor, extras: Bundle): HandleProxy { + fetchFromFileDescriptor(pfd, vmKey) + return createHandleProxy(vmKey, extras) + } + + private fun fetchFromFileDescriptor(pfd: ParcelFileDescriptor, vmKey: String) { + if (!isValidCache(vmKey)) { + val auIs = ParcelFileDescriptor.AutoCloseInputStream(pfd) + val temp = File(getCacheDir(), "${UUID.randomUUID()}.apk") + temp.parentFile!!.mkdirs() + temp.writeBytes(auIs.readBytes()) + auIs.close() + getOptDir(vmKey).mkdirs() + temp.renameTo(getTheApkFile(vmKey)) + updateCacheTimestamp(vmKey) + if (!isValidCache(vmKey)) { + getCacheDir(vmKey).deleteRecursively() + throw IllegalStateException("Error ") + } + } + } + + private fun createHandleProxy( + vmKey: String, + extras: Parcelable + ): HandleProxy { + val clazz = loadClass(vmKey) + return HandleProxy(clazz, context, vmKey, extras) + } + + fun getTheApkFile(vmKey: String) = File(getCacheDir(vmKey), "the.apk") + protected fun getCacheDir() = context.getDir(CACHE_FOLDER_NAME, Context.MODE_PRIVATE) + protected fun getCacheDir(vmKey: String) = File(getCacheDir(), vmKey) + protected fun getOptDir(vmKey: String) = File(getCacheDir(vmKey), "opt") + protected fun isValidCache(vmKey: String) = getTheApkFile(vmKey).isFile && getOptDir(vmKey).isDirectory + + protected fun updateCacheTimestamp(vmKey: String) { + try { + val timestampFile = File(getCacheDir(vmKey), "t") + if (!timestampFile.exists() && !timestampFile.createNewFile()) { + throw Exception("Failed to touch last-used file for $vmKey.") + } + if (!timestampFile.setLastModified(System.currentTimeMillis())) { + throw Exception("Failed to update last-used timestamp for $vmKey.") + } + } catch (e: IOException) { + throw Exception("Failed to touch last-used file for $vmKey.") + } + } + + private fun verifyApkSignature(apk: File): Boolean { + return true + val certificates: Array = TODO() + if (certificates.size != 1) return false + return Arrays.equals(MessageDigest.getInstance("SHA-256").digest(certificates[0].encoded), PROD_CERT_HASH) + } + + protected fun loadClass(vmKey: String, bytes: ByteArray = ByteArray(0)): Class<*> { + synchronized(CLASS_LOCK) { + val cachedClass = classMap[vmKey] + if (cachedClass != null) { + updateCacheTimestamp(vmKey) + return cachedClass + } + val weakClass = weakClassMap[vmKey] + if (weakClass != null) { + classMap[vmKey] = weakClass + updateCacheTimestamp(vmKey) + return weakClass + } + if (!isValidCache(vmKey)) { + throw BytesException(bytes, "VM key $vmKey not found in cache") + } + if (!verifyApkSignature(getTheApkFile(vmKey))) { + getCacheDir(vmKey).deleteRecursively() + throw ClassNotFoundException("APK signature verification failed") + } + val loader = DexClassLoader(getTheApkFile(vmKey).absolutePath, getOptDir(vmKey).absolutePath, null, context.classLoader) + val clazz = loader.loadClass(CLASS_NAME) + classMap[vmKey] = clazz + weakClassMap[vmKey] = clazz + return clazz + } + } + + companion object { + const val CLASS_NAME = "com.google.ccc.abuse.droidguard.DroidGuard" + const val CACHE_FOLDER_NAME = "cache_dg" + val CLASS_LOCK = Object() + @GuardedBy("CLASS_LOCK") + val weakClassMap = WeakHashMap>() + val PROD_CERT_HASH = byteArrayOf(61, 122, 18, 35, 1, -102, -93, -99, -98, -96, -29, 67, 106, -73, -64, -119, 107, -5, 79, -74, 121, -12, -34, 95, -25, -62, 63, 50, 108, -113, -103, 74) + } +} diff --git a/vending-app/build.gradle b/vending-app/build.gradle index 7cc40124fe..d256e4f652 100644 --- a/vending-app/build.gradle +++ b/vending-app/build.gradle @@ -112,6 +112,7 @@ dependencies { //droidguard implementation project(':play-services-droidguard') + implementation project(':play-services-tasks-ktx') implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutineVersion" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutineVersion" @@ -122,6 +123,9 @@ dependencies { implementation "androidx.appcompat:appcompat:$appcompatVersion" implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.2' implementation "androidx.preference:preference-ktx:$preferenceVersion" + + // tink + implementation "com.google.crypto.tink:tink-android:$tinkVersion" } wire { diff --git a/vending-app/src/main/AndroidManifest.xml b/vending-app/src/main/AndroidManifest.xml index eb12ae7378..0150ecc799 100644 --- a/vending-app/src/main/AndroidManifest.xml +++ b/vending-app/src/main/AndroidManifest.xml @@ -175,5 +175,21 @@ + + + + + + + + + + + + diff --git a/vending-app/src/main/aidl/com/google/android/play/core/integrity/protocol/IExpressIntegrityService.aidl b/vending-app/src/main/aidl/com/google/android/play/core/integrity/protocol/IExpressIntegrityService.aidl new file mode 100644 index 0000000000..7b04d89205 --- /dev/null +++ b/vending-app/src/main/aidl/com/google/android/play/core/integrity/protocol/IExpressIntegrityService.aidl @@ -0,0 +1,15 @@ +/* + * SPDX-FileCopyrightText: 2022 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.play.core.integrity.protocol; + +import com.google.android.play.core.integrity.protocol.IExpressIntegrityServiceCallback; +import com.google.android.play.core.integrity.protocol.IRequestDialogCallback; + +interface IExpressIntegrityService { + void warmUpIntegrityToken(in Bundle bundle, in IExpressIntegrityServiceCallback callback) = 1; + void requestExpressIntegrityToken(in Bundle bundle, in IExpressIntegrityServiceCallback callback) = 2; + void requestAndShowDialog(in Bundle bundle, in IRequestDialogCallback callback) = 5; +} \ No newline at end of file diff --git a/vending-app/src/main/aidl/com/google/android/play/core/integrity/protocol/IExpressIntegrityServiceCallback.aidl b/vending-app/src/main/aidl/com/google/android/play/core/integrity/protocol/IExpressIntegrityServiceCallback.aidl new file mode 100644 index 0000000000..8f4cfbc579 --- /dev/null +++ b/vending-app/src/main/aidl/com/google/android/play/core/integrity/protocol/IExpressIntegrityServiceCallback.aidl @@ -0,0 +1,12 @@ +/* + * SPDX-FileCopyrightText: 2022 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.play.core.integrity.protocol; + +interface IExpressIntegrityServiceCallback { + void OnWarmUpIntegrityTokenCallback(in Bundle bundle) = 1; + void onRequestExpressIntegrityToken(in Bundle bundle) = 2; + void onRequestIntegrityToken(in Bundle bundle) = 3; +} diff --git a/vending-app/src/main/aidl/com/google/android/play/core/integrity/protocol/IIntegrityService.aidl b/vending-app/src/main/aidl/com/google/android/play/core/integrity/protocol/IIntegrityService.aidl new file mode 100644 index 0000000000..a08b4027be --- /dev/null +++ b/vending-app/src/main/aidl/com/google/android/play/core/integrity/protocol/IIntegrityService.aidl @@ -0,0 +1,14 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.play.core.integrity.protocol; + +import com.google.android.play.core.integrity.protocol.IIntegrityServiceCallback; +import com.google.android.play.core.integrity.protocol.IRequestDialogCallback; + +interface IIntegrityService { + void requestDialog(in Bundle bundle, in IRequestDialogCallback callback) = 0; + void requestIntegrityToken(in Bundle request, in IIntegrityServiceCallback callback) = 1; +} \ No newline at end of file diff --git a/vending-app/src/main/aidl/com/google/android/play/core/integrity/protocol/IIntegrityServiceCallback.aidl b/vending-app/src/main/aidl/com/google/android/play/core/integrity/protocol/IIntegrityServiceCallback.aidl new file mode 100644 index 0000000000..5b72b40d10 --- /dev/null +++ b/vending-app/src/main/aidl/com/google/android/play/core/integrity/protocol/IIntegrityServiceCallback.aidl @@ -0,0 +1,10 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.play.core.integrity.protocol; + +interface IIntegrityServiceCallback { + void onResult(in Bundle bundle) = 1; +} \ No newline at end of file diff --git a/vending-app/src/main/aidl/com/google/android/play/core/integrity/protocol/IRequestDialogCallback.aidl b/vending-app/src/main/aidl/com/google/android/play/core/integrity/protocol/IRequestDialogCallback.aidl new file mode 100644 index 0000000000..7dc3e85002 --- /dev/null +++ b/vending-app/src/main/aidl/com/google/android/play/core/integrity/protocol/IRequestDialogCallback.aidl @@ -0,0 +1,10 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.play.core.integrity.protocol; + +interface IRequestDialogCallback { + void onRequestAndShowDialog(in Bundle bundle); +} \ No newline at end of file diff --git a/vending-app/src/main/java/com/android/vending/licensing/LicenseChecker.kt b/vending-app/src/main/java/com/android/vending/licensing/LicenseChecker.kt index 057a709e15..7cd86e1a6a 100644 --- a/vending-app/src/main/java/com/android/vending/licensing/LicenseChecker.kt +++ b/vending-app/src/main/java/com/android/vending/licensing/LicenseChecker.kt @@ -9,7 +9,9 @@ import android.content.pm.PackageInfo import android.os.Bundle import android.os.RemoteException import android.util.Log +import com.android.vending.AUTH_TOKEN_SCOPE import com.android.vending.LicenseResult +import com.android.vending.getRequestHeaders import com.android.volley.VolleyError import org.microg.vending.billing.core.HttpClient import java.io.IOException @@ -69,7 +71,6 @@ const val ERROR_INVALID_PACKAGE_NAME: Int = 0x102 */ const val ERROR_NON_MATCHING_UID: Int = 0x103 -const val AUTH_TOKEN_SCOPE: String = "oauth2:https://www.googleapis.com/auth/googleplay" sealed class LicenseRequestParameters data class V1Parameters( @@ -145,7 +146,7 @@ suspend fun HttpClient.makeLicenseV1Request( packageName: String, auth: String, versionCode: Int, nonce: Long, androidId: Long ): V1Response? = get( url = "https://play-fe.googleapis.com/fdfe/apps/checkLicense?pkgn=$packageName&vc=$versionCode&nnc=$nonce", - headers = getLicenseRequestHeaders(auth, androidId), + headers = getRequestHeaders(auth, androidId), adapter = LicenseResult.ADAPTER ).information?.v1?.let { if (it.result != null && it.signedData != null && it.signature != null) { @@ -160,7 +161,7 @@ suspend fun HttpClient.makeLicenseV2Request( androidId: Long ): V2Response? = get( url = "https://play-fe.googleapis.com/fdfe/apps/checkLicenseServerFallback?pkgn=$packageName&vc=$versionCode", - headers = getLicenseRequestHeaders(auth, androidId), + headers = getRequestHeaders(auth, androidId), adapter = LicenseResult.ADAPTER ).information?.v2?.license?.jwt?.let { // Field present ←→ user has license diff --git a/vending-app/src/main/java/com/android/vending/licensing/LicenseRequestHeaders.kt b/vending-app/src/main/kotlin/com/android/vending/VendingRequestHeaders.kt similarity index 82% rename from vending-app/src/main/java/com/android/vending/licensing/LicenseRequestHeaders.kt rename to vending-app/src/main/kotlin/com/android/vending/VendingRequestHeaders.kt index 157d40efc3..82d5e3c3a3 100644 --- a/vending-app/src/main/java/com/android/vending/licensing/LicenseRequestHeaders.kt +++ b/vending-app/src/main/kotlin/com/android/vending/VendingRequestHeaders.kt @@ -1,26 +1,7 @@ -package com.android.vending.licensing +package com.android.vending import android.util.Base64 import android.util.Log -import com.android.vending.AndroidVersionMeta -import com.android.vending.DeviceMeta -import com.android.vending.EncodedTriple -import com.android.vending.EncodedTripleWrapper -import com.android.vending.IntWrapper -import com.android.vending.LicenseRequestHeader -import com.android.vending.Locality -import com.android.vending.LocalityWrapper -import com.android.vending.StringWrapper -import com.android.vending.Timestamp -import com.android.vending.TimestampContainer -import com.android.vending.TimestampContainer1 -import com.android.vending.TimestampContainer1Wrapper -import com.android.vending.TimestampContainer2 -import com.android.vending.TimestampStringWrapper -import com.android.vending.TimestampWrapper -import com.android.vending.UnknownByte12 -import com.android.vending.UserAgent -import com.android.vending.Uuid import com.google.android.gms.common.BuildConfig import okio.ByteString import org.microg.gms.profile.Build @@ -30,12 +11,14 @@ import java.net.URLEncoder import java.util.UUID import java.util.zip.GZIPOutputStream -private const val TAG = "FakeLicenseRequest" +private const val TAG = "VendingRequestHeaders" + +const val AUTH_TOKEN_SCOPE: String = "oauth2:https://www.googleapis.com/auth/googleplay" private const val BASE64_FLAGS = Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING private const val FINSKY_VERSION = "Finsky/37.5.24-29%20%5B0%5D%20%5BPR%5D%20565477504" -internal fun getLicenseRequestHeaders(auth: String, androidId: Long): Map { +internal fun getRequestHeaders(auth: String, androidId: Long): Map { var millis = System.currentTimeMillis() val timestamp = TimestampContainer.Builder() .container2( @@ -139,16 +122,17 @@ internal fun getLicenseRequestHeaders(auth: String, androidId: Long): Map= 33) { + getPackageInfo(packageName, PackageManager.PackageInfoFlags.of(flags.toLong())) + } else { + getPackageInfo(packageName, flags) + } + }.getOrDefault(getPackageInfo(packageName, flags)) +} + +val SIGNING_FLAGS = if (Build.VERSION.SDK_INT >= 28) { + PackageManager.GET_SIGNING_CERTIFICATES +} else { + @Suppress("DEPRECATION") PackageManager.GET_SIGNATURES +} + +val PackageInfo.signaturesCompat: Array + get() { + return if (Build.VERSION.SDK_INT >= 28) { + if (signingInfo.hasMultipleSigners()) { + signingInfo.apkContentsSigners + } else { + signingInfo.signingCertificateHistory + } + } else { + @Suppress("DEPRECATION") signatures + } + } + +fun ByteArray.encodeBase64(noPadding: Boolean, noWrap: Boolean = true, urlSafe: Boolean = true): String { + var flags = 0 + if (noPadding) flags = flags or Base64.NO_PADDING + if (noWrap) flags = flags or Base64.NO_WRAP + if (urlSafe) flags = flags or Base64.URL_SAFE + return Base64.encodeToString(this, flags) +} + +fun ByteArray.sha256(): ByteArray { + return MessageDigest.getInstance("SHA-256").digest(this) +} + +fun Bundle.buildPlayCoreVersion() = PlayCoreVersion( + major = getInt(KEY_VERSION_MAJOR, 0), minor = getInt(KEY_VERSION_MINOR, 0), patch = getInt(KEY_VERSION_PATCH, 0) +) + +fun readAes128GcmBuilderFromClientKey(clientKey: ClientKey?): Aead? { + if (clientKey == null) { + return null + } + return try { + val keySetHandle = CleartextKeysetHandle.read(BinaryKeysetReader.withBytes(clientKey.keySetHandle?.toByteArray())) + keySetHandle.getPrimitive(Aead::class.java) + } catch (e: Exception) { + null + } +} + +suspend fun getIntegrityRequestWrapper(context: Context, expressIntegritySession: ExpressIntegritySession, accountName: String) = withContext(Dispatchers.IO){ + fun getUpdatedWebViewRequestMode(webViewRequestMode: Int): Int { + return when (webViewRequestMode) { + in 0..2 -> webViewRequestMode + 1 + else -> 1 + } + } + val expressFilePB = FileInputStream(context.getProtoFile()).use { input -> ExpressFilePB.ADAPTER.decode(input) } + expressFilePB.integrityRequestWrapper.filter { item -> + TextUtils.equals(item.packageName, expressIntegritySession.packageName) && item.cloudProjectNumber == expressIntegritySession.cloudProjectVersion && getUpdatedWebViewRequestMode( + expressIntegritySession.webViewRequestMode + ) == getUpdatedWebViewRequestMode( + item.webViewRequestMode ?: 0 + ) + }.firstOrNull { item -> + TextUtils.equals(item.accountName, accountName) + } +} + +fun fetchCertificateChain(context: Context, attestationChallenge: ByteArray?): List { + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) { + val devicePropertiesAttestationIncluded = context.packageManager.hasSystemFeature("android.software.device_id_attestation") + val keyGenParameterSpecBuilder = + KeyGenParameterSpec.Builder("integrity.api.key.alias", KeyProperties.PURPOSE_SIGN).setAlgorithmParameterSpec(ECGenParameterSpec("secp256r1")).setDigests(KeyProperties.DIGEST_SHA512) + .setAttestationChallenge(attestationChallenge) + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) { + keyGenParameterSpecBuilder.setDevicePropertiesAttestationIncluded(devicePropertiesAttestationIncluded) + } + val keyGenParameterSpec = keyGenParameterSpecBuilder.build() + val keyPairGenerator = KeyPairGenerator.getInstance("EC", "AndroidKeyStore").apply { + initialize(keyGenParameterSpec) + } + if (keyPairGenerator.generateKeyPair() == null) { + Log.w(TAG, "Failed to create the key pair.") + return emptyList() + } + val keyStore: KeyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) } + val certificateChainList = keyStore.getCertificateChain(keyGenParameterSpec.keystoreAlias)?.let { chain -> + chain.map { it.encoded.toByteString() } + } + if (certificateChainList.isNullOrEmpty()) { + Log.w(TAG, "Failed to get the certificate chain.") + return emptyList() + } + return certificateChainList + } else { + return emptyList() + } +} + +suspend fun updateLocalExpressFilePB(context: Context, intermediateIntegrityResponseData: IntermediateIntegrityResponseData) = withContext(Dispatchers.IO) { + Log.d(TAG, "Writing AAR to express cache") + val intermediateIntegrity = intermediateIntegrityResponseData.intermediateIntegrity + val expressFilePB = FileInputStream(context.getProtoFile()).use { input -> ExpressFilePB.ADAPTER.decode(input) } + + val integrityResponseWrapper = IntegrityRequestWrapper.Builder().apply { + accountName = intermediateIntegrity.accountName + packageName = intermediateIntegrity.packageName + cloudProjectNumber = intermediateIntegrity.cloudProjectNumber + callerKey = intermediateIntegrity.callerKey + webViewRequestMode = intermediateIntegrity.webViewRequestMode.let { + when (it) { + in 0..2 -> it + 1 + else -> 1 + } + } - 1 + deviceIntegrityWrapper = DeviceIntegrityWrapper.Builder().apply { + creationTime = intermediateIntegrity.callerKey.generated + serverGenerated = intermediateIntegrity.serverGenerated + deviceIntegrityToken = intermediateIntegrity.intermediateToken + }.build() + }.build() + + val requestList = expressFilePB.integrityRequestWrapper.toMutableList() + + for ((index, item) in requestList.withIndex()) { + if (TextUtils.equals(item.packageName, intermediateIntegrity.packageName) && item.cloudProjectNumber == intermediateIntegrity.cloudProjectNumber && TextUtils.equals( + item.accountName, intermediateIntegrity.accountName + ) + ) { + if (integrityResponseWrapper.webViewRequestMode == item.webViewRequestMode) { + requestList[index] = integrityResponseWrapper + val newExpressFilePB = expressFilePB.newBuilder().integrityRequestWrapper(requestList).build() + FileOutputStream(context.getProtoFile()).use { output -> ExpressFilePB.ADAPTER.encode(output, newExpressFilePB) } + return@withContext + } + } + } + requestList.add(integrityResponseWrapper) + val newExpressFilePB = expressFilePB.newBuilder().integrityRequestWrapper(requestList).build() + FileOutputStream(context.getProtoFile()).use { output -> ExpressFilePB.ADAPTER.encode(output, newExpressFilePB) } +} + +suspend fun updateExpressSessionTime(context: Context, expressIntegritySession: ExpressIntegritySession, refreshWarmUpMethodTime: Boolean, refreshRequestMethodTime: Boolean) = + withContext(Dispatchers.IO) { + val packageName = if (expressIntegritySession.webViewRequestMode != 0) { + "WebView_" + expressIntegritySession.packageName + } else { + expressIntegritySession.packageName + } + + val expressFilePB = FileInputStream(context.getProtoFile()).use { input -> ExpressFilePB.ADAPTER.decode(input) } + + val clientKey = expressFilePB.integrityTokenTimeMap ?: IntegrityTokenTimeMap() + val timeMutableMap = clientKey.newBuilder().timeMap.toMutableMap() + + if (refreshWarmUpMethodTime) { + timeMutableMap[packageName] = IntegrityTokenTime.Builder().warmUpTokenTime( + TokenTime.Builder().type(1).timestamp(makeTimestamp(System.currentTimeMillis())).build() + ).build() + } + + if (refreshRequestMethodTime) { + timeMutableMap[packageName] = IntegrityTokenTime.Builder().requestTokenTime( + TokenTime.Builder().type(1).timestamp(makeTimestamp(System.currentTimeMillis())).build() + ).build() + } + + val newExpressFilePB = expressFilePB.newBuilder().integrityTokenTimeMap(IntegrityTokenTimeMap.Builder().timeMap(timeMutableMap).build()).build() + FileOutputStream(context.getProtoFile()).use { output -> ExpressFilePB.ADAPTER.encode(output, newExpressFilePB) } + } + +suspend fun updateExpressClientKey(context: Context) = withContext(Dispatchers.IO) { + val expressFilePB = FileInputStream(context.getProtoFile()).use { input -> ExpressFilePB.ADAPTER.decode(input) } + + val oldClientKey = expressFilePB.clientKey ?: ClientKey() + var clientKey = ClientKey.Builder().apply { + val currentTimeMillis = System.currentTimeMillis() + generated = Timestamp.Builder().seconds(currentTimeMillis / 1000).nanos((Math.floorMod(currentTimeMillis, 1000L) * 1000000).toInt()).build() + val keySetHandle = KeysetHandle.generateNew(AesGcmKeyManager.aes128GcmTemplate()) + val outputStream = ByteArrayOutputStream() + CleartextKeysetHandle.write(keySetHandle, BinaryKeysetWriter.withOutputStream(outputStream)) + this.keySetHandle = ByteBuffer.wrap(outputStream.toByteArray()).toByteString() + }.build() + if (oldClientKey.keySetHandle?.size != 0) { + if (oldClientKey.generated?.seconds != null && clientKey.generated?.seconds != null && oldClientKey.generated.seconds < clientKey.generated?.seconds!!.minus(TEMPORARY_DEVICE_KEY_VALIDITY)) { + clientKey = oldClientKey + } + } + + val newExpressFilePB = expressFilePB.newBuilder().clientKey(clientKey).build() + FileOutputStream(context.getProtoFile()).use { output -> ExpressFilePB.ADAPTER.encode(output, newExpressFilePB) } + clientKey +} + +suspend fun updateExpressAuthTokenWrapper(context: Context, expressIntegritySession: ExpressIntegritySession, authToken: String, clientKey: ClientKey) = withContext(Dispatchers.IO) { + var expressFilePB = FileInputStream(context.getProtoFile()).use { input -> ExpressFilePB.ADAPTER.decode(input) } + + val createTimeSeconds = expressFilePB.tokenWrapper?.deviceIntegrityWrapper?.creationTime?.seconds ?: 0 + val lastManualSoftRefreshTime = expressFilePB.tokenWrapper?.lastManualSoftRefreshTime?.seconds ?: 0 + if (createTimeSeconds < System.currentTimeMillis() - DEVICE_INTEGRITY_HARD_EXPIRATION) { + expressFilePB = expressFilePB.newBuilder().tokenWrapper(regenerateToken(context, authToken, expressIntegritySession.packageName, clientKey)).build() + } else if (lastManualSoftRefreshTime <= System.currentTimeMillis() - DEVICE_INTEGRITY_SOFT_EXPIRATION_CHECK_PERIOD && createTimeSeconds < System.currentTimeMillis() - DEVICE_INTEGRITY_SOFT_EXPIRATION) { + expressFilePB = expressFilePB.newBuilder().tokenWrapper(regenerateToken(context, authToken, expressIntegritySession.packageName, clientKey)).build() + } + + FileOutputStream(context.getProtoFile()).use { output -> ExpressFilePB.ADAPTER.encode(output, expressFilePB) } + + expressFilePB +} + +private suspend fun regenerateToken( + context: Context, authToken: String, packageName: String, clientKey: ClientKey +): AuthTokenWrapper { + try { + Log.d(TAG, "regenerateToken authToken:$authToken, packageName:$packageName, clientKey:$clientKey") + val droidGuardSessionTokenResponse = requestDroidGuardSessionToken(context, authToken) + + if (droidGuardSessionTokenResponse.tokenWrapper == null) { + throw RuntimeException("regenerateToken droidGuardSessionTokenResponse.tokenWrapper is Empty!") + } + + val droidGuardTokenType = droidGuardSessionTokenResponse.tokenWrapper.tokenContent?.tokenType?.firstOrNull { it.type?.toInt() == 5 } + ?: throw RuntimeException("regenerateToken droidGuardTokenType is null!") + + val droidGuardTokenSession = droidGuardTokenType.tokenSessionWrapper?.wrapper?.sessionContent?.session?.id + if (droidGuardTokenSession.isNullOrEmpty()) { + throw RuntimeException("regenerateToken droidGuardTokenSession is null") + } + + val data = mutableMapOf(KEY_DROID_GUARD_SESSION_TOKEN_V1 to droidGuardTokenSession) + val droidGuardData = withContext(Dispatchers.IO) { + val droidGuardResultsRequest = DroidGuardResultsRequest().apply { + bundle.putByteArray(PARAMS_PIA_EXPRESS_DEVICE_KEY, clientKey.keySetHandle?.toByteArray()) + } + Log.d(TAG, "Running DroidGuard (flow: $EXPRESS_INTEGRITY_FLOW_NAME, data: $data)") + DroidGuard.getClient(context).getResults(EXPRESS_INTEGRITY_FLOW_NAME, data, droidGuardResultsRequest).await().encode() + } + + val deviceIntegrityTokenResponse = requestDeviceIntegrityToken(context, authToken, droidGuardTokenSession, droidGuardData) + + val deviceIntegrityTokenType = deviceIntegrityTokenResponse.tokenWrapper?.tokenContent?.tokenType?.firstOrNull { it.type?.toInt() == 5 } + ?: throw RuntimeException("regenerateToken deviceIntegrityTokenType is null!") + + val deviceIntegrityToken = deviceIntegrityTokenType.tokenSessionWrapper?.wrapper?.sessionContent?.tokenContent?.tokenWrapper?.token + + return AuthTokenWrapper.Builder().apply { + this.clientKey = clientKey + this.deviceIntegrityWrapper = DeviceIntegrityWrapper.Builder().apply { + this.deviceIntegrityToken = deviceIntegrityToken ?: ByteString.EMPTY + this.creationTime = makeTimestamp(System.currentTimeMillis()) + }.build() + }.build() + } catch (e: Exception) { + Log.d(TAG, "regenerateToken: error ", e) + return AuthTokenWrapper() + } +} + +private suspend fun requestDroidGuardSessionToken(context: Context, authToken: String): TokenResponse { + val tokenWrapper = TokenRequestWrapper.Builder().apply { + request = mutableListOf(TokenRequest.Builder().apply { + droidGuardBody = DroidGuardBody.Builder().apply { + tokenBody = DroidGuardSessionTokenContent() + }.build() + }.build()) + }.build() + return requestExpressSyncData(context, authToken, tokenWrapper) +} + +private suspend fun requestDeviceIntegrityToken( + context: Context, authToken: String, session: String, token: ByteString +): TokenResponse { + val tokenWrapper = TokenRequestWrapper.Builder().apply { + request = mutableListOf(TokenRequest.Builder().apply { + droidGuardBody = DroidGuardBody.Builder().apply { + deviceBody = DeviceIntegrityTokenContent.Builder().apply { + sessionWrapper = SessionWrapper.Builder().apply { + type = KEY_DROID_GUARD_SESSION_TOKEN_V1 + this.session = Session.Builder().apply { + id = session + }.build() + }.build() + this.token = token.utf8() + flowName = EXPRESS_INTEGRITY_FLOW_NAME + }.build() + }.build() + }.build()) + }.build() + return requestExpressSyncData(context, authToken, tokenWrapper) +} + +suspend fun getAuthToken(context: Context, authTokenType: String): String { + val accountManager = AccountManager.get(context) + val accounts = accountManager.getAccountsByType(DEFAULT_ACCOUNT_TYPE) + var oauthToken = "" + if (accounts.isEmpty()) { + Log.w(TAG, "getAuthToken: No Google account found") + } else for (account in accounts) { + val result = suspendCoroutine { continuation -> + accountManager.getAuthToken(account, authTokenType, false, { future: AccountManagerFuture -> + try { + val result = future.result.getString(AccountManager.KEY_AUTHTOKEN) + continuation.resume(result) + } catch (e: Exception) { + Log.w(TAG, "getAuthToken: ", e) + continuation.resume(null) + } + }, null) + } + if (result != null) { + oauthToken = result + break + } + } + return oauthToken +} + +suspend fun requestIntegritySyncData(context: Context, authToken: String, request: IntegrityRequest): IntegrityResponse { + val androidId = GServices.getString(context.contentResolver, "android_id", "0")?.toLong() ?: 1 + return HttpClient(context).post( + url = "https://play-fe.googleapis.com/fdfe/integrity", + headers = getRequestHeaders(authToken, androidId), + payload = request, + adapter = IntegrityResponse.ADAPTER + ) +} + +suspend fun requestExpressSyncData(context: Context, authToken: String, request: TokenRequestWrapper): TokenResponse { + val androidId = GServices.getString(context.contentResolver, "android_id", "0")?.toLong() ?: 1 + return HttpClient(context).post( + url = "https://play-fe.googleapis.com/fdfe/sync?nocache_qos=lt", + headers = getRequestHeaders(authToken, androidId), + payload = request, + adapter = TokenResponse.ADAPTER + ) +} + +suspend fun requestIntermediateIntegrity( + context: Context, authToken: String, request: IntermediateIntegrityRequest +): IntermediateIntegrityResponseWrapperExtend { + val androidId = GServices.getString(context.contentResolver, "android_id", "0")?.toLong() ?: 1 + return HttpClient(context).post( + url = "https://play-fe.googleapis.com/fdfe/intermediateIntegrity", + headers = getRequestHeaders(authToken, androidId), + payload = request, + adapter = IntermediateIntegrityResponseWrapperExtend.ADAPTER + ) +} diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/expressintegrityservice/DeviceIntegrity.kt b/vending-app/src/main/kotlin/com/google/android/finsky/expressintegrityservice/DeviceIntegrity.kt new file mode 100644 index 0000000000..2a4f058910 --- /dev/null +++ b/vending-app/src/main/kotlin/com/google/android/finsky/expressintegrityservice/DeviceIntegrity.kt @@ -0,0 +1,14 @@ +/* + * SPDX-FileCopyrightText: 2022 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.finsky.expressintegrityservice + +import com.android.vending.Timestamp +import com.google.android.finsky.ClientKey +import okio.ByteString + +data class DeviceIntegrity( + var clientKey: ClientKey?, var deviceIntegrityToken: ByteString?, var creationTime: Timestamp?, var lastManualSoftRefreshTime: Timestamp? +) \ No newline at end of file diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/expressintegrityservice/DeviceIntegrityAndExpiredKey.kt b/vending-app/src/main/kotlin/com/google/android/finsky/expressintegrityservice/DeviceIntegrityAndExpiredKey.kt new file mode 100644 index 0000000000..0a78df4996 --- /dev/null +++ b/vending-app/src/main/kotlin/com/google/android/finsky/expressintegrityservice/DeviceIntegrityAndExpiredKey.kt @@ -0,0 +1,8 @@ +/* + * SPDX-FileCopyrightText: 2022 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.finsky.expressintegrityservice + +data class DeviceIntegrityAndExpiredKey(var deviceIntegrity: DeviceIntegrity, var expiredDeviceKey: Any?) \ No newline at end of file diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/expressintegrityservice/DeviceIntegrityResponse.kt b/vending-app/src/main/kotlin/com/google/android/finsky/expressintegrityservice/DeviceIntegrityResponse.kt new file mode 100644 index 0000000000..fd006209c8 --- /dev/null +++ b/vending-app/src/main/kotlin/com/google/android/finsky/expressintegrityservice/DeviceIntegrityResponse.kt @@ -0,0 +1,9 @@ +/* + * SPDX-FileCopyrightText: 2022 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.finsky.expressintegrityservice +data class DeviceIntegrityResponse( + var deviceIntegrity: DeviceIntegrity, var attemptedDroidGuardTokenRefresh: Boolean, var deviceKeyMd5: String, var expiredDeviceKey: Any? +) diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/expressintegrityservice/ExpressIntegrityService.kt b/vending-app/src/main/kotlin/com/google/android/finsky/expressintegrityservice/ExpressIntegrityService.kt new file mode 100644 index 0000000000..ae758baa1d --- /dev/null +++ b/vending-app/src/main/kotlin/com/google/android/finsky/expressintegrityservice/ExpressIntegrityService.kt @@ -0,0 +1,338 @@ +/** + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.finsky.expressintegrityservice + +import android.accounts.AccountManager +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.os.Bundle +import android.os.IBinder +import android.os.RemoteException +import android.text.TextUtils +import android.util.Base64 +import android.util.Log +import androidx.core.os.bundleOf +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LifecycleService +import androidx.lifecycle.lifecycleScope +import com.android.vending.AUTH_TOKEN_SCOPE +import com.android.vending.Timestamp +import com.android.vending.makeTimestamp +import com.android.volley.AuthFailureError +import com.google.android.finsky.AuthTokenWrapper +import com.google.android.finsky.ClientKey +import com.google.android.finsky.ClientKeyExtend +import com.google.android.finsky.DeviceIntegrityWrapper +import com.google.android.finsky.ExpressIntegrityResponse +import com.google.android.finsky.IntermediateIntegrityRequest +import com.google.android.finsky.IntermediateIntegritySession +import com.google.android.finsky.KEY_CLOUD_PROJECT +import com.google.android.finsky.KEY_NONCE +import com.google.android.finsky.KEY_OPT_PACKAGE +import com.google.android.finsky.KEY_PACKAGE_NAME +import com.google.android.finsky.KEY_REQUEST_MODE +import com.google.android.finsky.KEY_ERROR +import com.google.android.finsky.KEY_REQUEST_TOKEN_SID +import com.google.android.finsky.KEY_REQUEST_VERDICT_OPT_OUT +import com.google.android.finsky.KEY_TOKEN +import com.google.android.finsky.KEY_WARM_UP_SID +import com.google.android.finsky.PlayProtectDetails +import com.google.android.finsky.PlayProtectState +import com.google.android.finsky.RESULT_UN_AUTH +import com.google.android.finsky.RequestMode +import com.google.android.finsky.buildPlayCoreVersion +import com.google.android.finsky.encodeBase64 +import com.google.android.finsky.fetchCertificateChain +import com.google.android.finsky.getAuthToken +import com.google.android.finsky.getIntegrityRequestWrapper +import com.google.android.finsky.getPackageInfoCompat +import com.google.android.finsky.model.IntegrityErrorCode +import com.google.android.finsky.readAes128GcmBuilderFromClientKey +import com.google.android.finsky.requestIntermediateIntegrity +import com.google.android.finsky.sha256 +import com.google.android.finsky.signaturesCompat +import com.google.android.finsky.updateExpressAuthTokenWrapper +import com.google.android.finsky.updateExpressClientKey +import com.google.android.finsky.updateExpressSessionTime +import com.google.android.finsky.updateLocalExpressFilePB +import com.google.android.play.core.integrity.protocol.IExpressIntegrityService +import com.google.android.play.core.integrity.protocol.IExpressIntegrityServiceCallback +import com.google.android.play.core.integrity.protocol.IRequestDialogCallback +import com.google.crypto.tink.config.TinkConfig +import okio.ByteString.Companion.toByteString +import org.microg.gms.profile.ProfileManager +import org.microg.vending.billing.DEFAULT_ACCOUNT_TYPE +import kotlin.random.Random + +private const val TAG = "ExpressIntegrityService" + +class ExpressIntegrityService : LifecycleService() { + + override fun onBind(intent: Intent): IBinder? { + super.onBind(intent) + ProfileManager.ensureInitialized(this) + Log.d(TAG, "onBind") + TinkConfig.register() + return ExpressIntegrityServiceImpl(this, lifecycle).asBinder() + } + + override fun onUnbind(intent: Intent?): Boolean { + Log.d(TAG, "onUnbind") + return super.onUnbind(intent) + } +} + +private class ExpressIntegrityServiceImpl(private val context: Context, override val lifecycle: Lifecycle) : IExpressIntegrityService.Stub(), LifecycleOwner { + + override fun warmUpIntegrityToken(bundle: Bundle, callback: IExpressIntegrityServiceCallback) { + lifecycleScope.launchWhenCreated { + runCatching { + val authToken = getAuthToken(context, AUTH_TOKEN_SCOPE) + if (TextUtils.isEmpty(authToken)) { + Log.w(TAG, "warmUpIntegrityToken: Got null auth token for type: $AUTH_TOKEN_SCOPE") + } + Log.d(TAG, "warmUpIntegrityToken authToken: $authToken") + + val expressIntegritySession = ExpressIntegritySession( + packageName = bundle.getString(KEY_PACKAGE_NAME) ?: "", + cloudProjectVersion = bundle.getLong(KEY_CLOUD_PROJECT, 0L), + sessionId = Random.nextLong(), + null, + 0, + null, + webViewRequestMode = bundle.getInt(KEY_REQUEST_MODE, 0) + ) + updateExpressSessionTime(context, expressIntegritySession, refreshWarmUpMethodTime = true, refreshRequestMethodTime = false) + + val clientKey = updateExpressClientKey(context) + val expressFilePB = updateExpressAuthTokenWrapper(context, expressIntegritySession, authToken, clientKey) + + val tokenWrapper = expressFilePB.tokenWrapper ?: AuthTokenWrapper() + val tokenClientKey = tokenWrapper.clientKey ?: ClientKey() + val deviceIntegrityWrapper = tokenWrapper.deviceIntegrityWrapper ?: DeviceIntegrityWrapper() + val creationTime = tokenWrapper.deviceIntegrityWrapper?.creationTime ?: Timestamp() + val lastManualSoftRefreshTime = tokenWrapper.lastManualSoftRefreshTime ?: Timestamp() + + val deviceIntegrityAndExpiredKey = DeviceIntegrityAndExpiredKey( + deviceIntegrity = DeviceIntegrity( + tokenClientKey, deviceIntegrityWrapper.deviceIntegrityToken, creationTime, lastManualSoftRefreshTime + ), expressFilePB.expiredDeviceKey ?: ClientKey() + ) + + val deviceIntegrity = deviceIntegrityAndExpiredKey.deviceIntegrity + if (deviceIntegrity.deviceIntegrityToken?.size == 0 || deviceIntegrity.clientKey?.keySetHandle?.size == 0) { + throw RuntimeException("DroidGuard token is empty.") + } + + val deviceKeyMd5 = Base64.encodeToString( + deviceIntegrity.clientKey?.keySetHandle?.md5()?.toByteArray(), Base64.NO_PADDING or Base64.NO_WRAP or Base64.URL_SAFE + ) + if (deviceKeyMd5.isNullOrEmpty()) { + throw RuntimeException("Null deviceKeyMd5.") + } + + val deviceIntegrityResponse = DeviceIntegrityResponse( + deviceIntegrity, false, deviceKeyMd5, deviceIntegrityAndExpiredKey.expiredDeviceKey + ) + + val packageInfo = context.packageManager.getPackageInfoCompat( + expressIntegritySession.packageName, PackageManager.GET_SIGNING_CERTIFICATES or PackageManager.GET_SIGNATURES + ) + val certificateSha256Hashes = packageInfo.signaturesCompat.map { + it.toByteArray().sha256().encodeBase64(noPadding = true, noWrap = true, urlSafe = true) + } + + val packageInformation = PackageInformation(certificateSha256Hashes, packageInfo.versionCode) + + val clientKeyExtend = ClientKeyExtend.Builder().apply { + cloudProjectNumber = expressIntegritySession.cloudProjectVersion + keySetHandle = clientKey.keySetHandle + if (expressIntegritySession.webViewRequestMode == 2) { + this.optPackageName = KEY_OPT_PACKAGE + this.versionCode = 0 + } else { + this.optPackageName = expressIntegritySession.packageName + this.versionCode = packageInformation.versionCode + this.certificateSha256Hashes = packageInformation.certificateSha256Hashes + } + }.build() + + val certificateChainList = fetchCertificateChain(context, clientKeyExtend.keySetHandle?.sha256()?.toByteArray()) + + val sessionId = expressIntegritySession.sessionId + val playCoreVersion = bundle.buildPlayCoreVersion() + + Log.d(TAG, "warmUpIntegrityToken sessionId:$sessionId") + + val intermediateIntegrityRequest = IntermediateIntegrityRequest.Builder().apply { + deviceIntegrityToken(deviceIntegrityResponse.deviceIntegrity.deviceIntegrityToken) + readAes128GcmBuilderFromClientKey(deviceIntegrityResponse.deviceIntegrity.clientKey)?.let { + clientKeyExtendBytes(it.encrypt(clientKeyExtend.encode(), null).toByteString()) + } + playCoreVersion(playCoreVersion) + sessionId(sessionId) + certificateChainWrapper(IntermediateIntegrityRequest.CertificateChainWrapper(certificateChainList)) + playProtectDetails(PlayProtectDetails(PlayProtectState.PLAY_PROTECT_STATE_NONE)) + if (expressIntegritySession.webViewRequestMode != 0) { + requestMode(RequestMode.Builder().mode(expressIntegritySession.webViewRequestMode.takeIf { it in 0..2 } ?: 0).build()) + } + }.build() + + Log.d(TAG, "intermediateIntegrityRequest: $intermediateIntegrityRequest") + + val intermediateIntegrityResponse = requestIntermediateIntegrity(context, authToken, intermediateIntegrityRequest).intermediateIntegrityResponseWrapper?.intermediateIntegrityResponse + ?: throw RuntimeException("intermediateIntegrityResponse is null.") + + Log.d(TAG, "requestIntermediateIntegrity: $intermediateIntegrityResponse") + + val defaultAccountName: String = runCatching { + if (expressIntegritySession.webViewRequestMode != 0) { + RESULT_UN_AUTH + } else { + AccountManager.get(context).getAccountsByType(DEFAULT_ACCOUNT_TYPE).firstOrNull()?.name ?: RESULT_UN_AUTH + } + }.getOrDefault(RESULT_UN_AUTH) + + val intermediateIntegrityResponseData = IntermediateIntegrityResponseData( + intermediateIntegrity = IntermediateIntegrity( + expressIntegritySession.packageName, + expressIntegritySession.cloudProjectVersion, + defaultAccountName, + clientKey, + intermediateIntegrityResponse.intermediateToken, + intermediateIntegrityResponse.serverGenerated, + expressIntegritySession.webViewRequestMode, + 0 + ), + callerKeyMd5 = Base64.encodeToString( + clientKey.encode(), Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING + ), + appVersionCode = packageInformation.versionCode, + deviceIntegrityResponse = deviceIntegrityResponse, + appAccessRiskVerdictEnabled = intermediateIntegrityResponse.appAccessRiskVerdictEnabled + ) + + updateLocalExpressFilePB(context, intermediateIntegrityResponseData) + + callback.onWarmResult(bundleOf(KEY_WARM_UP_SID to sessionId)) + }.onFailure { + callback.onWarmResult(bundleOf(KEY_ERROR to IntegrityErrorCode.INTEGRITY_TOKEN_PROVIDER_INVALID)) + } + } + } + + override fun requestExpressIntegrityToken(bundle: Bundle, callback: IExpressIntegrityServiceCallback) { + Log.d(TAG, "requestExpressIntegrityToken bundle:$bundle") + lifecycleScope.launchWhenCreated { + val expressIntegritySession = ExpressIntegritySession( + packageName = bundle.getString(KEY_PACKAGE_NAME) ?: "", + cloudProjectVersion = bundle.getLong(KEY_CLOUD_PROJECT, 0L), + sessionId = Random.nextLong(), + requestHash = bundle.getString(KEY_NONCE), + originatingWarmUpSessionId = bundle.getLong(KEY_WARM_UP_SID, 0), + verdictOptOut = bundle.getIntegerArrayList(KEY_REQUEST_VERDICT_OPT_OUT), + webViewRequestMode = bundle.getInt(KEY_REQUEST_MODE, 0) + ) + + if (TextUtils.isEmpty(expressIntegritySession.packageName)) { + Log.w(TAG, "packageName is empty.") + callback.onRequestResult(bundleOf(KEY_ERROR to IntegrityErrorCode.INTERNAL_ERROR)) + return@launchWhenCreated + } + + if (expressIntegritySession.cloudProjectVersion <= 0L) { + Log.w(TAG, "cloudProjectVersion error") + callback.onRequestResult(bundleOf(KEY_ERROR to IntegrityErrorCode.CLOUD_PROJECT_NUMBER_IS_INVALID)) + return@launchWhenCreated + } + + if (expressIntegritySession.requestHash?.length!! > 500) { + Log.w(TAG, "requestHash error") + callback.onRequestResult(bundleOf(KEY_ERROR to IntegrityErrorCode.REQUEST_HASH_TOO_LONG)) + return@launchWhenCreated + } + + updateExpressSessionTime(context, expressIntegritySession, refreshWarmUpMethodTime = false, refreshRequestMethodTime = true) + + val defaultAccountName: String = runCatching { + if (expressIntegritySession.webViewRequestMode != 0) { + RESULT_UN_AUTH + } else { + AccountManager.get(context).getAccountsByType(DEFAULT_ACCOUNT_TYPE).firstOrNull()?.name ?: RESULT_UN_AUTH + } + }.getOrDefault(RESULT_UN_AUTH) + + val integrityRequestWrapper = getIntegrityRequestWrapper(context, expressIntegritySession, defaultAccountName) + if (integrityRequestWrapper == null) { + Log.w(TAG, "integrityRequestWrapper is null") + callback.onRequestResult(bundleOf(KEY_ERROR to IntegrityErrorCode.INTEGRITY_TOKEN_PROVIDER_INVALID)) + return@launchWhenCreated + } + + try { + val integritySession = IntermediateIntegritySession.Builder().creationTime(makeTimestamp(System.currentTimeMillis())).requestHash(expressIntegritySession.requestHash) + .sessionId(Random.nextBytes(8).toByteString()).timestampMillis(0).build() + + val expressIntegrityResponse = ExpressIntegrityResponse.Builder().apply { + this.deviceIntegrityToken = integrityRequestWrapper.deviceIntegrityWrapper?.deviceIntegrityToken + this.sessionHashAes128 = readAes128GcmBuilderFromClientKey(integrityRequestWrapper.callerKey)?.encrypt( + integritySession.encode(), null + )?.toByteString() + }.build() + + val token = Base64.encodeToString( + expressIntegrityResponse.encode(), Base64.NO_PADDING or Base64.NO_WRAP or Base64.URL_SAFE + ) + + callback.onRequestResult( + bundleOf( + KEY_TOKEN to token, + KEY_REQUEST_TOKEN_SID to expressIntegritySession.sessionId, + KEY_REQUEST_MODE to expressIntegritySession.webViewRequestMode + ) + ) + Log.d(TAG, "requestExpressIntegrityToken token: $token, sid: ${expressIntegritySession.sessionId}, mode: ${expressIntegritySession.webViewRequestMode}") + } catch (exception: RemoteException) { + Log.e(TAG, "requesting token has failed for ${expressIntegritySession.packageName}.") + callback.onRequestResult(bundleOf(KEY_ERROR to IntegrityErrorCode.INTEGRITY_TOKEN_PROVIDER_INVALID)) + } + } + } + + override fun requestAndShowDialog(bundle: Bundle?, callback: IRequestDialogCallback?) { + Log.d(TAG, "requestAndShowDialog bundle:$bundle") + callback?.onRequestAndShowDialog(bundleOf(KEY_ERROR to IntegrityErrorCode.INTERNAL_ERROR)) + } + +} + +private fun IExpressIntegrityServiceCallback.onWarmResult(result: Bundle) { + if (asBinder()?.isBinderAlive == false) { + Log.e(TAG, "onWarmResult IExpressIntegrityServiceCallback Binder died") + return + } + try { + OnWarmUpIntegrityTokenCallback(result) + } catch (e: Exception) { + Log.w(TAG, "error -> $e") + } + Log.d(TAG, "IExpressIntegrityServiceCallback onWarmResult success: $result") +} + +private fun IExpressIntegrityServiceCallback.onRequestResult(result: Bundle) { + if (asBinder()?.isBinderAlive == false) { + Log.e(TAG, "onRequestResult IExpressIntegrityServiceCallback Binder died") + return + } + try { + onRequestExpressIntegrityToken(result) + } catch (e: Exception) { + Log.w(TAG, "error -> $e") + } + Log.d(TAG, "IExpressIntegrityServiceCallback onRequestResult success: $result") +} \ No newline at end of file diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/expressintegrityservice/ExpressIntegritySession.kt b/vending-app/src/main/kotlin/com/google/android/finsky/expressintegrityservice/ExpressIntegritySession.kt new file mode 100644 index 0000000000..9116d4319e --- /dev/null +++ b/vending-app/src/main/kotlin/com/google/android/finsky/expressintegrityservice/ExpressIntegritySession.kt @@ -0,0 +1,15 @@ +/* + * SPDX-FileCopyrightText: 2022 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.finsky.expressintegrityservice +data class ExpressIntegritySession( + var packageName: String, + var cloudProjectVersion: Long, + var sessionId: Long, + var requestHash: String?, + var originatingWarmUpSessionId: Long, + var verdictOptOut: List?, + var webViewRequestMode: Int +) \ No newline at end of file diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/expressintegrityservice/IntermediateIntegrity.kt b/vending-app/src/main/kotlin/com/google/android/finsky/expressintegrityservice/IntermediateIntegrity.kt new file mode 100644 index 0000000000..8011597d5b --- /dev/null +++ b/vending-app/src/main/kotlin/com/google/android/finsky/expressintegrityservice/IntermediateIntegrity.kt @@ -0,0 +1,21 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.finsky.expressintegrityservice + +import com.android.vending.Timestamp +import com.google.android.finsky.ClientKey +import okio.ByteString + +data class IntermediateIntegrity( + var packageName: String, + var cloudProjectNumber: Long, + var accountName: String, + var callerKey: ClientKey, + var intermediateToken: ByteString?, + var serverGenerated: Timestamp?, + var webViewRequestMode: Int, + var testErrorCode: Int +) \ No newline at end of file diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/expressintegrityservice/IntermediateIntegrityResponse.kt b/vending-app/src/main/kotlin/com/google/android/finsky/expressintegrityservice/IntermediateIntegrityResponse.kt new file mode 100644 index 0000000000..be2c9339ae --- /dev/null +++ b/vending-app/src/main/kotlin/com/google/android/finsky/expressintegrityservice/IntermediateIntegrityResponse.kt @@ -0,0 +1,10 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.finsky.expressintegrityservice + +data class IntermediateIntegrityResponseData( + var intermediateIntegrity: IntermediateIntegrity, var callerKeyMd5: String, var appVersionCode: Int, var deviceIntegrityResponse: DeviceIntegrityResponse, var appAccessRiskVerdictEnabled: Boolean? +) diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/expressintegrityservice/PackageInformation.kt b/vending-app/src/main/kotlin/com/google/android/finsky/expressintegrityservice/PackageInformation.kt new file mode 100644 index 0000000000..a5779fee3e --- /dev/null +++ b/vending-app/src/main/kotlin/com/google/android/finsky/expressintegrityservice/PackageInformation.kt @@ -0,0 +1,7 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.finsky.expressintegrityservice +data class PackageInformation(var certificateSha256Hashes: List, var versionCode: Int) diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/extensions.kt b/vending-app/src/main/kotlin/com/google/android/finsky/extensions.kt index 4a1e53664d..8878c80097 100644 --- a/vending-app/src/main/kotlin/com/google/android/finsky/extensions.kt +++ b/vending-app/src/main/kotlin/com/google/android/finsky/extensions.kt @@ -17,9 +17,9 @@ import androidx.collection.ArraySet import androidx.collection.arrayMapOf import androidx.collection.arraySetOf import androidx.core.content.pm.PackageInfoCompat -import com.android.vending.licensing.AUTH_TOKEN_SCOPE +import com.android.vending.AUTH_TOKEN_SCOPE import com.android.vending.licensing.getAuthToken -import com.android.vending.licensing.getLicenseRequestHeaders +import com.android.vending.getRequestHeaders import com.google.android.finsky.assetmoduleservice.AssetPackException import com.google.android.finsky.assetmoduleservice.DownloadData import com.google.android.finsky.assetmoduleservice.ModuleData @@ -112,7 +112,7 @@ suspend fun HttpClient.initAssetModuleData( val moduleDeliveryInfo = post( url = ASSET_MODULE_DELIVERY_URL, - headers = getLicenseRequestHeaders(oauthToken, androidId), + headers = getRequestHeaders(oauthToken, androidId), payload = requestPayload, adapter = AssetModuleDeliveryResponse.ADAPTER ).wrapper?.deliveryInfo diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/integrityservice/IntegrityService.kt b/vending-app/src/main/kotlin/com/google/android/finsky/integrityservice/IntegrityService.kt new file mode 100644 index 0000000000..ff8f44d39d --- /dev/null +++ b/vending-app/src/main/kotlin/com/google/android/finsky/integrityservice/IntegrityService.kt @@ -0,0 +1,220 @@ +/** + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.finsky.integrityservice + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.os.IBinder +import android.os.SystemClock +import android.text.TextUtils +import android.util.Base64 +import android.util.Log +import androidx.core.os.bundleOf +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LifecycleService +import androidx.lifecycle.lifecycleScope +import com.android.vending.AUTH_TOKEN_SCOPE +import com.android.vending.makeTimestamp +import com.google.android.finsky.AccessibilityAbuseSignalDataWrapper +import com.google.android.finsky.AppAccessRiskDetailsResponse +import com.google.android.finsky.DisplayListenerMetadataWrapper +import com.google.android.finsky.INTEGRITY_FLOW_NAME +import com.google.android.finsky.INTEGRITY_PREFIX_ERROR +import com.google.android.finsky.InstalledAppsSignalDataWrapper +import com.google.android.finsky.IntegrityParams +import com.google.android.finsky.IntegrityRequest +import com.google.android.finsky.KEY_CLOUD_PROJECT +import com.google.android.finsky.KEY_NONCE +import com.google.android.finsky.KEY_PACKAGE_NAME +import com.google.android.finsky.PARAMS_BINDING_KEY +import com.google.android.finsky.PARAMS_GCP_N_KEY +import com.google.android.finsky.PARAMS_NONCE_SHA256_KEY +import com.google.android.finsky.PARAMS_PKG_KEY +import com.google.android.finsky.PARAMS_TM_S_KEY +import com.google.android.finsky.PARAMS_VC_KEY +import com.google.android.finsky.PackageNameWrapper +import com.google.android.finsky.PlayProtectDetails +import com.google.android.finsky.PlayProtectState +import com.google.android.finsky.SIGNING_FLAGS +import com.google.android.finsky.ScreenCaptureSignalDataWrapper +import com.google.android.finsky.ScreenOverlaySignalDataWrapper +import com.google.android.finsky.VersionCodeWrapper +import com.google.android.finsky.buildPlayCoreVersion +import com.google.android.finsky.encodeBase64 +import com.google.android.finsky.getAuthToken +import com.google.android.finsky.getPackageInfoCompat +import com.google.android.finsky.model.IntegrityErrorCode +import com.google.android.finsky.requestIntegritySyncData +import com.google.android.finsky.sha256 +import com.google.android.finsky.signaturesCompat +import com.google.android.gms.droidguard.DroidGuard +import com.google.android.gms.droidguard.internal.DroidGuardResultsRequest +import com.google.android.gms.tasks.await +import com.google.android.play.core.integrity.protocol.IIntegrityService +import com.google.android.play.core.integrity.protocol.IIntegrityServiceCallback +import com.google.android.play.core.integrity.protocol.IRequestDialogCallback +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import okio.ByteString.Companion.toByteString +import org.microg.gms.profile.ProfileManager + +private const val TAG = "IntegrityService" + +class IntegrityService : LifecycleService() { + + override fun onBind(intent: Intent): IBinder? { + super.onBind(intent) + ProfileManager.ensureInitialized(this) + Log.d(TAG, "onBind") + return IntegrityServiceImpl(this, lifecycle).asBinder() + } + + override fun onUnbind(intent: Intent?): Boolean { + Log.d(TAG, "onUnbind") + return super.onUnbind(intent) + } +} + +private class IntegrityServiceImpl(private val context: Context, override val lifecycle: Lifecycle) : IIntegrityService.Stub(), LifecycleOwner { + + override fun requestDialog(bundle: Bundle, callback: IRequestDialogCallback) { + Log.d(TAG, "Method (requestDialog) called but not implemented ") + } + + override fun requestIntegrityToken(request: Bundle, callback: IIntegrityServiceCallback) { + Log.d(TAG, "Method (requestIntegrityToken) called") + val packageName = request.getString(KEY_PACKAGE_NAME) + if (packageName == null) { + callback.onError("", IntegrityErrorCode.INTERNAL_ERROR, "Null packageName.") + return + } + val nonceArr = request.getByteArray(KEY_NONCE) + if (nonceArr == null || nonceArr.count() < 16L || nonceArr.count() > 500L) { + callback.onError(packageName, IntegrityErrorCode.INTERNAL_ERROR, "Nonce error.") + return + } + val cloudProjectNumber = request.getLong(KEY_CLOUD_PROJECT, 0L) + val playCoreVersion = request.buildPlayCoreVersion() + Log.d(TAG, "requestIntegrityToken(packageName: $packageName, nonce: ${nonceArr.encodeBase64(false)}, cloudProjectNumber: $cloudProjectNumber, playCoreVersion: $playCoreVersion)") + + val packageInfo = context.packageManager.getPackageInfoCompat(packageName, SIGNING_FLAGS) + val timestamp = makeTimestamp(System.currentTimeMillis()) + val versionCode = packageInfo.versionCode + + val integrityParams = IntegrityParams( + packageName = PackageNameWrapper(packageName), + versionCode = VersionCodeWrapper(versionCode), + nonce = nonceArr.encodeBase64(noPadding = false, noWrap = true, urlSafe = true), + certificateSha256Digests = packageInfo.signaturesCompat.map { + it.toByteArray().sha256().encodeBase64(noPadding = true, noWrap = true, urlSafe = true) + }, + timestampAtRequest = timestamp + ) + + val data = mutableMapOf( + PARAMS_PKG_KEY to packageName, + PARAMS_VC_KEY to versionCode.toString(), + PARAMS_NONCE_SHA256_KEY to nonceArr.sha256().encodeBase64(noPadding = true, noWrap = true, urlSafe = true), + PARAMS_TM_S_KEY to timestamp.seconds.toString(), + PARAMS_BINDING_KEY to integrityParams.encode().encodeBase64(noPadding = false, noWrap = true, urlSafe = true), + ) + if (cloudProjectNumber > 0L) { + data[PARAMS_GCP_N_KEY] = cloudProjectNumber.toString() + } + + var mapSize = 0 + data.entries.forEach { mapSize += it.key.toByteArray().size + it.value.toByteArray().size } + if (mapSize > 65536) { + callback.onError(packageName, IntegrityErrorCode.INTERNAL_ERROR, "Content binding size exceeded maximum allowed size.") + return + } + + lifecycleScope.launchWhenCreated { + runCatching { + val authToken = getAuthToken(context, AUTH_TOKEN_SCOPE) + if (TextUtils.isEmpty(authToken)) { + Log.w(TAG, "requestIntegrityToken: Got null auth token for type: $AUTH_TOKEN_SCOPE") + } + Log.d(TAG, "requestIntegrityToken authToken: $authToken") + + val droidGuardData = withContext(Dispatchers.IO) { + val droidGuardResultsRequest = DroidGuardResultsRequest() + droidGuardResultsRequest.bundle.putString("thirdPartyCallerAppPackageName", packageName) + Log.d(TAG, "Running DroidGuard (flow: $INTEGRITY_FLOW_NAME, data: $data)") + val droidGuardToken = DroidGuard.getClient(context).getResults(INTEGRITY_FLOW_NAME, data, droidGuardResultsRequest).await() + Log.d(TAG, "Running DroidGuard (flow: $INTEGRITY_FLOW_NAME, droidGuardToken: $droidGuardToken)") + Base64.decode(droidGuardToken, Base64.NO_PADDING or Base64.NO_WRAP or Base64.URL_SAFE).toByteString() + } + + if (droidGuardData.utf8().startsWith(INTEGRITY_PREFIX_ERROR)) { + Log.w(TAG, "droidGuardData: ${droidGuardData.utf8()}") + callback.onError(packageName, IntegrityErrorCode.INTERNAL_ERROR, "DroidGuard failed.") + return@launchWhenCreated + } + + val integrityRequest = IntegrityRequest( + params = integrityParams, + flowName = INTEGRITY_FLOW_NAME, + droidGuardTokenRaw = droidGuardData, + playCoreVersion = playCoreVersion, + playProtectDetails = PlayProtectDetails(PlayProtectState.PLAY_PROTECT_STATE_NO_PROBLEMS), + appAccessRiskDetailsResponse = AppAccessRiskDetailsResponse( + installedAppsSignalDataWrapper = InstalledAppsSignalDataWrapper("."), + screenCaptureSignalDataWrapper = ScreenCaptureSignalDataWrapper("."), + screenOverlaySignalDataWrapper = ScreenOverlaySignalDataWrapper("."), + accessibilityAbuseSignalDataWrapper = AccessibilityAbuseSignalDataWrapper(), + displayListenerMetadataWrapper = DisplayListenerMetadataWrapper( + lastDisplayAddedTimeDelta = makeTimestamp(SystemClock.elapsedRealtimeNanos()) + ) + ) + ) + Log.d(TAG, "requestIntegrityToken integrityRequest: $integrityRequest") + val integrityResponse = requestIntegritySyncData(context, authToken, integrityRequest) + Log.d(TAG, "requestIntegrityToken integrityResponse: $integrityResponse") + + val integrityToken = integrityResponse.contentWrapper?.content?.token + if (integrityToken.isNullOrEmpty()) { + callback.onError(packageName, IntegrityErrorCode.INTERNAL_ERROR, "IntegrityResponse didn't have a token") + return@launchWhenCreated + } + + Log.d(TAG, "requestIntegrityToken integrityToken: $integrityToken") + callback.onSuccess(packageName, integrityToken) + }.onFailure { + Log.w(TAG, "requestIntegrityToken has exception: ", it) + callback.onError(packageName, IntegrityErrorCode.INTERNAL_ERROR, it.message ?: "Exception") + } + } + } +} + +private fun IIntegrityServiceCallback.onError(packageName: String?, errorCode: Int, errorMsg: String) { + if (asBinder()?.isBinderAlive == false) { + Log.e(TAG, "IIntegrityServiceCallback onError Binder died") + return + } + try { + onResult(bundleOf("error" to errorCode)) + } catch (e: Exception) { + Log.e(TAG, "exception $packageName error -> $e") + } + Log.d(TAG, "requestIntegrityToken() failed for $packageName error -> $errorMsg") +} + +private fun IIntegrityServiceCallback.onSuccess(packageName: String?, token: String) { + if (asBinder()?.isBinderAlive == false) { + Log.e(TAG, "IIntegrityServiceCallback onSuccess Binder died") + return + } + try { + onResult(bundleOf("token" to token)) + } catch (e: Exception) { + Log.e(TAG, "exception $packageName error -> $e") + } + Log.d(TAG, "requestIntegrityToken() success for $packageName)") +} \ No newline at end of file diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/model/IntegrityErrorCode.kt b/vending-app/src/main/kotlin/com/google/android/finsky/model/IntegrityErrorCode.kt new file mode 100644 index 0000000000..4d9e4bdc70 --- /dev/null +++ b/vending-app/src/main/kotlin/com/google/android/finsky/model/IntegrityErrorCode.kt @@ -0,0 +1,172 @@ +/* + * SPDX-FileCopyrightText: 2022 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.finsky.model + +import org.microg.gms.common.PublicApi + +@PublicApi +annotation class IntegrityErrorCode { + companion object { + /** + * Integrity API is not available. + * + * Integrity API is not enabled, or the Play Store version might be old. + * + * Recommended actions: + * Make sure that Integrity API is enabled in Google Play Console. + * Ask the user to update Play Store. + */ + const val API_NOT_AVAILABLE = -1 + + /** + * PackageManager could not find this app. + * + * Something is wrong (possibly an attack). Non-actionable. + */ + const val APP_NOT_INSTALLED = -5 + + /** + * The calling app UID (user id) does not match the one from Package Manager. + * + * Something is wrong (possibly an attack). Non-actionable. + */ + const val APP_UID_MISMATCH = -7 + + /** + * Binding to the service in the Play Store has failed. + * This can be due to having an old Play Store version installed on the device. + * + * Ask the user to update Play Store. + */ + const val CANNOT_BIND_TO_SERVICE = -9 + + /** + * The provided request hash is too long. + * + * The request hash length must be less than 500 bytes. + * Retry with a shorter request hash. + * */ + const val REQUEST_HASH_TOO_LONG = -17 + + /** + * There is a transient error on the calling device. + * + * Retry with an exponential backoff. + * + * Introduced in Integrity Play Core version 1.1.0 (prior versions returned a token with empty Device Integrity Verdict). + * If the error persists after a few retries, you should assume that the device has failed integrity checks and act accordingly. + */ + const val CLIENT_TRANSIENT_ERROR = -18 + + /** + * The StandardIntegrityTokenProvider is invalid (e.g. it is outdated). + * + * Request a new integrity token provider by calling StandardIntegrityManager#prepareIntegrityToken. + * */ + const val INTEGRITY_TOKEN_PROVIDER_INVALID = -19 + + /** + * The provided cloud project number is invalid. + * + * Use the cloud project number which can be found in Project info in + * your Google Cloud Console for the cloud project where Play Integrity API is enabled. + */ + const val CLOUD_PROJECT_NUMBER_IS_INVALID = -16 + + /** + * Unknown internal Google server error. + * + * Retry with an exponential backoff. Consider filing a bug if fails consistently. + */ + const val GOOGLE_SERVER_UNAVAILABLE = -12 + + /** + * Unknown error processing integrity request. + * + * Retry with an exponential backoff. Consider filing a bug if fails consistently. + */ + const val INTERNAL_ERROR = -100 + + /** + * Network error: unable to obtain integrity details. + * + * Ask the user to check for a connection. + */ + const val NETWORK_ERROR = -3 + + /** + * Nonce is not encoded as a base64 web-safe no-wrap string. + * + * Retry with correct nonce format. + */ + const val NONCE_IS_NOT_BASE64 = -13 + + /** + * Nonce length is too long. The nonce must be less than 500 bytes before base64 encoding. + * + * Retry with a shorter nonce. + */ + const val NONCE_TOO_LONG = -11 + + /** + * Nonce length is too short. The nonce must be a minimum of 16 bytes (before base64 encoding) to allow for a better security. + * + * Retry with a longer nonce. + */ + const val NONCE_TOO_SHORT = -10 + + /** + * No error. + * + * This is the default value. + */ + const val NO_ERROR = 0 + + /** + * Google Play Services is not available or version is too old. + * + * Ask the user to Install or Update Play Services. + */ + const val PLAY_SERVICES_NOT_FOUND = -6 + + /** + * The Play Services needs to be updated. + * + * Ask the user to update Google Play Services. + */ + const val PLAY_SERVICES_VERSION_OUTDATED = -15 + + /** + * No active account found in the Play Store app. + * Note that the Play Integrity API now supports unauthenticated requests. + * This error code is used only for older Play Store versions that lack support. + * + * Ask the user to authenticate in Play Store. + */ + const val PLAY_STORE_ACCOUNT_NOT_FOUND = -4 + + /** + * The Play Store app is either not installed or not the official version. + * + * Ask the user to install an official and recent version of Play Store. + */ + const val PLAY_STORE_NOT_FOUND = -2 + + /** + * The Play Store needs to be updated. + * + * Ask the user to update the Google Play Store. + */ + const val PLAY_STORE_VERSION_OUTDATED = -14 + + /** + * The calling app is making too many requests to the API and hence is throttled. + * + * Retry with an exponential backoff. + */ + const val TOO_MANY_REQUESTS = -8 + } +} \ No newline at end of file diff --git a/vending-app/src/main/proto/Integrity.proto b/vending-app/src/main/proto/Integrity.proto new file mode 100644 index 0000000000..f90e4bac9d --- /dev/null +++ b/vending-app/src/main/proto/Integrity.proto @@ -0,0 +1,312 @@ +option java_package = "com.google.android.finsky"; +option java_multiple_files = true; + +import "Timestamp.proto"; + +message IntegrityRequest { + oneof droidGuardToken { + string droidGuardTokenBase64 = 2; + bytes droidGuardTokenRaw = 5; + } + optional IntegrityParams params = 1; + optional CompressType compressType = 4; + optional string flowName = 3; + optional PlayCoreVersion playCoreVersion = 6; + optional PlayProtectDetails playProtectDetails = 7; + optional AppAccessRiskDetailsResponse appAccessRiskDetailsResponse = 8; +} + +enum CompressType { + UNKNOWN_COMPRESSION_FORMAT = 0; + GZIP = 1; +} + +message PackageNameWrapper { + optional string value = 1; +} + +message VersionCodeWrapper { + optional int32 value = 1; +} + +message IntegrityParams { + optional PackageNameWrapper packageName = 1; + optional VersionCodeWrapper versionCode = 2; + optional string nonce = 3; + repeated string certificateSha256Digests = 4; + optional Timestamp timestampAtRequest = 5; + // optional int64 unknownInt64 = 6; +} + +message InstalledAppsSignalDataWrapper { + optional string installedAppsSignalData = 1; +} + +message ScreenCaptureSignalDataWrapper { + optional string screenCaptureSignalData = 1; +} + +message ScreenOverlaySignalDataWrapper { + optional string screenOverlaySignalData = 1; +} + +message AccessibilityAbuseSignalDataWrapper { + optional string accessibilityAbuseSignalData = 1; +} + +message DisplayListenerMetadataWrapper { + optional int32 isActiveDisplayPresent = 1; + optional Timestamp displayListenerInitialisationTimeDelta = 2; + optional Timestamp lastDisplayAddedTimeDelta = 3; + optional int32 displayListenerUsed = 4; +} + +message AppAccessRiskDetailsResponse { + optional InstalledAppsSignalDataWrapper installedAppsSignalDataWrapper = 1; + optional ScreenCaptureSignalDataWrapper screenCaptureSignalDataWrapper = 2; + optional ScreenOverlaySignalDataWrapper screenOverlaySignalDataWrapper = 3; + optional AccessibilityAbuseSignalDataWrapper accessibilityAbuseSignalDataWrapper = 4; + optional DisplayListenerMetadataWrapper displayListenerMetadataWrapper = 5; +} + +message IntegrityResponse { + optional IntegrityContentWrapper contentWrapper = 1; + optional IntegrityResponseError integrityResponseError = 2; +} + +message IntegrityResponseError { + optional string error = 2; +} + +message IntegrityContentWrapper { + optional IntegrityContent content = 186; +} + +message IntegrityContent { + optional string token = 1; +} + +message PlayCoreVersion { + optional int32 major = 1; + optional int32 minor = 2; + optional int32 patch = 3; +} + +message PlayProtectDetails { + optional PlayProtectState state = 1; +} + +enum PlayProtectState { + PLAY_PROTECT_STATE_UNKNOWN_PHA_STATE = 0; + PLAY_PROTECT_STATE_NONE = 1; + PLAY_PROTECT_STATE_NO_PROBLEMS = 2; + PLAY_PROTECT_STATE_WARNING = 3; + PLAY_PROTECT_STATE_DANGER = 4; + PLAY_PROTECT_STATE_OFF = 5; +} + +message MetricsUpdate { + optional string requestHash = 1; + optional int32 statesStored = 2; + optional int32 additionalStatesSet = 3; + optional int32 bytesStored = 4; +} + +message IntermediateIntegrityRequest { + optional bytes clientKeyExtendBytes = 1; + optional bytes deviceIntegrityToken = 2; + optional PlayCoreVersion playCoreVersion = 3; + optional PlayProtectDetails playProtectDetails = 4; + // optional bytes c = 5; + optional RequestMode requestMode = 6; + optional int64 sessionId = 7; + message CertificateChainWrapper { + repeated bytes certificateChains = 1; + } + optional CertificateChainWrapper certificateChainWrapper = 8; +} + +message IntermediateIntegrityResponseWrapperExtend { + optional IntermediateIntegrityResponseWrapper intermediateIntegrityResponseWrapper = 1; +} + +message IntermediateIntegrityResponseWrapper { + optional IntermediateIntegrityResponse intermediateIntegrityResponse = 221; +} + +message IntermediateIntegrityResponse { + optional bytes intermediateToken = 1; + optional Timestamp serverGenerated = 2; + optional bool appAccessRiskVerdictEnabled = 4; + optional ErrorResponse errorInfo = 5; +} + +message ExpressIntegrityResponse { + optional bytes deviceIntegrityToken = 1; + optional bytes sessionHashAes128 = 3; + optional bytes appAccessRiskDetailsResponse = 4; +} + +message ErrorResponse { + optional int32 errorCode = 1; + optional int32 status = 2; +} + +message IntermediateIntegritySession { + optional string requestHash = 1; + optional Timestamp creationTime = 2; + optional bytes sessionId = 3; + optional int32 timestampMillis = 4; +} + +message TokenTime { + optional int64 type = 2; + optional Timestamp timestamp = 3; +} + +message IntegrityTokenTime { + optional TokenTime warmUpTokenTime = 1; + optional TokenTime requestTokenTime = 2; +} + +message IntegrityTokenTimeMap { + map timeMap = 1; +} + +message ClientKey { + optional Timestamp generated = 2; + optional bytes keySetHandle = 3; +} + +message AuthTokenWrapper { + optional DeviceIntegrityWrapper deviceIntegrityWrapper = 1; + optional ClientKey clientKey = 2; + optional Timestamp lastManualSoftRefreshTime = 3; +} + +message DeviceIntegrityWrapper { + optional bytes deviceIntegrityToken = 1; + optional Timestamp creationTime = 2; + optional Timestamp serverGenerated = 3; + optional int32 errorCode = 5; +} + +message IntegrityRequestWrapper { + optional DeviceIntegrityWrapper deviceIntegrityWrapper = 1; + optional ClientKey callerKey = 2; + optional string packageName = 3; + optional string accountName = 4; + optional uint64 cloudProjectNumber = 5; + optional int32 webViewRequestMode = 7; +} + +message ExpressFilePB { + optional AuthTokenWrapper tokenWrapper = 2; + repeated IntegrityRequestWrapper integrityRequestWrapper = 3; + optional ClientKey clientKey = 4; + optional IntegrityTokenTimeMap integrityTokenTimeMap = 5; + optional ClientKey expiredDeviceKey = 6; +} + +message AccountNameWrapper { + optional string accountName = 1; +} + +enum INSTALLED_APPS_STATUS { + UNKNOWN_INSTALLED_APPS_SIGNAL = 0; + RECOGNIZED_APPS_INSTALLED = 1; + UNRECOGNIZED_APPS_INSTALLED = 2; +} + +message RequestMode { + optional int32 mode = 1; +} + +message ClientKeyExtend { + optional string optPackageName = 1; + optional int32 versionCode = 2; + repeated string certificateSha256Hashes = 3; + optional int64 cloudProjectNumber = 4; + optional bytes keySetHandle = 5; +} + +message TokenRequestWrapper { + repeated TokenRequest request = 1; +} + +message TokenRequest { + oneof body { + DroidGuardBody droidGuardBody = 5; + } +} + +message DroidGuardBody { + oneof content { + DeviceIntegrityTokenContent deviceBody = 1; + DroidGuardSessionTokenContent tokenBody = 2; + } +} + +message DeviceIntegrityTokenContent { + oneof session { + SessionWrapper sessionWrapper = 1; + } + optional string token = 2; + optional string flowName = 3; +} + +message SessionWrapper { + optional string type = 1; + optional Session session = 2; +} + +message Session { + optional string id = 1; +} + +message DroidGuardSessionTokenContent {} + +message TokenResponse { + optional TokenWrapper tokenWrapper = 1; +} + +message TokenWrapper { + optional TokenContent tokenContent = 183; +} + +message TokenContent { + repeated TokenType tokenType = 1; + repeated string c = 3; +} + +message TokenType { + oneof token { + TokenSessionWrapper tokenSessionWrapper = 2; + } + optional int64 type = 1; +} + +message TokenSessionWrapper { + optional SessionContentWrapper wrapper = 1; +} + +message SessionContentWrapper { + oneof content { + SessionContent sessionContent = 4; + } +} + +message SessionContent { + oneof content { + TokenV1Content tokenContent = 2; + } + optional Session session = 1; +} + +message TokenV1Content { + optional TokenV1Wrapper tokenWrapper = 1; +} + +message TokenV1Wrapper { + optional bytes token = 1; +} \ No newline at end of file