diff --git a/vending-app/build.gradle b/vending-app/build.gradle
index 24ef7bd3e1..5f333ad318 100644
--- a/vending-app/build.gradle
+++ b/vending-app/build.gradle
@@ -4,6 +4,8 @@
*/
apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+apply plugin: 'com.squareup.wire'
android {
namespace "com.android.vending"
@@ -11,6 +13,7 @@ android {
buildToolsVersion "$androidBuildVersionTools"
defaultConfig {
+ multiDexEnabled = true
versionName vendingAppVersionName
versionCode vendingAppVersionCode
minSdkVersion androidMinSdk
@@ -33,13 +36,36 @@ android {
}
compileOptions {
+ coreLibraryDesugaringEnabled true
+
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
+
+ kotlinOptions {
+ jvmTarget = 1.8
+ }
}
dependencies {
implementation project(':fake-signature')
+
+ implementation project(':play-services-droidguard')
+ implementation project(':play-services-tasks-ktx')
+
+ coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.3")
+ implementation "androidx.annotation:annotation:$annotationVersion"
+ implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion"
+ implementation "androidx.lifecycle:lifecycle-service:$lifecycleVersion"
+
+ implementation "com.squareup.wire:wire-runtime:$wireVersion"
+ implementation("io.ktor:ktor-client-android:2.3.5")
+}
+
+wire {
+ kotlin {
+ javaInterop = true
+ }
}
if (file('user.gradle').exists()) {
diff --git a/vending-app/src/main/AndroidManifest.xml b/vending-app/src/main/AndroidManifest.xml
index 2a78870455..5a6ece7ddb 100644
--- a/vending-app/src/main/AndroidManifest.xml
+++ b/vending-app/src/main/AndroidManifest.xml
@@ -9,6 +9,8 @@
android:name="com.android.vending.CHECK_LICENSE"
android:protectionLevel="normal" />
+
+
+
+
+
+
+
+
+
+
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..b45329d6ac
--- /dev/null
+++ b/vending-app/src/main/aidl/com/google/android/play/core/integrity/protocol/IIntegrityService.aidl
@@ -0,0 +1,12 @@
+/*
+ * 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;
+
+interface IIntegrityService {
+ void requestIntegrityToken(in Bundle request, IIntegrityServiceCallback callback) = 1;
+}
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..a53278449b
--- /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 onRequestIntegrityToken(in Bundle request) = 1;
+}
diff --git a/vending-app/src/main/java/com/google/android/finsky/integrityservice/IntegrityService.kt b/vending-app/src/main/java/com/google/android/finsky/integrityservice/IntegrityService.kt
new file mode 100644
index 0000000000..65bcb62424
--- /dev/null
+++ b/vending-app/src/main/java/com/google/android/finsky/integrityservice/IntegrityService.kt
@@ -0,0 +1,192 @@
+/*
+ * SPDX-FileCopyrightText: 2023 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.util.Base64
+import android.util.Log
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.LifecycleService
+import androidx.lifecycle.lifecycleScope
+import com.google.android.finsky.ResponseWrapper
+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.model.IntegrityErrorCode
+import com.google.android.play.core.integrity.protocol.IIntegrityService
+import com.google.android.play.core.integrity.protocol.IIntegrityServiceCallback
+import io.ktor.client.HttpClient
+import io.ktor.client.call.body
+import io.ktor.client.engine.android.Android
+import io.ktor.client.request.accept
+import io.ktor.client.request.headers
+import io.ktor.client.request.post
+import io.ktor.client.request.setBody
+import io.ktor.http.ContentType
+import io.ktor.http.contentType
+import io.ktor.http.userAgent
+import kotlinx.coroutines.launch
+import okio.ByteString.Companion.toByteString
+import org.microg.vending.FINSKY_USER_AGENT
+import org.microg.vending.utils.SIGNING_FLAGS
+import org.microg.vending.utils.encodeBase64
+import org.microg.vending.utils.getPackageInfoCompat
+import org.microg.vending.utils.sha256
+import org.microg.vending.utils.signaturesCompat
+import java.io.InputStream
+import java.time.Instant
+
+private const val TAG = "IntegrityService"
+
+class IntegrityService : LifecycleService() {
+ override fun onBind(intent: Intent): IBinder {
+ super.onBind(intent)
+ return IntegrityServiceImpl(this, lifecycle).asBinder()
+ }
+}
+
+private const val DROIDGUARD_FLOW = "pia_attest_e1"
+
+class IntegrityServiceImpl(
+ private val context: Context,
+ private val lifecycle: Lifecycle,
+) : IIntegrityService.Stub(), LifecycleOwner {
+ override fun getLifecycle(): Lifecycle = lifecycle
+
+ // TODO use OkHttp or CIO
+ private val httpClient = HttpClient(Android)
+
+ override fun requestIntegrityToken(request: Bundle, callback: IIntegrityServiceCallback) {
+ val callingUid = getCallingUid()
+
+ lifecycleScope.launch {
+ try {
+ val packageName = request.getString("package.name")
+ val nonce = request.getByteArray("nonce")
+ val cloudProjectNumber = request.getLongOrNull("cloud.prj")
+ val playCoreVersion = PlayCoreVersion(
+ request.getInt("playcore.integrity.version.major", 1),
+ request.getInt("playcore.integrity.version.minor", 0),
+ request.getInt("playcore.integrity.version.patch", 0),
+ )
+
+ Log.d(
+ TAG,
+ "requestIntegrityToken(packageName: $packageName, nonce: ${nonce?.encodeBase64(false)}, cloudProjectNumber: $cloudProjectNumber, playCoreVersion: $playCoreVersion)"
+ )
+
+ if (packageName == null) throw IntegrityException(IntegrityErrorCode.INTERNAL_ERROR, "Package name missing")
+
+ if (nonce == null) throw IntegrityException(IntegrityErrorCode.INTERNAL_ERROR, "Nonce missing")
+ if (nonce.count() < 16) throw IntegrityException(IntegrityErrorCode.NONCE_TOO_SHORT)
+ if (nonce.count() > 500) throw IntegrityException(IntegrityErrorCode.NONCE_TOO_LONG)
+
+ val packageInfo = context.packageManager.getPackageInfoCompat(packageName, SIGNING_FLAGS)
+ if (packageInfo.applicationInfo.uid != callingUid) {
+ throw IntegrityException(
+ IntegrityErrorCode.APP_UID_MISMATCH,
+ "UID for the requested package name (${packageInfo.applicationInfo.uid}) doesn't match the calling UID ($callingUid)"
+ )
+ }
+
+ val certificateSha256Digests = packageInfo.signaturesCompat.map { it.toByteArray().sha256().encodeBase64(true) }
+
+ val versionCode = packageInfo.versionCode
+
+ val timestamp = Instant.now()
+
+ val details = IntegrityRequest.Details(
+ packageName = IntegrityRequest.Details.PackageNameWrapper(packageName),
+ versionCode = IntegrityRequest.Details.VersionCodeWrapper(versionCode),
+ nonce = nonce.encodeBase64(false),
+ certificateSha256Digests = certificateSha256Digests,
+ timestampAtRequest = timestamp,
+ cloudProjectNumber = cloudProjectNumber
+ )
+
+ val data = mutableMapOf(
+ "pkg_key" to packageName,
+ "vc_key" to versionCode.toString(),
+ "nonce_sha256_key" to nonce.sha256().encodeBase64(true),
+ "tm_s_key" to timestamp.epochSecond.toString(),
+ "binding_key" to details.encode().encodeBase64(false),
+ )
+
+ if (cloudProjectNumber != null) {
+ data["gcp_n_key"] = cloudProjectNumber.toString()
+ }
+
+ val droidGuardResultsRequest = DroidGuardResultsRequest()
+ droidGuardResultsRequest.bundle.putString("thirdPartyCallerAppPackageName", packageName)
+
+ Log.d(TAG, "Running DroidGuard (flow: $DROIDGUARD_FLOW, data: $data)")
+
+ val droidGuardToken = DroidGuard.getClient(context).getResults(DROIDGUARD_FLOW, data, droidGuardResultsRequest).await()
+
+ val droidGuardTokenRaw = Base64.decode(droidGuardToken, Base64.NO_PADDING or Base64.NO_WRAP or Base64.URL_SAFE).toByteString()
+
+ // TODO change how errors work in microg droidguard?
+ if (droidGuardTokenRaw.utf8().startsWith("ERROR :")) {
+ throw IntegrityException(IntegrityErrorCode.INTERNAL_ERROR, "DroidGuard failed")
+ }
+
+ val integrityRequest = IntegrityRequest(
+ details = details,
+ flowName = DROIDGUARD_FLOW,
+ droidGuardTokenRaw = droidGuardTokenRaw,
+ playCoreVersion = playCoreVersion,
+ playProtectDetails = PlayProtectDetails(PlayProtectState.PLAY_PROTECT_STATE_NO_PROBLEMS),
+ )
+
+ Log.d(TAG, "Calling Integrity API (integrityRequest: $integrityRequest)")
+ val response = httpClient.post("https://play-fe.googleapis.com/fdfe/integrity") {
+ setBody(integrityRequest.encode())
+ headers {
+ Log.d(TAG, "userAgent: $FINSKY_USER_AGENT")
+ userAgent(FINSKY_USER_AGENT)
+
+ ContentType("application", "x-protobuf").let {
+ contentType(it)
+ accept(it)
+ }
+
+ // TODO this should be enough because integrity doesn't require auth, but maybe should we do the whole X-PS-RH dance anyway?
+ append("X-DFE-Device-Id", "1")
+ }
+ }
+
+ val responseWrapper = ResponseWrapper.ADAPTER.decode(response.body())
+ Log.d(TAG, "Integrity API response: $responseWrapper")
+
+ val integrityResponse = responseWrapper.payload?.integrityResponse
+ if (integrityResponse?.token == null) {
+ throw IntegrityException(
+ when (response.status.value) {
+ 429 -> IntegrityErrorCode.TOO_MANY_REQUESTS
+ 460 -> IntegrityErrorCode.CLIENT_TRANSIENT_ERROR
+ else -> IntegrityErrorCode.NETWORK_ERROR
+ }, "IntegrityResponse didn't have a token"
+ )
+ }
+
+ callback.onRequestIntegrityToken(integrityResponse.token)
+ } catch (e: IntegrityException) {
+ Log.e(TAG, "requestIntegrityToken failed", e)
+ callback.onRequestIntegrityToken(e.errorCode)
+ }
+ }
+ }
+
+ class IntegrityException(@IntegrityErrorCode val errorCode: Int, message: String? = null) : Exception(message)
+}
+
+private fun Bundle.getLongOrNull(key: String): Long? {
+ return if (containsKey(key)) getLong(key) else null
+}
diff --git a/vending-app/src/main/java/com/google/android/finsky/integrityservice/Utils.kt b/vending-app/src/main/java/com/google/android/finsky/integrityservice/Utils.kt
new file mode 100644
index 0000000000..88665770b5
--- /dev/null
+++ b/vending-app/src/main/java/com/google/android/finsky/integrityservice/Utils.kt
@@ -0,0 +1,22 @@
+/*
+ * SPDX-FileCopyrightText: 2023 microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.google.android.finsky.integrityservice
+
+import android.os.Bundle
+import com.google.android.play.core.integrity.model.IntegrityErrorCode
+import com.google.android.play.core.integrity.protocol.IIntegrityServiceCallback
+
+fun IIntegrityServiceCallback.onRequestIntegrityToken(@IntegrityErrorCode error: Int = IntegrityErrorCode.NO_ERROR) {
+ onRequestIntegrityToken(Bundle().apply {
+ putInt("error", error)
+ })
+}
+
+fun IIntegrityServiceCallback.onRequestIntegrityToken(token: String) {
+ onRequestIntegrityToken(Bundle().apply {
+ putString("token", token)
+ })
+}
diff --git a/vending-app/src/main/java/com/google/android/play/core/integrity/model/IntegrityErrorCode.java b/vending-app/src/main/java/com/google/android/play/core/integrity/model/IntegrityErrorCode.java
new file mode 100644
index 0000000000..548282932d
--- /dev/null
+++ b/vending-app/src/main/java/com/google/android/play/core/integrity/model/IntegrityErrorCode.java
@@ -0,0 +1,149 @@
+package com.google.android.play.core.integrity.model;
+
+import androidx.annotation.IntDef;
+
+import org.microg.gms.common.PublicApi;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+@Retention(RetentionPolicy.SOURCE)
+@IntDef({
+ IntegrityErrorCode.NO_ERROR,
+ IntegrityErrorCode.API_NOT_AVAILABLE,
+ IntegrityErrorCode.PLAY_STORE_NOT_FOUND,
+ IntegrityErrorCode.NETWORK_ERROR,
+ IntegrityErrorCode.PLAY_STORE_ACCOUNT_NOT_FOUND,
+ IntegrityErrorCode.APP_NOT_INSTALLED,
+ IntegrityErrorCode.PLAY_SERVICES_NOT_FOUND,
+ IntegrityErrorCode.APP_UID_MISMATCH,
+ IntegrityErrorCode.TOO_MANY_REQUESTS,
+ IntegrityErrorCode.CANNOT_BIND_TO_SERVICE,
+ IntegrityErrorCode.NONCE_TOO_SHORT,
+ IntegrityErrorCode.NONCE_TOO_LONG,
+ IntegrityErrorCode.GOOGLE_SERVER_UNAVAILABLE,
+ IntegrityErrorCode.NONCE_IS_NOT_BASE64,
+ IntegrityErrorCode.PLAY_STORE_VERSION_OUTDATED,
+ IntegrityErrorCode.PLAY_SERVICES_VERSION_OUTDATED,
+ IntegrityErrorCode.CLOUD_PROJECT_NUMBER_IS_INVALID,
+ IntegrityErrorCode.CLIENT_TRANSIENT_ERROR,
+ IntegrityErrorCode.INTERNAL_ERROR,
+})
+@PublicApi
+public @interface IntegrityErrorCode {
+ /**
+ * 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.
+ *
+ */
+ int API_NOT_AVAILABLE = -1;
+
+ /**
+ * The calling app is not installed.
+ * Something is wrong (possibly an attack). Non-actionable.
+ */
+ int 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.
+ */
+ int 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.
+ */
+ int CANNOT_BIND_TO_SERVICE = -9;
+
+ /**
+ * There was a transient error in the client 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.
+ */
+ int CLIENT_TRANSIENT_ERROR = -17;
+
+ /**
+ * 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.
+ */
+ int CLOUD_PROJECT_NUMBER_IS_INVALID = -16;
+
+ /**
+ * Unknown internal Google server error.
+ * Retry with an exponential backoff. Consider filing a bug if fails consistently.
+ */
+ int GOOGLE_SERVER_UNAVAILABLE = -12;
+
+ /**
+ * Unknown internal error.
+ * Retry with an exponential backoff. Consider filing a bug if fails consistently.
+ */
+ int INTERNAL_ERROR = -100;
+
+ /**
+ * No available network is found.
+ * Ask the user to check for a connection.
+ */
+ int NETWORK_ERROR = -3;
+
+ /**
+ * Nonce is not encoded as a base64 web-safe no-wrap string.
+ * Retry with correct nonce format.
+ */
+ int 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.
+ */
+ int 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.
+ */
+ int NONCE_TOO_SHORT = -10;
+
+ int NO_ERROR = 0;
+
+ /**
+ * Play Services is not available or version is too old.
+ * Ask the user to Install or Update Play Services.
+ */
+ int PLAY_SERVICES_NOT_FOUND = -6;
+
+ /**
+ * Play Services needs to be updated.
+ * Ask the user to update Google Play Services.
+ */
+ int PLAY_SERVICES_VERSION_OUTDATED = -15;
+
+ /**
+ * No Play Store account is found on device. 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.
+ */
+ int PLAY_STORE_ACCOUNT_NOT_FOUND = -4;
+
+ /**
+ * No Play Store app is found on device or not official version is installed.
+ * Ask the user to install an official and recent version of Play Store.
+ */
+ int PLAY_STORE_NOT_FOUND = -2;
+
+ /**
+ * The Play Store needs to be updated.
+ * Ask the user to update the Google Play Store.
+ */
+ int 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.
+ */
+ int TOO_MANY_REQUESTS = -8;
+}
diff --git a/vending-app/src/main/java/com/google/android/play/core/integrity/model/StandardIntegrityErrorCode.java b/vending-app/src/main/java/com/google/android/play/core/integrity/model/StandardIntegrityErrorCode.java
new file mode 100644
index 0000000000..0d201ff2bd
--- /dev/null
+++ b/vending-app/src/main/java/com/google/android/play/core/integrity/model/StandardIntegrityErrorCode.java
@@ -0,0 +1,120 @@
+package com.google.android.play.core.integrity.model;
+
+import androidx.annotation.IntDef;
+
+import org.microg.gms.common.PublicApi;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+@Retention(RetentionPolicy.SOURCE)
+@IntDef({
+ StandardIntegrityErrorCode.NO_ERROR,
+ StandardIntegrityErrorCode.API_NOT_AVAILABLE,
+ StandardIntegrityErrorCode.PLAY_STORE_NOT_FOUND,
+ StandardIntegrityErrorCode.NETWORK_ERROR,
+ StandardIntegrityErrorCode.APP_NOT_INSTALLED,
+ StandardIntegrityErrorCode.PLAY_SERVICES_NOT_FOUND,
+ StandardIntegrityErrorCode.APP_UID_MISMATCH,
+ StandardIntegrityErrorCode.TOO_MANY_REQUESTS,
+ StandardIntegrityErrorCode.CANNOT_BIND_TO_SERVICE,
+ StandardIntegrityErrorCode.GOOGLE_SERVER_UNAVAILABLE,
+ StandardIntegrityErrorCode.PLAY_STORE_VERSION_OUTDATED,
+ StandardIntegrityErrorCode.PLAY_SERVICES_VERSION_OUTDATED,
+ StandardIntegrityErrorCode.CLOUD_PROJECT_NUMBER_IS_INVALID,
+ StandardIntegrityErrorCode.REQUEST_HASH_TOO_LONG,
+ StandardIntegrityErrorCode.INTERNAL_ERROR,
+})
+@PublicApi
+public @interface StandardIntegrityErrorCode {
+ /**
+ * Standard Integrity API is not available.
+ * Standard Integrity API is not yet available due to the Play Store version being too old.
+ *
+ * Recommended actions:
+ * - Ask the user to update Play Store.
+ *
+ */
+ int API_NOT_AVAILABLE = -1;
+
+ /**
+ * The calling app is not installed.
+ * Something is wrong (possibly an attack). Non-actionable.
+ */
+ int 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.
+ */
+ int 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 or device memory is overloaded.
+ * Ask the user to update Play Store.
+ * Retry with an exponential backoff.
+ */
+ int CANNOT_BIND_TO_SERVICE = -9;
+
+ /**
+ * 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.
+ */
+ int CLOUD_PROJECT_NUMBER_IS_INVALID = -16;
+
+ /**
+ * Unknown internal Google server error.
+ * Retry with an exponential backoff. Consider filing a bug if fails consistently.
+ */
+ int GOOGLE_SERVER_UNAVAILABLE = -12;
+
+ /**
+ * Unknown internal error.
+ * Retry with an exponential backoff. Consider filing a bug if fails consistently.
+ */
+ int INTERNAL_ERROR = -100;
+
+ /**
+ * No available network is found.
+ * Ask the user to check for a connection.
+ */
+ int NETWORK_ERROR = -3;
+
+ int NO_ERROR = 0;
+
+ /**
+ * Play Services is not available or version is too old.
+ * Ask the user to Install or Update Play Services.
+ */
+ int PLAY_SERVICES_NOT_FOUND = -6;
+
+ /**
+ * Play Services needs to be updated.
+ * Ask the user to update Google Play Services.
+ */
+ int PLAY_SERVICES_VERSION_OUTDATED = -15;
+
+ /**
+ * No Play Store app is found on device or not official version is installed.
+ * Ask the user to install an official and recent version of Play Store.
+ */
+ int PLAY_STORE_NOT_FOUND = -2;
+
+ /**
+ * The Play Store needs to be updated.
+ * Ask the user to update the Google Play Store.
+ */
+ int PLAY_STORE_VERSION_OUTDATED = -14;
+
+ /**
+ * The provided request hash is too long. The request hash length must be less than 500 characters.
+ * Retry with a shorter request hash.
+ */
+ int REQUEST_HASH_TOO_LONG = -17;
+
+ /**
+ * The calling app is making too many requests to the API and hence is throttled.
+ * Retry with an exponential backoff.
+ */
+ int TOO_MANY_REQUESTS = -8;
+}
diff --git a/vending-app/src/main/java/org/microg/vending/FinskyConstants.kt b/vending-app/src/main/java/org/microg/vending/FinskyConstants.kt
new file mode 100644
index 0000000000..c060e2a0db
--- /dev/null
+++ b/vending-app/src/main/java/org/microg/vending/FinskyConstants.kt
@@ -0,0 +1,41 @@
+/*
+ * SPDX-FileCopyrightText: 2023 microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.microg.vending
+
+import android.os.Build
+
+// TODO move to gradle?
+const val FINSKY_VERSION_NAME = "37.9.18-29 [0] [PR] 571399392"
+const val FINSKY_VERSION_CODE = 83791820
+
+private val supportedAbis: Array =
+ if (Build.VERSION.SDK_INT >= 21) {
+ Build.SUPPORTED_ABIS
+ } else {
+ @Suppress("DEPRECATION")
+ if (Build.CPU_ABI2 == null || Build.CPU_ABI2 == "unknown") {
+ arrayOf(Build.CPU_ABI)
+ } else {
+ arrayOf(Build.CPU_ABI, Build.CPU_ABI2)
+ }
+ }
+
+// TODO use device profile after https://github.com/microg/GmsCore/pull/2071 is merged
+private val userAgentProperties = mapOf(
+ "api" to "3",
+ "versionCode" to FINSKY_VERSION_CODE.toString(),
+ "sdk" to Build.VERSION.SDK_INT.toString(),
+ "device" to Build.DEVICE.toString(),
+ "hardware" to Build.HARDWARE.toString(),
+ "product" to Build.PRODUCT.toString(),
+ "platformVersionRelease" to Build.VERSION.RELEASE.toString(),
+ "model" to Build.MODEL.toString(),
+ "buildId" to Build.ID.toString(),
+ "isWideScreen" to "0",
+ "supportedAbis" to supportedAbis.joinToString(";"),
+)
+
+val FINSKY_USER_AGENT = "Android-Finsky/${FINSKY_VERSION_NAME} (${userAgentProperties.entries.joinToString(",")})"
diff --git a/vending-app/src/main/java/org/microg/vending/utils/ByteArrayUtils.kt b/vending-app/src/main/java/org/microg/vending/utils/ByteArrayUtils.kt
new file mode 100644
index 0000000000..9bd8d29a54
--- /dev/null
+++ b/vending-app/src/main/java/org/microg/vending/utils/ByteArrayUtils.kt
@@ -0,0 +1,33 @@
+/*
+ * SPDX-FileCopyrightText: 2023 microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.microg.vending.utils
+
+import android.util.Base64
+import java.io.ByteArrayOutputStream
+import java.security.MessageDigest
+import java.util.zip.GZIPOutputStream
+
+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 ByteArray.gzip(): ByteArray {
+ ByteArrayOutputStream().use { byteOutput ->
+ GZIPOutputStream(byteOutput).use { gzipOutput ->
+ gzipOutput.write(this)
+ gzipOutput.finish()
+ return byteOutput.toByteArray()
+ }
+ }
+}
diff --git a/vending-app/src/main/java/org/microg/vending/utils/CompatUtils.kt b/vending-app/src/main/java/org/microg/vending/utils/CompatUtils.kt
new file mode 100644
index 0000000000..2a22ceaac7
--- /dev/null
+++ b/vending-app/src/main/java/org/microg/vending/utils/CompatUtils.kt
@@ -0,0 +1,38 @@
+/*
+ * SPDX-FileCopyrightText: 2023 microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.microg.vending.utils
+
+import android.content.pm.PackageInfo
+import android.content.pm.PackageManager
+import android.content.pm.Signature
+import android.os.Build
+
+fun PackageManager.getPackageInfoCompat(packageName: String, flags: Int = 0): PackageInfo {
+ return if (Build.VERSION.SDK_INT >= 33) {
+ getPackageInfo(packageName, PackageManager.PackageInfoFlags.of(flags.toLong()))
+ } else {
+ @Suppress("DEPRECATION") 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
+ }
+ }
diff --git a/vending-app/src/main/proto/ResponseWrapper.proto b/vending-app/src/main/proto/ResponseWrapper.proto
new file mode 100644
index 0000000000..a36345bf52
--- /dev/null
+++ b/vending-app/src/main/proto/ResponseWrapper.proto
@@ -0,0 +1,21 @@
+syntax = "proto2";
+
+import "integrity/IntegrityResponse.proto";
+
+option java_package = "com.google.android.finsky";
+option java_multiple_files = true;
+
+message ResponseWrapper {
+ optional Payload payload = 1;
+ optional ServerCommands commands = 2;
+}
+
+message ServerCommands {
+ optional bool clearCache = 1;
+ optional string displayErrorMessage = 2;
+ optional string logErrorStacktrace = 3;
+}
+
+message Payload {
+ optional IntegrityResponse integrityResponse = 186;
+}
diff --git a/vending-app/src/main/proto/integrity/IntegrityRequest.proto b/vending-app/src/main/proto/integrity/IntegrityRequest.proto
new file mode 100644
index 0000000000..f2fcca6f16
--- /dev/null
+++ b/vending-app/src/main/proto/integrity/IntegrityRequest.proto
@@ -0,0 +1,53 @@
+syntax = "proto2";
+
+import "google/protobuf/timestamp.proto";
+
+option java_package = "com.google.android.finsky.integrityservice";
+option java_multiple_files = true;
+
+message IntegrityRequest {
+ message Details {
+ message PackageNameWrapper {
+ optional string value = 1;
+ }
+
+ message VersionCodeWrapper {
+ optional int32 value = 1;
+ }
+
+ optional PackageNameWrapper packageName = 1;
+ optional VersionCodeWrapper versionCode = 2;
+ optional string nonce = 3;
+ repeated string certificateSha256Digests = 4;
+ optional google.protobuf.Timestamp timestampAtRequest = 5;
+ optional int64 cloudProjectNumber = 6;
+ }
+
+ optional Details details = 1;
+ optional string flowName = 3;
+ oneof droidGuardToken {
+ string droidGuardTokenBase64 = 2;
+ bytes droidGuardTokenRaw = 5;
+ }
+ optional PlayCoreVersion playCoreVersion = 6;
+ optional PlayProtectDetails playProtectDetails = 7;
+}
+
+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;
+}
diff --git a/vending-app/src/main/proto/integrity/IntegrityResponse.proto b/vending-app/src/main/proto/integrity/IntegrityResponse.proto
new file mode 100644
index 0000000000..5af2c6de87
--- /dev/null
+++ b/vending-app/src/main/proto/integrity/IntegrityResponse.proto
@@ -0,0 +1,8 @@
+syntax = "proto2";
+
+option java_package = "com.google.android.finsky.integrityservice";
+option java_multiple_files = true;
+
+message IntegrityResponse {
+ optional string token = 1;
+}