Loading vending-app/src/main/kotlin/com/google/android/finsky/IntegrityExtensions.kt +124 −7 Original line number Diff line number Diff line Loading @@ -8,9 +8,11 @@ package com.google.android.finsky import android.accounts.AccountManager import android.accounts.AccountManagerFuture import android.content.Context import android.content.pm.ApplicationInfo import android.content.pm.PackageInfo import android.content.pm.PackageManager import android.content.pm.Signature import android.net.ConnectivityManager import android.os.Bundle import android.security.keystore.KeyGenParameterSpec import android.security.keystore.KeyProperties Loading @@ -21,6 +23,7 @@ import com.android.vending.buildRequestHeaders import com.android.vending.makeTimestamp import com.google.android.finsky.expressintegrityservice.ExpressIntegritySession import com.google.android.finsky.expressintegrityservice.IntermediateIntegrityResponseData import com.google.android.finsky.expressintegrityservice.PackageInformation import com.google.android.gms.droidguard.DroidGuard import com.google.android.gms.droidguard.internal.DroidGuardResultsRequest import com.google.android.gms.tasks.await Loading @@ -35,7 +38,9 @@ import kotlinx.coroutines.withContext import okio.ByteString import okio.ByteString.Companion.encode import okio.ByteString.Companion.toByteString import org.microg.gms.common.Constants import org.microg.gms.profile.Build import org.microg.gms.profile.ProfileManager import org.microg.vending.billing.DEFAULT_ACCOUNT_TYPE import org.microg.vending.billing.GServices import org.microg.vending.billing.core.HttpClient Loading @@ -49,6 +54,7 @@ import java.security.KeyPair import java.security.KeyPairGenerator import java.security.KeyStore import java.security.MessageDigest import java.security.NoSuchAlgorithmException import java.security.ProviderException import java.security.spec.ECGenParameterSpec import kotlin.coroutines.resume Loading Loading @@ -111,6 +117,15 @@ private fun Context.getProtoFile(): File { return file } fun Context.isNetworkConnected(): Boolean { return try { val connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager connectivityManager.activeNetworkInfo?.isConnected == true } catch (_: RuntimeException) { false } } private fun getExpressFilePB(context: Context): ExpressFilePB { return runCatching { FileInputStream(context.getProtoFile()).use { input -> ExpressFilePB.ADAPTER.decode(input) } } .onFailure { Log.w(TAG, "Failed to read express cache ", it) } Loading Loading @@ -155,6 +170,15 @@ fun ByteArray.encodeBase64(noPadding: Boolean, noWrap: Boolean = true, urlSafe: return Base64.encodeToString(this, flags) } fun ByteArray.md5(): ByteArray? { return try { val md5 = MessageDigest.getInstance("MD5") md5.digest(this) } catch (e: NoSuchAlgorithmException) { null } } fun ByteArray.sha256(): ByteArray { return MessageDigest.getInstance("SHA-256").digest(this) } Loading @@ -163,6 +187,11 @@ fun Bundle.getPlayCoreVersion() = PlayCoreVersion( major = getInt(KEY_VERSION_MAJOR, 0), minor = getInt(KEY_VERSION_MINOR, 0), patch = getInt(KEY_VERSION_PATCH, 0) ) fun List<AdviceType>?.ensureContainsLockBootloader(): List<AdviceType> { if (isNullOrEmpty()) return listOf(AdviceType.LOCK_BOOTLOADER) return if (contains(AdviceType.LOCK_BOOTLOADER)) this else listOf(AdviceType.LOCK_BOOTLOADER) + this } fun readAes128GcmBuilderFromClientKey(clientKey: ClientKey?): Aead? { if (clientKey == null) { return null Loading Loading @@ -249,7 +278,6 @@ fun fetchCertificateChain(context: Context, attestationChallenge: ByteArray?): L } suspend fun updateLocalExpressFilePB(context: Context, intermediateIntegrityResponseData: IntermediateIntegrityResponseData) = withContext(Dispatchers.IO) { Log.d(TAG, "Writing AAR to express cache") val intermediateIntegrity = intermediateIntegrityResponseData.intermediateIntegrity val expressFilePB = getExpressFilePB(context) Loading @@ -258,17 +286,13 @@ suspend fun updateLocalExpressFilePB(context: Context, intermediateIntegrityResp packageName = intermediateIntegrity.packageName cloudProjectNumber = intermediateIntegrity.cloudProjectNumber callerKey = intermediateIntegrity.callerKey webViewRequestMode = intermediateIntegrity.webViewRequestMode.let { when (it) { in 0..2 -> it + 1 else -> 1 } } - 1 webViewRequestMode = intermediateIntegrity.webViewRequestMode.takeIf { it in 0..2 } ?: 0 deviceIntegrityWrapper = DeviceIntegrityWrapper.Builder().apply { creationTime = intermediateIntegrity.callerKey.generated serverGenerated = intermediateIntegrity.serverGenerated deviceIntegrityToken = intermediateIntegrity.intermediateToken }.build() intermediateIntegrity.integrityAdvice?.let { advice = it } }.build() val requestList = expressFilePB.integrityRequestWrapper.toMutableList() Loading Loading @@ -508,3 +532,96 @@ suspend fun requestIntermediateIntegrity( adapter = IntermediateIntegrityResponseWrapperExtend.ADAPTER ) } fun buildClientKeyExtend( context: Context, session: ExpressIntegritySession, packageInformation: PackageInformation, clientKey: ClientKey ): ClientKeyExtend { return ClientKeyExtend.Builder().apply { cloudProjectNumber = session.cloudProjectNumber keySetHandle = clientKey.keySetHandle if (session.webViewRequestMode == 2) { this.optPackageName = KEY_OPT_PACKAGE this.versionCode = 0 } else { this.optPackageName = session.packageName this.versionCode = packageInformation.versionCode this.certificateSha256Hashes = packageInformation.certificateSha256Hashes } this.deviceSerialHash = ProfileManager.getSerial(context).toByteArray().sha256().toByteString() }.build() } fun buildInstallSourceMetaData( context: Context, packageName: String ): InstallSourceMetaData { fun resolveInstallerType(name: String?): InstallerType = when { name.isNullOrEmpty() -> InstallerType.UNSPECIFIED_INSTALLER name == Constants.VENDING_PACKAGE_NAME -> InstallerType.PHONESKY_INSTALLER else -> InstallerType.OTHER_INSTALLER } fun resolvePackageSourceType(type: Int): PackageSourceType = when (type) { 1 -> PackageSourceType.PACKAGE_SOURCE_OTHER 2 -> PackageSourceType.PACKAGE_SOURCE_STORE 3 -> PackageSourceType.PACKAGE_SOURCE_LOCAL_FILE 4 -> PackageSourceType.PACKAGE_SOURCE_DOWNLOADED_FILE else -> PackageSourceType.PACKAGE_SOURCE_UNSPECIFIED } val builder = InstallSourceMetaData.Builder().apply { installingPackageName = InstallerType.UNSPECIFIED_INSTALLER initiatingPackageName = InstallerType.UNSPECIFIED_INSTALLER originatingPackageName = InstallerType.UNSPECIFIED_INSTALLER updateOwnerPackageName = InstallerType.UNSPECIFIED_INSTALLER packageSourceType = PackageSourceType.PACKAGE_SOURCE_UNSPECIFIED } val applicationInfo = runCatching { context.packageManager.getApplicationInfo(packageName, 0) }.getOrNull() if (Build.VERSION.SDK_INT >= 30) { runCatching { val info = context.packageManager.getInstallSourceInfo(packageName) builder.apply { initiatingPackageName = resolveInstallerType(info.initiatingPackageName) installingPackageName = resolveInstallerType(info.installingPackageName) originatingPackageName = resolveInstallerType(info.originatingPackageName) if (Build.VERSION.SDK_INT >= 34) { updateOwnerPackageName = resolveInstallerType(info.updateOwnerPackageName) } if (Build.VERSION.SDK_INT >= 33) { packageSourceType = resolvePackageSourceType(info.packageSource) } } } } else { builder.installingPackageName = runCatching { resolveInstallerType(context.packageManager.getInstallerPackageName(packageName)) }.getOrElse { InstallerType.UNSPECIFIED_INSTALLER } } builder.appFlags = applicationInfo?.let { info -> buildList { if (info.flags and ApplicationInfo.FLAG_SYSTEM != 0) add(SystemAppFlag.FLAG_SYSTEM) if (info.flags and ApplicationInfo.FLAG_UPDATED_SYSTEM_APP != 0) { add(SystemAppFlag.FLAG_UPDATED_SYSTEM_APP) } }.ifEmpty { listOf(SystemAppFlag.SYSTEM_APP_INFO_UNSPECIFIED) } } ?: listOf(SystemAppFlag.SYSTEM_APP_INFO_UNSPECIFIED) return builder.build() } fun validateIntermediateIntegrityResponse(intermediateIntegrityResponse: IntermediateIntegrityResponseData) { val intermediateIntegrity = intermediateIntegrityResponse.intermediateIntegrity requireNotNull(intermediateIntegrity.intermediateToken) { "Null intermediateToken" } requireNotNull(intermediateIntegrity.serverGenerated) { "Null serverGenerated" } } vending-app/src/main/kotlin/com/google/android/finsky/expressintegrityservice/ExpressIntegrityService.kt +62 −41 Original line number Diff line number Diff line Loading @@ -26,8 +26,10 @@ 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.IntegrityAdvice import com.google.android.finsky.INTERMEDIATE_INTEGRITY_HARD_EXPIRATION import com.google.android.finsky.IntermediateIntegrityRequest import com.google.android.finsky.IntermediateIntegrityResponse import com.google.android.finsky.IntermediateIntegritySession import com.google.android.finsky.KEY_CLOUD_PROJECT import com.google.android.finsky.KEY_NONCE Loading @@ -43,13 +45,20 @@ 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.TestErrorType import com.google.android.finsky.buildClientKeyExtend import com.google.android.finsky.buildInstallSourceMetaData import com.google.android.finsky.getPlayCoreVersion import com.google.android.finsky.encodeBase64 import com.google.android.finsky.ensureContainsLockBootloader import com.google.android.finsky.getAuthToken import com.google.android.finsky.getExpirationTime import com.google.android.finsky.getIntegrityRequestWrapper import com.google.android.finsky.getPackageInfoCompat import com.google.android.finsky.isNetworkConnected import com.google.android.finsky.md5 import com.google.android.finsky.model.IntegrityErrorCode import com.google.android.finsky.model.StandardIntegrityException import com.google.android.finsky.readAes128GcmBuilderFromClientKey import com.google.android.finsky.requestIntermediateIntegrity import com.google.android.finsky.sha256 Loading @@ -58,6 +67,7 @@ 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.finsky.validateIntermediateIntegrityResponse 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 Loading Loading @@ -91,11 +101,9 @@ private class ExpressIntegrityServiceImpl(private val context: Context, override 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") if (!context.isNetworkConnected()) { throw StandardIntegrityException(IntegrityErrorCode.NETWORK_ERROR, "No network is available") } Log.d(TAG, "warmUpIntegrityToken authToken: $authToken") val expressIntegritySession = ExpressIntegritySession( packageName = bundle.getString(KEY_PACKAGE_NAME) ?: "", Loading @@ -106,9 +114,18 @@ private class ExpressIntegrityServiceImpl(private val context: Context, override null, webViewRequestMode = bundle.getInt(KEY_REQUEST_MODE, 0) ) Log.d(TAG, "warmUpIntegrityToken session:$expressIntegritySession}") updateExpressSessionTime(context, expressIntegritySession, refreshWarmUpMethodTime = true, refreshRequestMethodTime = false) val clientKey = updateExpressClientKey(context) 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 expressFilePB = updateExpressAuthTokenWrapper(context, expressIntegritySession, authToken, clientKey) val tokenWrapper = expressFilePB.tokenWrapper ?: AuthTokenWrapper() Loading @@ -125,14 +142,14 @@ private class ExpressIntegrityServiceImpl(private val context: Context, override val deviceIntegrity = deviceIntegrityAndExpiredKey.deviceIntegrity if (deviceIntegrity.deviceIntegrityToken?.size == 0 || deviceIntegrity.clientKey?.keySetHandle?.size == 0) { throw RuntimeException("DroidGuard token is empty.") throw StandardIntegrityException("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.") throw StandardIntegrityException("Null deviceKeyMd5.") } val deviceIntegrityResponse = DeviceIntegrityResponse( Loading @@ -147,35 +164,16 @@ private class ExpressIntegrityServiceImpl(private val context: Context, override } val packageInformation = PackageInformation(certificateSha256Hashes, packageInfo.versionCode) val clientKeyExtend = ClientKeyExtend.Builder().apply { cloudProjectNumber = expressIntegritySession.cloudProjectNumber 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.getPlayCoreVersion() Log.d(TAG, "warmUpIntegrityToken sessionId:$sessionId") val clientKeyExtend = buildClientKeyExtend(context, expressIntegritySession, packageInformation, clientKey) 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)) playCoreVersion(bundle.getPlayCoreVersion()) sessionId(expressIntegritySession.sessionId) installSourceMetaData(buildInstallSourceMetaData(context, expressIntegritySession.packageName)) cloudProjectNumber(expressIntegritySession.cloudProjectNumber) playProtectDetails(PlayProtectDetails(PlayProtectState.PLAY_PROTECT_STATE_NONE)) if (expressIntegritySession.webViewRequestMode != 0) { requestMode(RequestMode.Builder().mode(expressIntegritySession.webViewRequestMode.takeIf { it in 0..2 } ?: 0).build()) Loading @@ -185,9 +183,19 @@ private class ExpressIntegrityServiceImpl(private val context: Context, override Log.d(TAG, "intermediateIntegrityRequest: $intermediateIntegrityRequest") val intermediateIntegrityResponse = requestIntermediateIntegrity(context, authToken, intermediateIntegrityRequest).intermediateIntegrityResponseWrapper?.intermediateIntegrityResponse ?: throw RuntimeException("intermediateIntegrityResponse is null.") Log.d(TAG, "requestIntermediateIntegrity: $intermediateIntegrityResponse") ?: IntermediateIntegrityResponse() Log.d(TAG, "requestIntermediateIntegrity response: ${intermediateIntegrityResponse.encode().encodeBase64(true)}") val errorCode = intermediateIntegrityResponse.errorInfo?.let { error -> if (error.errorCode == null) { null } else if (error.testErrorType == TestErrorType.REQUEST_EXPRESS) { error.errorCode } else if (error.testErrorType == TestErrorType.WARMUP) { throw StandardIntegrityException(error.errorCode, "Server-specified exception") } else null } val defaultAccountName: String = runCatching { if (expressIntegritySession.webViewRequestMode != 0) { Loading @@ -197,9 +205,13 @@ private class ExpressIntegrityServiceImpl(private val context: Context, override } }.getOrDefault(RESULT_UN_AUTH) val callerKeyMd5 = clientKey.encode().md5() ?: throw StandardIntegrityException("Null callerKeyMd5") val refreshClientKey = clientKey.newBuilder() .generated(makeTimestamp(System.currentTimeMillis())) .build() val fixedAdvice = IntegrityAdvice.Builder() .advices(intermediateIntegrityResponse.integrityAdvice?.advices.ensureContainsLockBootloader()) .build() val intermediateIntegrityResponseData = IntermediateIntegrityResponseData( intermediateIntegrity = IntermediateIntegrity( expressIntegritySession.packageName, Loading @@ -209,21 +221,24 @@ private class ExpressIntegrityServiceImpl(private val context: Context, override intermediateIntegrityResponse.intermediateToken, intermediateIntegrityResponse.serverGenerated, expressIntegritySession.webViewRequestMode, 0 ), callerKeyMd5 = Base64.encodeToString( refreshClientKey.encode(), Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING errorCode, fixedAdvice ), callerKeyMd5 = callerKeyMd5.encodeBase64(noPadding = true), appVersionCode = packageInformation.versionCode, deviceIntegrityResponse = deviceIntegrityResponse, appAccessRiskVerdictEnabled = intermediateIntegrityResponse.appAccessRiskVerdictEnabled ) validateIntermediateIntegrityResponse(intermediateIntegrityResponseData) updateLocalExpressFilePB(context, intermediateIntegrityResponseData) callback?.onWarmResult(bundleOf(KEY_WARM_UP_SID to sessionId)) callback?.onWarmResult(bundleOf(KEY_WARM_UP_SID to expressIntegritySession.sessionId)) }.onFailure { callback?.onWarmResult(bundleOf(KEY_ERROR to IntegrityErrorCode.INTEGRITY_TOKEN_PROVIDER_INVALID)) val exception = it as? StandardIntegrityException ?: StandardIntegrityException(it.message) Log.w(TAG, "warm up has failed: code=${exception.code}, message=${exception.message}", exception) callback?.onWarmResult(bundleOf(KEY_ERROR to exception.code)) } } } Loading @@ -242,6 +257,8 @@ private class ExpressIntegrityServiceImpl(private val context: Context, override webViewRequestMode = bundle.getInt(KEY_REQUEST_MODE, 0) ) Log.d(TAG, "requestExpressIntegrityToken session:$expressIntegritySession}") if (TextUtils.isEmpty(expressIntegritySession.packageName)) { Log.w(TAG, "packageName is empty.") callback?.onRequestResult(bundleOf(KEY_ERROR to IntegrityErrorCode.INTERNAL_ERROR)) Loading Loading @@ -277,6 +294,9 @@ private class ExpressIntegrityServiceImpl(private val context: Context, override return@launchWhenCreated } integrityRequestWrapper.deviceIntegrityWrapper?.errorCode?.let { throw StandardIntegrityException(it, "Server-specified exception") } val expirationTime = integrityRequestWrapper.getExpirationTime() if (expirationTime > INTERMEDIATE_INTEGRITY_HARD_EXPIRATION * 1000) { Loading Loading @@ -309,8 +329,9 @@ private class ExpressIntegrityServiceImpl(private val context: Context, override ) ) }.onFailure { Log.e(TAG, "requesting token has failed for ${bundle.getString(KEY_PACKAGE_NAME)}.") callback?.onRequestResult(bundleOf(KEY_ERROR to IntegrityErrorCode.INTEGRITY_TOKEN_PROVIDER_INVALID)) val exception = it as? StandardIntegrityException ?: StandardIntegrityException(it.message) Log.w(TAG, "requesting token has failed: code=${exception.code}, message=${exception.message}", exception) callback?.onRequestResult(bundleOf(KEY_ERROR to exception.code)) } } } Loading vending-app/src/main/kotlin/com/google/android/finsky/expressintegrityservice/ExpressIntegritySession.kt +9 −1 Original line number Diff line number Diff line Loading @@ -12,4 +12,12 @@ data class ExpressIntegritySession( var originatingWarmUpSessionId: Long, var verdictOptOut: List<Int>?, var webViewRequestMode: Int ) { override fun toString(): String { return "ExpressIntegritySession(packageName='$packageName', cloudProjectNumber=$cloudProjectNumber, sessionId=$sessionId, requestHash=$requestHash, originatingWarmUpSessionId=$originatingWarmUpSessionId, verdictOptOut=${ verdictOptOut?.joinToString( prefix = "[", postfix = "]" ) }, webViewRequestMode=$webViewRequestMode)" } } No newline at end of file vending-app/src/main/kotlin/com/google/android/finsky/expressintegrityservice/IntermediateIntegrity.kt +3 −1 Original line number Diff line number Diff line Loading @@ -6,6 +6,7 @@ package com.google.android.finsky.expressintegrityservice import com.google.android.finsky.ClientKey import com.google.android.finsky.IntegrityAdvice import okio.ByteString import org.microg.vending.proto.Timestamp Loading @@ -17,5 +18,6 @@ data class IntermediateIntegrity( var intermediateToken: ByteString?, var serverGenerated: Timestamp?, var webViewRequestMode: Int, var testErrorCode: Int var testErrorCode: Int?, var integrityAdvice: IntegrityAdvice? ) No newline at end of file vending-app/src/main/kotlin/com/google/android/finsky/model/StandardIntegrityException.kt 0 → 100644 +18 −0 Original line number Diff line number Diff line /** * SPDX-FileCopyrightText: 2025 microG Project Team * SPDX-License-Identifier: Apache-2.0 */ package com.google.android.finsky.model class StandardIntegrityException : Exception { val code: Int constructor(code: Int, message: String) : super(message) { this.code = code } constructor(cause: String?) : super(cause) { this.code = IntegrityErrorCode.INTERNAL_ERROR } } No newline at end of file Loading
vending-app/src/main/kotlin/com/google/android/finsky/IntegrityExtensions.kt +124 −7 Original line number Diff line number Diff line Loading @@ -8,9 +8,11 @@ package com.google.android.finsky import android.accounts.AccountManager import android.accounts.AccountManagerFuture import android.content.Context import android.content.pm.ApplicationInfo import android.content.pm.PackageInfo import android.content.pm.PackageManager import android.content.pm.Signature import android.net.ConnectivityManager import android.os.Bundle import android.security.keystore.KeyGenParameterSpec import android.security.keystore.KeyProperties Loading @@ -21,6 +23,7 @@ import com.android.vending.buildRequestHeaders import com.android.vending.makeTimestamp import com.google.android.finsky.expressintegrityservice.ExpressIntegritySession import com.google.android.finsky.expressintegrityservice.IntermediateIntegrityResponseData import com.google.android.finsky.expressintegrityservice.PackageInformation import com.google.android.gms.droidguard.DroidGuard import com.google.android.gms.droidguard.internal.DroidGuardResultsRequest import com.google.android.gms.tasks.await Loading @@ -35,7 +38,9 @@ import kotlinx.coroutines.withContext import okio.ByteString import okio.ByteString.Companion.encode import okio.ByteString.Companion.toByteString import org.microg.gms.common.Constants import org.microg.gms.profile.Build import org.microg.gms.profile.ProfileManager import org.microg.vending.billing.DEFAULT_ACCOUNT_TYPE import org.microg.vending.billing.GServices import org.microg.vending.billing.core.HttpClient Loading @@ -49,6 +54,7 @@ import java.security.KeyPair import java.security.KeyPairGenerator import java.security.KeyStore import java.security.MessageDigest import java.security.NoSuchAlgorithmException import java.security.ProviderException import java.security.spec.ECGenParameterSpec import kotlin.coroutines.resume Loading Loading @@ -111,6 +117,15 @@ private fun Context.getProtoFile(): File { return file } fun Context.isNetworkConnected(): Boolean { return try { val connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager connectivityManager.activeNetworkInfo?.isConnected == true } catch (_: RuntimeException) { false } } private fun getExpressFilePB(context: Context): ExpressFilePB { return runCatching { FileInputStream(context.getProtoFile()).use { input -> ExpressFilePB.ADAPTER.decode(input) } } .onFailure { Log.w(TAG, "Failed to read express cache ", it) } Loading Loading @@ -155,6 +170,15 @@ fun ByteArray.encodeBase64(noPadding: Boolean, noWrap: Boolean = true, urlSafe: return Base64.encodeToString(this, flags) } fun ByteArray.md5(): ByteArray? { return try { val md5 = MessageDigest.getInstance("MD5") md5.digest(this) } catch (e: NoSuchAlgorithmException) { null } } fun ByteArray.sha256(): ByteArray { return MessageDigest.getInstance("SHA-256").digest(this) } Loading @@ -163,6 +187,11 @@ fun Bundle.getPlayCoreVersion() = PlayCoreVersion( major = getInt(KEY_VERSION_MAJOR, 0), minor = getInt(KEY_VERSION_MINOR, 0), patch = getInt(KEY_VERSION_PATCH, 0) ) fun List<AdviceType>?.ensureContainsLockBootloader(): List<AdviceType> { if (isNullOrEmpty()) return listOf(AdviceType.LOCK_BOOTLOADER) return if (contains(AdviceType.LOCK_BOOTLOADER)) this else listOf(AdviceType.LOCK_BOOTLOADER) + this } fun readAes128GcmBuilderFromClientKey(clientKey: ClientKey?): Aead? { if (clientKey == null) { return null Loading Loading @@ -249,7 +278,6 @@ fun fetchCertificateChain(context: Context, attestationChallenge: ByteArray?): L } suspend fun updateLocalExpressFilePB(context: Context, intermediateIntegrityResponseData: IntermediateIntegrityResponseData) = withContext(Dispatchers.IO) { Log.d(TAG, "Writing AAR to express cache") val intermediateIntegrity = intermediateIntegrityResponseData.intermediateIntegrity val expressFilePB = getExpressFilePB(context) Loading @@ -258,17 +286,13 @@ suspend fun updateLocalExpressFilePB(context: Context, intermediateIntegrityResp packageName = intermediateIntegrity.packageName cloudProjectNumber = intermediateIntegrity.cloudProjectNumber callerKey = intermediateIntegrity.callerKey webViewRequestMode = intermediateIntegrity.webViewRequestMode.let { when (it) { in 0..2 -> it + 1 else -> 1 } } - 1 webViewRequestMode = intermediateIntegrity.webViewRequestMode.takeIf { it in 0..2 } ?: 0 deviceIntegrityWrapper = DeviceIntegrityWrapper.Builder().apply { creationTime = intermediateIntegrity.callerKey.generated serverGenerated = intermediateIntegrity.serverGenerated deviceIntegrityToken = intermediateIntegrity.intermediateToken }.build() intermediateIntegrity.integrityAdvice?.let { advice = it } }.build() val requestList = expressFilePB.integrityRequestWrapper.toMutableList() Loading Loading @@ -508,3 +532,96 @@ suspend fun requestIntermediateIntegrity( adapter = IntermediateIntegrityResponseWrapperExtend.ADAPTER ) } fun buildClientKeyExtend( context: Context, session: ExpressIntegritySession, packageInformation: PackageInformation, clientKey: ClientKey ): ClientKeyExtend { return ClientKeyExtend.Builder().apply { cloudProjectNumber = session.cloudProjectNumber keySetHandle = clientKey.keySetHandle if (session.webViewRequestMode == 2) { this.optPackageName = KEY_OPT_PACKAGE this.versionCode = 0 } else { this.optPackageName = session.packageName this.versionCode = packageInformation.versionCode this.certificateSha256Hashes = packageInformation.certificateSha256Hashes } this.deviceSerialHash = ProfileManager.getSerial(context).toByteArray().sha256().toByteString() }.build() } fun buildInstallSourceMetaData( context: Context, packageName: String ): InstallSourceMetaData { fun resolveInstallerType(name: String?): InstallerType = when { name.isNullOrEmpty() -> InstallerType.UNSPECIFIED_INSTALLER name == Constants.VENDING_PACKAGE_NAME -> InstallerType.PHONESKY_INSTALLER else -> InstallerType.OTHER_INSTALLER } fun resolvePackageSourceType(type: Int): PackageSourceType = when (type) { 1 -> PackageSourceType.PACKAGE_SOURCE_OTHER 2 -> PackageSourceType.PACKAGE_SOURCE_STORE 3 -> PackageSourceType.PACKAGE_SOURCE_LOCAL_FILE 4 -> PackageSourceType.PACKAGE_SOURCE_DOWNLOADED_FILE else -> PackageSourceType.PACKAGE_SOURCE_UNSPECIFIED } val builder = InstallSourceMetaData.Builder().apply { installingPackageName = InstallerType.UNSPECIFIED_INSTALLER initiatingPackageName = InstallerType.UNSPECIFIED_INSTALLER originatingPackageName = InstallerType.UNSPECIFIED_INSTALLER updateOwnerPackageName = InstallerType.UNSPECIFIED_INSTALLER packageSourceType = PackageSourceType.PACKAGE_SOURCE_UNSPECIFIED } val applicationInfo = runCatching { context.packageManager.getApplicationInfo(packageName, 0) }.getOrNull() if (Build.VERSION.SDK_INT >= 30) { runCatching { val info = context.packageManager.getInstallSourceInfo(packageName) builder.apply { initiatingPackageName = resolveInstallerType(info.initiatingPackageName) installingPackageName = resolveInstallerType(info.installingPackageName) originatingPackageName = resolveInstallerType(info.originatingPackageName) if (Build.VERSION.SDK_INT >= 34) { updateOwnerPackageName = resolveInstallerType(info.updateOwnerPackageName) } if (Build.VERSION.SDK_INT >= 33) { packageSourceType = resolvePackageSourceType(info.packageSource) } } } } else { builder.installingPackageName = runCatching { resolveInstallerType(context.packageManager.getInstallerPackageName(packageName)) }.getOrElse { InstallerType.UNSPECIFIED_INSTALLER } } builder.appFlags = applicationInfo?.let { info -> buildList { if (info.flags and ApplicationInfo.FLAG_SYSTEM != 0) add(SystemAppFlag.FLAG_SYSTEM) if (info.flags and ApplicationInfo.FLAG_UPDATED_SYSTEM_APP != 0) { add(SystemAppFlag.FLAG_UPDATED_SYSTEM_APP) } }.ifEmpty { listOf(SystemAppFlag.SYSTEM_APP_INFO_UNSPECIFIED) } } ?: listOf(SystemAppFlag.SYSTEM_APP_INFO_UNSPECIFIED) return builder.build() } fun validateIntermediateIntegrityResponse(intermediateIntegrityResponse: IntermediateIntegrityResponseData) { val intermediateIntegrity = intermediateIntegrityResponse.intermediateIntegrity requireNotNull(intermediateIntegrity.intermediateToken) { "Null intermediateToken" } requireNotNull(intermediateIntegrity.serverGenerated) { "Null serverGenerated" } }
vending-app/src/main/kotlin/com/google/android/finsky/expressintegrityservice/ExpressIntegrityService.kt +62 −41 Original line number Diff line number Diff line Loading @@ -26,8 +26,10 @@ 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.IntegrityAdvice import com.google.android.finsky.INTERMEDIATE_INTEGRITY_HARD_EXPIRATION import com.google.android.finsky.IntermediateIntegrityRequest import com.google.android.finsky.IntermediateIntegrityResponse import com.google.android.finsky.IntermediateIntegritySession import com.google.android.finsky.KEY_CLOUD_PROJECT import com.google.android.finsky.KEY_NONCE Loading @@ -43,13 +45,20 @@ 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.TestErrorType import com.google.android.finsky.buildClientKeyExtend import com.google.android.finsky.buildInstallSourceMetaData import com.google.android.finsky.getPlayCoreVersion import com.google.android.finsky.encodeBase64 import com.google.android.finsky.ensureContainsLockBootloader import com.google.android.finsky.getAuthToken import com.google.android.finsky.getExpirationTime import com.google.android.finsky.getIntegrityRequestWrapper import com.google.android.finsky.getPackageInfoCompat import com.google.android.finsky.isNetworkConnected import com.google.android.finsky.md5 import com.google.android.finsky.model.IntegrityErrorCode import com.google.android.finsky.model.StandardIntegrityException import com.google.android.finsky.readAes128GcmBuilderFromClientKey import com.google.android.finsky.requestIntermediateIntegrity import com.google.android.finsky.sha256 Loading @@ -58,6 +67,7 @@ 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.finsky.validateIntermediateIntegrityResponse 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 Loading Loading @@ -91,11 +101,9 @@ private class ExpressIntegrityServiceImpl(private val context: Context, override 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") if (!context.isNetworkConnected()) { throw StandardIntegrityException(IntegrityErrorCode.NETWORK_ERROR, "No network is available") } Log.d(TAG, "warmUpIntegrityToken authToken: $authToken") val expressIntegritySession = ExpressIntegritySession( packageName = bundle.getString(KEY_PACKAGE_NAME) ?: "", Loading @@ -106,9 +114,18 @@ private class ExpressIntegrityServiceImpl(private val context: Context, override null, webViewRequestMode = bundle.getInt(KEY_REQUEST_MODE, 0) ) Log.d(TAG, "warmUpIntegrityToken session:$expressIntegritySession}") updateExpressSessionTime(context, expressIntegritySession, refreshWarmUpMethodTime = true, refreshRequestMethodTime = false) val clientKey = updateExpressClientKey(context) 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 expressFilePB = updateExpressAuthTokenWrapper(context, expressIntegritySession, authToken, clientKey) val tokenWrapper = expressFilePB.tokenWrapper ?: AuthTokenWrapper() Loading @@ -125,14 +142,14 @@ private class ExpressIntegrityServiceImpl(private val context: Context, override val deviceIntegrity = deviceIntegrityAndExpiredKey.deviceIntegrity if (deviceIntegrity.deviceIntegrityToken?.size == 0 || deviceIntegrity.clientKey?.keySetHandle?.size == 0) { throw RuntimeException("DroidGuard token is empty.") throw StandardIntegrityException("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.") throw StandardIntegrityException("Null deviceKeyMd5.") } val deviceIntegrityResponse = DeviceIntegrityResponse( Loading @@ -147,35 +164,16 @@ private class ExpressIntegrityServiceImpl(private val context: Context, override } val packageInformation = PackageInformation(certificateSha256Hashes, packageInfo.versionCode) val clientKeyExtend = ClientKeyExtend.Builder().apply { cloudProjectNumber = expressIntegritySession.cloudProjectNumber 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.getPlayCoreVersion() Log.d(TAG, "warmUpIntegrityToken sessionId:$sessionId") val clientKeyExtend = buildClientKeyExtend(context, expressIntegritySession, packageInformation, clientKey) 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)) playCoreVersion(bundle.getPlayCoreVersion()) sessionId(expressIntegritySession.sessionId) installSourceMetaData(buildInstallSourceMetaData(context, expressIntegritySession.packageName)) cloudProjectNumber(expressIntegritySession.cloudProjectNumber) playProtectDetails(PlayProtectDetails(PlayProtectState.PLAY_PROTECT_STATE_NONE)) if (expressIntegritySession.webViewRequestMode != 0) { requestMode(RequestMode.Builder().mode(expressIntegritySession.webViewRequestMode.takeIf { it in 0..2 } ?: 0).build()) Loading @@ -185,9 +183,19 @@ private class ExpressIntegrityServiceImpl(private val context: Context, override Log.d(TAG, "intermediateIntegrityRequest: $intermediateIntegrityRequest") val intermediateIntegrityResponse = requestIntermediateIntegrity(context, authToken, intermediateIntegrityRequest).intermediateIntegrityResponseWrapper?.intermediateIntegrityResponse ?: throw RuntimeException("intermediateIntegrityResponse is null.") Log.d(TAG, "requestIntermediateIntegrity: $intermediateIntegrityResponse") ?: IntermediateIntegrityResponse() Log.d(TAG, "requestIntermediateIntegrity response: ${intermediateIntegrityResponse.encode().encodeBase64(true)}") val errorCode = intermediateIntegrityResponse.errorInfo?.let { error -> if (error.errorCode == null) { null } else if (error.testErrorType == TestErrorType.REQUEST_EXPRESS) { error.errorCode } else if (error.testErrorType == TestErrorType.WARMUP) { throw StandardIntegrityException(error.errorCode, "Server-specified exception") } else null } val defaultAccountName: String = runCatching { if (expressIntegritySession.webViewRequestMode != 0) { Loading @@ -197,9 +205,13 @@ private class ExpressIntegrityServiceImpl(private val context: Context, override } }.getOrDefault(RESULT_UN_AUTH) val callerKeyMd5 = clientKey.encode().md5() ?: throw StandardIntegrityException("Null callerKeyMd5") val refreshClientKey = clientKey.newBuilder() .generated(makeTimestamp(System.currentTimeMillis())) .build() val fixedAdvice = IntegrityAdvice.Builder() .advices(intermediateIntegrityResponse.integrityAdvice?.advices.ensureContainsLockBootloader()) .build() val intermediateIntegrityResponseData = IntermediateIntegrityResponseData( intermediateIntegrity = IntermediateIntegrity( expressIntegritySession.packageName, Loading @@ -209,21 +221,24 @@ private class ExpressIntegrityServiceImpl(private val context: Context, override intermediateIntegrityResponse.intermediateToken, intermediateIntegrityResponse.serverGenerated, expressIntegritySession.webViewRequestMode, 0 ), callerKeyMd5 = Base64.encodeToString( refreshClientKey.encode(), Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING errorCode, fixedAdvice ), callerKeyMd5 = callerKeyMd5.encodeBase64(noPadding = true), appVersionCode = packageInformation.versionCode, deviceIntegrityResponse = deviceIntegrityResponse, appAccessRiskVerdictEnabled = intermediateIntegrityResponse.appAccessRiskVerdictEnabled ) validateIntermediateIntegrityResponse(intermediateIntegrityResponseData) updateLocalExpressFilePB(context, intermediateIntegrityResponseData) callback?.onWarmResult(bundleOf(KEY_WARM_UP_SID to sessionId)) callback?.onWarmResult(bundleOf(KEY_WARM_UP_SID to expressIntegritySession.sessionId)) }.onFailure { callback?.onWarmResult(bundleOf(KEY_ERROR to IntegrityErrorCode.INTEGRITY_TOKEN_PROVIDER_INVALID)) val exception = it as? StandardIntegrityException ?: StandardIntegrityException(it.message) Log.w(TAG, "warm up has failed: code=${exception.code}, message=${exception.message}", exception) callback?.onWarmResult(bundleOf(KEY_ERROR to exception.code)) } } } Loading @@ -242,6 +257,8 @@ private class ExpressIntegrityServiceImpl(private val context: Context, override webViewRequestMode = bundle.getInt(KEY_REQUEST_MODE, 0) ) Log.d(TAG, "requestExpressIntegrityToken session:$expressIntegritySession}") if (TextUtils.isEmpty(expressIntegritySession.packageName)) { Log.w(TAG, "packageName is empty.") callback?.onRequestResult(bundleOf(KEY_ERROR to IntegrityErrorCode.INTERNAL_ERROR)) Loading Loading @@ -277,6 +294,9 @@ private class ExpressIntegrityServiceImpl(private val context: Context, override return@launchWhenCreated } integrityRequestWrapper.deviceIntegrityWrapper?.errorCode?.let { throw StandardIntegrityException(it, "Server-specified exception") } val expirationTime = integrityRequestWrapper.getExpirationTime() if (expirationTime > INTERMEDIATE_INTEGRITY_HARD_EXPIRATION * 1000) { Loading Loading @@ -309,8 +329,9 @@ private class ExpressIntegrityServiceImpl(private val context: Context, override ) ) }.onFailure { Log.e(TAG, "requesting token has failed for ${bundle.getString(KEY_PACKAGE_NAME)}.") callback?.onRequestResult(bundleOf(KEY_ERROR to IntegrityErrorCode.INTEGRITY_TOKEN_PROVIDER_INVALID)) val exception = it as? StandardIntegrityException ?: StandardIntegrityException(it.message) Log.w(TAG, "requesting token has failed: code=${exception.code}, message=${exception.message}", exception) callback?.onRequestResult(bundleOf(KEY_ERROR to exception.code)) } } } Loading
vending-app/src/main/kotlin/com/google/android/finsky/expressintegrityservice/ExpressIntegritySession.kt +9 −1 Original line number Diff line number Diff line Loading @@ -12,4 +12,12 @@ data class ExpressIntegritySession( var originatingWarmUpSessionId: Long, var verdictOptOut: List<Int>?, var webViewRequestMode: Int ) { override fun toString(): String { return "ExpressIntegritySession(packageName='$packageName', cloudProjectNumber=$cloudProjectNumber, sessionId=$sessionId, requestHash=$requestHash, originatingWarmUpSessionId=$originatingWarmUpSessionId, verdictOptOut=${ verdictOptOut?.joinToString( prefix = "[", postfix = "]" ) }, webViewRequestMode=$webViewRequestMode)" } } No newline at end of file
vending-app/src/main/kotlin/com/google/android/finsky/expressintegrityservice/IntermediateIntegrity.kt +3 −1 Original line number Diff line number Diff line Loading @@ -6,6 +6,7 @@ package com.google.android.finsky.expressintegrityservice import com.google.android.finsky.ClientKey import com.google.android.finsky.IntegrityAdvice import okio.ByteString import org.microg.vending.proto.Timestamp Loading @@ -17,5 +18,6 @@ data class IntermediateIntegrity( var intermediateToken: ByteString?, var serverGenerated: Timestamp?, var webViewRequestMode: Int, var testErrorCode: Int var testErrorCode: Int?, var integrityAdvice: IntegrityAdvice? ) No newline at end of file
vending-app/src/main/kotlin/com/google/android/finsky/model/StandardIntegrityException.kt 0 → 100644 +18 −0 Original line number Diff line number Diff line /** * SPDX-FileCopyrightText: 2025 microG Project Team * SPDX-License-Identifier: Apache-2.0 */ package com.google.android.finsky.model class StandardIntegrityException : Exception { val code: Int constructor(code: Int, message: String) : super(message) { this.code = code } constructor(cause: String?) : super(cause) { this.code = IntegrityErrorCode.INTERNAL_ERROR } } No newline at end of file