Skip to content

Commit

Permalink
Added Vending IntegrityService
Browse files Browse the repository at this point in the history
  • Loading branch information
DaVinci9196 committed Oct 28, 2024
1 parent 511afe8 commit 1b04f4b
Show file tree
Hide file tree
Showing 27 changed files with 1,833 additions and 28 deletions.
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import android.content.Context;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;

import org.microg.gms.checkin.LastCheckinInfo;
Expand Down Expand Up @@ -152,8 +153,13 @@ private static Bundle handleResponse(GcmDatabase database, RegisterRequest reque
}
} else {
if (!request.app.equals(response.deleted) && !request.app.equals(response.token) && !request.sender.equals(response.token)) {
database.noteAppRegistrationError(request.app, response.responseText);
resultBundle.putString(EXTRA_ERROR, attachRequestId(ERROR_SERVICE_NOT_AVAILABLE, requestId));
if (TextUtils.isEmpty(response.token)) {
database.noteAppUnregistered(request.app, request.appSignature);
resultBundle.putString(EXTRA_UNREGISTERED, attachRequestId(request.app, requestId));
} else {
database.noteAppRegistrationError(request.app, response.responseText);
resultBundle.putString(EXTRA_ERROR, attachRequestId(ERROR_SERVICE_NOT_AVAILABLE, requestId));
}
} else {
database.noteAppUnregistered(request.app, request.appSignature);
resultBundle.putString(EXTRA_UNREGISTERED, attachRequestId(request.app, requestId));
Expand Down
5 changes: 5 additions & 0 deletions play-services-droidguard/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

apply plugin: 'com.android.library'
apply plugin: 'maven-publish'
apply plugin: 'kotlin-android'
apply plugin: 'signing'

android {
Expand All @@ -31,6 +32,10 @@ android {
sourceCompatibility = 1.8
targetCompatibility = 1.8
}

kotlinOptions {
jvmTarget = 1.8
}
}

apply from: '../gradle/publish-android.gradle'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -29,6 +30,7 @@ public class DroidGuardApiClient extends GmsClient<IDroidGuardService> {
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);
Expand All @@ -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) {
Expand All @@ -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);
}
}
}
Expand All @@ -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");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* SPDX-FileCopyrightText: 2022 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/

package org.microg.gms.droidguard

import android.content.Context
import android.os.Parcelable

class HandleProxy(private val handle: Any, val vmKey: String) {
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 it
}, vmKey
)

fun init(): Boolean {
try {
return handle.javaClass.getDeclaredMethod("init").invoke(handle) as Boolean
} catch (e: Exception) {
throw e
}
}

fun close() {
try {
handle.javaClass.getDeclaredMethod("close").invoke(handle)
} catch (e: Exception) {
throw e
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
* SPDX-FileCopyrightText: 2022 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 dalvik.system.DexClassLoader
import java.io.File
import java.io.IOException
import java.util.UUID

class HandleProxyFactory(private val context: Context) {
private 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
}

private fun copyTheApk(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("unknown except")
}
}
}

fun createHandle(vmKey: String, pfd: ParcelFileDescriptor, extras: Bundle): HandleProxy {
copyTheApk(pfd, vmKey)
val clazz = loadClass(vmKey)
return HandleProxy(clazz, context, vmKey, extras)
}

private fun loadClass(vmKey: String): Class<*> {
val clazz = classMap[vmKey]
if (clazz != null) {
updateCacheTimestamp(vmKey)
return clazz
} else {
if (!isValidCache(vmKey)) {
throw RuntimeException("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
return clazz
}
}

companion object {
const val CLASS_NAME = "com.google.ccc.abuse.droidguard.DroidGuard"
const val CACHE_FOLDER_NAME = "cache_dg"
val classMap = hashMapOf<String, Class<*>>()
}
}
4 changes: 4 additions & 0 deletions vending-app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -112,13 +112,17 @@ dependencies {

//droidguard
implementation project(':play-services-droidguard')
implementation project(':play-services-tasks-ktx')

//androidx
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion"
implementation "androidx.core:core-ktx:$coreVersion"
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 {
Expand Down
16 changes: 16 additions & 0 deletions vending-app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -175,5 +175,21 @@
</intent-filter>
</service>

<service
android:name="com.google.android.finsky.integrityservice.IntegrityService"
android:exported="true">
<intent-filter>
<action android:name="com.google.android.play.core.integrityservice.BIND_INTEGRITY_SERVICE" />
</intent-filter>
</service>

<service
android:name="com.google.android.finsky.expressintegrityservice.ExpressIntegrityService"
android:exported="true">
<intent-filter>
<action android:name="com.google.android.play.core.expressintegrityservice.BIND_EXPRESS_INTEGRITY_SERVICE"/>
</intent-filter>
</service>

</application>
</manifest>
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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) {
Expand All @@ -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
Expand Down
Loading

0 comments on commit 1b04f4b

Please sign in to comment.