Skip to content

Commit

Permalink
Start working on Play Integrity
Browse files Browse the repository at this point in the history
  • Loading branch information
js6pak committed Oct 26, 2023
1 parent 44b9173 commit 45a3732
Show file tree
Hide file tree
Showing 14 changed files with 741 additions and 0 deletions.
26 changes: 26 additions & 0 deletions vending-app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@
*/

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'com.squareup.wire'

android {
namespace "com.android.vending"
compileSdkVersion androidCompileSdk
buildToolsVersion "$androidBuildVersionTools"

defaultConfig {
multiDexEnabled = true
versionName vendingAppVersionName
versionCode vendingAppVersionCode
minSdkVersion androidMinSdk
Expand All @@ -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()) {
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 @@ -9,6 +9,8 @@
android:name="com.android.vending.CHECK_LICENSE"
android:protectionLevel="normal" />

<uses-permission android:name="android.permission.INTERNET" />

<application
android:forceQueryable="true"
android:icon="@mipmap/ic_app"
Expand Down Expand Up @@ -36,6 +38,20 @@
</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>
-->

<service
android:name="com.google.android.finsky.externalreferrer.GetInstallReferrerService"
android:exported="true">
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
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 onRequestIntegrityToken(in Bundle request) = 1;
}
Original file line number Diff line number Diff line change
@@ -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<InputStream>())
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
}
Original file line number Diff line number Diff line change
@@ -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)
})
}
Loading

0 comments on commit 45a3732

Please sign in to comment.