diff --git a/play-services-base/core/src/main/kotlin/org/microg/gms/settings/SettingsContract.kt b/play-services-base/core/src/main/kotlin/org/microg/gms/settings/SettingsContract.kt index 8a9ff68474..5ef726412c 100644 --- a/play-services-base/core/src/main/kotlin/org/microg/gms/settings/SettingsContract.kt +++ b/play-services-base/core/src/main/kotlin/org/microg/gms/settings/SettingsContract.kt @@ -279,6 +279,7 @@ object SettingsContract { const val ASSET_DEVICE_SYNC = "vending_device_sync" const val APPS_INSTALL = "vending_apps_install" const val APPS_INSTALLER_LIST = "vending_apps_installer_list" + const val PLAY_INTEGRITY_APP_LIST = "vending_play_integrity_apps" val PROJECTION = arrayOf( LICENSING, @@ -289,6 +290,7 @@ object SettingsContract { ASSET_DEVICE_SYNC, APPS_INSTALL, APPS_INSTALLER_LIST, + PLAY_INTEGRITY_APP_LIST ) } diff --git a/play-services-base/core/src/main/kotlin/org/microg/gms/settings/SettingsProvider.kt b/play-services-base/core/src/main/kotlin/org/microg/gms/settings/SettingsProvider.kt index fd05f7b639..df0cabfd41 100644 --- a/play-services-base/core/src/main/kotlin/org/microg/gms/settings/SettingsProvider.kt +++ b/play-services-base/core/src/main/kotlin/org/microg/gms/settings/SettingsProvider.kt @@ -369,6 +369,7 @@ class SettingsProvider : ContentProvider() { Vending.SPLIT_INSTALL -> getSettingsBoolean(key, false) Vending.APPS_INSTALL -> getSettingsBoolean(key, false) Vending.APPS_INSTALLER_LIST -> getSettingsString(key, "") + Vending.PLAY_INTEGRITY_APP_LIST -> getSettingsString(key, "") else -> throw IllegalArgumentException("Unknown key: $key") } } @@ -386,6 +387,7 @@ class SettingsProvider : ContentProvider() { Vending.ASSET_DEVICE_SYNC -> editor.putBoolean(key, value as Boolean) Vending.APPS_INSTALL -> editor.putBoolean(key, value as Boolean) Vending.APPS_INSTALLER_LIST -> editor.putString(key, value as String) + Vending.PLAY_INTEGRITY_APP_LIST -> editor.putString(key, value as String) else -> throw IllegalArgumentException("Unknown key: $key") } } diff --git a/play-services-base/core/src/main/kotlin/org/microg/gms/vending/PlayIntegrityData.kt b/play-services-base/core/src/main/kotlin/org/microg/gms/vending/PlayIntegrityData.kt new file mode 100644 index 0000000000..bb5fde337c --- /dev/null +++ b/play-services-base/core/src/main/kotlin/org/microg/gms/vending/PlayIntegrityData.kt @@ -0,0 +1,71 @@ +/** + * SPDX-FileCopyrightText: 2025 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.vending + +import org.json.JSONException +import org.json.JSONObject + +class PlayIntegrityData(var allowed: Boolean, + val packageName: String, + val pkgSignSha256: String, + var lastTime: Long, + var lastResult: String? = null, + var lastStatus: Boolean = false) { + + override fun toString(): String { + return JSONObject() + .put(ALLOWED, allowed) + .put(PACKAGE_NAME, packageName) + .put(SIGNATURE, pkgSignSha256) + .put(LAST_VISIT_TIME, lastTime) + .put(LAST_VISIT_RESULT, lastResult) + .put(LAST_VISIT_STATUS, lastStatus) + .toString() + } + + companion object { + private const val PACKAGE_NAME = "packageName" + private const val ALLOWED = "allowed" + private const val SIGNATURE = "signature" + private const val LAST_VISIT_TIME = "lastVisitTime" + private const val LAST_VISIT_RESULT = "lastVisitResult" + private const val LAST_VISIT_STATUS = "lastVisitStatus" + + private fun parse(jsonString: String): PlayIntegrityData? { + try { + val json = JSONObject(jsonString) + return PlayIntegrityData( + json.getBoolean(ALLOWED), + json.getString(PACKAGE_NAME), + json.getString(SIGNATURE), + json.getLong(LAST_VISIT_TIME), + json.getString(LAST_VISIT_RESULT), + json.getBoolean(LAST_VISIT_STATUS) + ) + } catch (e: JSONException) { + return null + } + } + + fun loadDataSet(content: String): Set { + return content.split("|").mapNotNull { parse(it) }.toSet() + } + + fun updateDataSetString(channelList: Set, channel: PlayIntegrityData): String { + val channelData = channelList.find { it.packageName == channel.packageName && it.pkgSignSha256 == channel.pkgSignSha256 } + val newChannelList = if (channelData != null) { + channelData.allowed = channel.allowed + channelData.lastTime = channel.lastTime + channelData.lastResult = channel.lastResult + channelData.lastStatus = channel.lastStatus + channelList + } else { + channelList + channel + } + return newChannelList.let { it -> it.joinToString(separator = "|") { it.toString() } } + } + } +} \ No newline at end of file diff --git a/play-services-core/src/main/kotlin/org/microg/gms/ui/SafetyNetAllAppsFragment.kt b/play-services-core/src/main/kotlin/org/microg/gms/ui/SafetyNetAllAppsFragment.kt index 78a489d040..cf06c39289 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/ui/SafetyNetAllAppsFragment.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/ui/SafetyNetAllAppsFragment.kt @@ -18,6 +18,8 @@ import com.google.android.gms.R import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.microg.gms.safetynet.SafetyNetDatabase +import org.microg.gms.vending.PlayIntegrityData +import org.microg.gms.vending.VendingPreferences class SafetyNetAllAppsFragment : PreferenceFragmentCompat() { private lateinit var database: SafetyNetDatabase @@ -50,8 +52,10 @@ class SafetyNetAllAppsFragment : PreferenceFragmentCompat() { private fun updateContent() { val context = requireContext() lifecycleScope.launchWhenResumed { + val playIntegrityData = VendingPreferences.getPlayIntegrityAppList(context) val apps = withContext(Dispatchers.IO) { - val res = database.recentApps.map { app -> + val playPairs = PlayIntegrityData.loadDataSet(playIntegrityData).map { it.packageName to it.lastTime } + val res = (database.recentApps + playPairs).map { app -> val pref = AppIconPreference(context) pref.packageName = app.first pref.summary = when { diff --git a/play-services-core/src/main/kotlin/org/microg/gms/ui/SafetyNetAppFragment.kt b/play-services-core/src/main/kotlin/org/microg/gms/ui/SafetyNetAppFragment.kt index 6e3cb295c1..c3cf395a4e 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/ui/SafetyNetAppFragment.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/ui/SafetyNetAppFragment.kt @@ -8,16 +8,26 @@ package org.microg.gms.ui import android.annotation.SuppressLint import android.os.Bundle import android.text.format.DateUtils +import androidx.core.content.ContextCompat import androidx.lifecycle.lifecycleScope -import androidx.preference.* +import androidx.preference.Preference +import androidx.preference.PreferenceCategory +import androidx.preference.PreferenceFragmentCompat +import androidx.preference.SwitchPreferenceCompat +import androidx.preference.isEmpty import com.google.android.gms.R import org.microg.gms.safetynet.SafetyNetDatabase -import org.microg.gms.safetynet.SafetyNetRequestType.* +import org.microg.gms.safetynet.SafetyNetRequestType.ATTESTATION +import org.microg.gms.safetynet.SafetyNetRequestType.RECAPTCHA +import org.microg.gms.safetynet.SafetyNetRequestType.RECAPTCHA_ENTERPRISE +import org.microg.gms.vending.PlayIntegrityData +import org.microg.gms.vending.VendingPreferences class SafetyNetAppFragment : PreferenceFragmentCompat() { private lateinit var appHeadingPreference: AppHeadingPreference private lateinit var recents: PreferenceCategory private lateinit var recentsNone: Preference + private lateinit var recentRequestAllow: SwitchPreferenceCompat private val packageName: String? get() = arguments?.getString("package") @@ -30,6 +40,16 @@ class SafetyNetAppFragment : PreferenceFragmentCompat() { appHeadingPreference = preferenceScreen.findPreference("pref_safetynet_app_heading") ?: appHeadingPreference recents = preferenceScreen.findPreference("prefcat_safetynet_recent_list") ?: recents recentsNone = preferenceScreen.findPreference("pref_safetynet_recent_none") ?: recentsNone + recentRequestAllow = preferenceScreen.findPreference("pref_safetynet_app_allow_request") ?: recentRequestAllow + recentRequestAllow.setOnPreferenceChangeListener { _, newValue -> + val playIntegrityDataSet = loadPlayIntegrityData() + val integrityData = packageName?.let { packageName -> playIntegrityDataSet.find { packageName == it.packageName } } + if (newValue is Boolean && integrityData != null) { + val content = PlayIntegrityData.updateDataSetString(playIntegrityDataSet, integrityData.apply { this.allowed = newValue }) + VendingPreferences.setPlayIntegrityAppList(requireContext(), content) + } + true + } } override fun onResume() { @@ -37,6 +57,11 @@ class SafetyNetAppFragment : PreferenceFragmentCompat() { updateContent() } + private fun loadPlayIntegrityData(): Set { + val playIntegrityData = VendingPreferences.getPlayIntegrityAppList(requireContext()) + return PlayIntegrityData.loadDataSet(playIntegrityData) + } + fun updateContent() { lifecycleScope.launchWhenResumed { appHeadingPreference.packageName = packageName @@ -52,7 +77,6 @@ class SafetyNetAppFragment : PreferenceFragmentCompat() { }.orEmpty() recents.removeAll() recents.addPreference(recentsNone) - recentsNone.isVisible = summaries.isEmpty() for (summary in summaries) { val preference = Preference(requireContext()) preference.onPreferenceClickListener = Preference.OnPreferenceClickListener { @@ -84,6 +108,23 @@ class SafetyNetAppFragment : PreferenceFragmentCompat() { } recents.addPreference(preference) } + val piContent = packageName?.let { packageName -> loadPlayIntegrityData().find { packageName == it.packageName } } + if (piContent != null) { + val preference = Preference(requireContext()) + val date = DateUtils.getRelativeDateTimeString( + context, + piContent.lastTime, + DateUtils.MINUTE_IN_MILLIS, + DateUtils.WEEK_IN_MILLIS, + DateUtils.FORMAT_SHOW_TIME + ) + preference.title = date + preference.summary = piContent.lastResult + preference.icon = if (piContent.lastStatus) ContextCompat.getDrawable(context, R.drawable.ic_circle_check) else ContextCompat.getDrawable(context, R.drawable.ic_circle_warn) + recents.addPreference(preference) + } + recentsNone.isVisible = recents.isEmpty() + recentRequestAllow.isVisible = piContent != null } } diff --git a/play-services-core/src/main/kotlin/org/microg/gms/ui/SafetyNetFragment.kt b/play-services-core/src/main/kotlin/org/microg/gms/ui/SafetyNetFragment.kt index e2a0090ddc..91f6e0b419 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/ui/SafetyNetFragment.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/ui/SafetyNetFragment.kt @@ -5,7 +5,6 @@ package org.microg.gms.ui -import android.annotation.SuppressLint import android.os.Bundle import android.util.Base64 import android.util.Log @@ -38,6 +37,8 @@ import org.microg.gms.safetynet.SafetyNetDatabase import org.microg.gms.safetynet.SafetyNetPreferences import org.microg.gms.safetynet.SafetyNetRequestType.* import org.microg.gms.utils.singleInstanceOf +import org.microg.gms.vending.PlayIntegrityData +import org.microg.gms.vending.VendingPreferences import java.net.URLEncoder import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException @@ -231,9 +232,10 @@ class SafetyNetFragment : PreferenceFragmentCompat() { lifecycleScope.launchWhenResumed { val context = requireContext() val (apps, showAll) = withContext(Dispatchers.IO) { + val playIntegrityData = VendingPreferences.getPlayIntegrityAppList(context) val db = SafetyNetDatabase(context) val apps = try { - db.recentApps + db.recentApps + PlayIntegrityData.loadDataSet(playIntegrityData).map { it.packageName to it.lastTime } } finally { db.close() } diff --git a/play-services-core/src/main/kotlin/org/microg/gms/vending/VendingPreferences.kt b/play-services-core/src/main/kotlin/org/microg/gms/vending/VendingPreferences.kt index 4f95a41b18..59de550ea8 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/vending/VendingPreferences.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/vending/VendingPreferences.kt @@ -129,4 +129,19 @@ object VendingPreferences { put(SettingsContract.Vending.APPS_INSTALLER_LIST, content) } } + + @JvmStatic + fun getPlayIntegrityAppList(context: Context): String { + val projection = arrayOf(SettingsContract.Vending.PLAY_INTEGRITY_APP_LIST) + return SettingsContract.getSettings(context, SettingsContract.Vending.getContentUri(context), projection) { c -> + c.getString(0) + } + } + + @JvmStatic + fun setPlayIntegrityAppList(context: Context, content: String) { + SettingsContract.setSettings(context, SettingsContract.Vending.getContentUri(context)) { + put(SettingsContract.Vending.PLAY_INTEGRITY_APP_LIST, content) + } + } } \ No newline at end of file diff --git a/play-services-core/src/main/res/values-zh-rCN/strings.xml b/play-services-core/src/main/res/values-zh-rCN/strings.xml index 4e9a452a5e..8e5c5417fd 100644 --- a/play-services-core/src/main/res/values-zh-rCN/strings.xml +++ b/play-services-core/src/main/res/values-zh-rCN/strings.xml @@ -136,6 +136,8 @@ 自定义:%s 自动:%s 系统:%s + 允许请求 + 允许应用程序请求设备身份验证 "测试 SafetyNet 认证" "Google SafetyNet 是一套设备认证系统,旨在确认设备具有适当安全性,并与 Android CTS 兼容。某些应用会出于安全考虑或是防篡改目的而使用 SafetyNet。 @@ -153,7 +155,7 @@ microG GmsCore 内置一套自由的 SafetyNet 实现,但是官方服务器要 从文件导入自定义的设备配置信息 选择配置信息 设备配置信息 - 使用 SafetyNet 的应用 + 使用设备认证的应用 清除近期的 SafetyNet 请求 最近使用于%1$s 评估类型 @@ -225,7 +227,7 @@ microG GmsCore 内置一套自由的 SafetyNet 实现,但是官方服务器要 证明:%s 添加和管理 Google 账号 读取Google服务配置 - Google SafetyNet + Google 设备认证 ReCaptcha: %s ReCaptcha Enterprise: %s Google 游戏账号 diff --git a/play-services-core/src/main/res/values-zh-rTW/strings.xml b/play-services-core/src/main/res/values-zh-rTW/strings.xml index 2d1b451be5..cad516518e 100644 --- a/play-services-core/src/main/res/values-zh-rTW/strings.xml +++ b/play-services-core/src/main/res/values-zh-rTW/strings.xml @@ -159,7 +159,7 @@ 警告:%s 執行中… 運作模式 - 使用 SafetyNet 的應用程式 + 使用設備認證的應用程式 清除最近的請求 原生 實機 @@ -195,7 +195,9 @@ 存取您的車輛行駛里程 車用廠商通訊通道 存取您車輛的車廠專屬通道,以交換與車輛相關的專屬資訊 - Google SafetyNet + Google 設備認證 + 允許請求 + 允許應用程式請求裝置身份驗證 啟用此功能後,驗證請求中將不包含裝置名稱,這可能允許未授權的裝置登入,但也可能導致不可預期的後果。 狀態 更多 diff --git a/play-services-core/src/main/res/values/strings.xml b/play-services-core/src/main/res/values/strings.xml index e1d7ecf16a..8d2fb3fea8 100644 --- a/play-services-core/src/main/res/values/strings.xml +++ b/play-services-core/src/main/res/values/strings.xml @@ -95,7 +95,7 @@ Please set up a password, PIN, or pattern lock screen." Google device registration Cloud Messaging - Google SafetyNet + Google Device Attestation Play Store services Work profile @@ -238,7 +238,9 @@ Please set up a password, PIN, or pattern lock screen." Running… Operation mode DroidGuard execution is unsupported on this device. SafetyNet services may misbehave. - Apps using SafetyNet + Apps using Device Attestation + Allow Request + Allow the app to request device authentication Clear recent requests Last use: %1$s diff --git a/play-services-core/src/main/res/xml/preferences_safetynet_app.xml b/play-services-core/src/main/res/xml/preferences_safetynet_app.xml index c799e545f9..6ef57ab4c9 100644 --- a/play-services-core/src/main/res/xml/preferences_safetynet_app.xml +++ b/play-services-core/src/main/res/xml/preferences_safetynet_app.xml @@ -13,6 +13,15 @@ tools:title="@tools:sample/lorem" app:allowDividerBelow="false" /> + + + c.getInt(0) != 0 + } + } + + @JvmStatic + fun getPlayIntegrityAppList(context: Context): String { + val projection = arrayOf(SettingsContract.Vending.PLAY_INTEGRITY_APP_LIST) + return SettingsContract.getSettings(context, SettingsContract.Vending.getContentUri(context), projection) { c -> + c.getString(0) + } + } + + @JvmStatic + fun setPlayIntegrityAppList(context: Context, content: String) { + SettingsContract.setSettings(context, SettingsContract.Vending.getContentUri(context)) { + put(SettingsContract.Vending.PLAY_INTEGRITY_APP_LIST, content) + } + } } \ No newline at end of file diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/IntegrityExtensions.kt b/vending-app/src/main/kotlin/com/google/android/finsky/IntegrityExtensions.kt index 2610e00697..0233fbf28d 100644 --- a/vending-app/src/main/kotlin/com/google/android/finsky/IntegrityExtensions.kt +++ b/vending-app/src/main/kotlin/com/google/android/finsky/IntegrityExtensions.kt @@ -19,11 +19,15 @@ import android.security.keystore.KeyProperties import android.text.TextUtils import android.util.Base64 import android.util.Log +import androidx.core.content.edit +import com.android.vending.VendingPreferences 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.finsky.model.IntegrityErrorCode +import com.google.android.finsky.model.StandardIntegrityException import com.google.android.gms.droidguard.DroidGuard import com.google.android.gms.droidguard.internal.DroidGuardResultsRequest import com.google.android.gms.tasks.await @@ -36,11 +40,14 @@ import com.google.crypto.tink.aead.AesGcmKeyManager import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import okio.ByteString +import okio.ByteString.Companion.decodeBase64 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.gms.utils.getFirstSignatureDigest +import org.microg.gms.vending.PlayIntegrityData import org.microg.vending.billing.DEFAULT_ACCOUNT_TYPE import org.microg.vending.billing.GServices import org.microg.vending.billing.core.HttpClient @@ -97,6 +104,32 @@ private const val DEVICE_INTEGRITY_HARD_EXPIRATION = 432000L // 5 day const val INTERMEDIATE_INTEGRITY_HARD_EXPIRATION = 86400L // 1 day private const val TAG = "IntegrityExtensions" +fun callerAppToVisitData(context: Context, callingPackage: String): PlayIntegrityData { + val pkgSignSha256ByteArray = context.packageManager.getFirstSignatureDigest(callingPackage, "SHA-256") + if (pkgSignSha256ByteArray == null) { + throw StandardIntegrityException(IntegrityErrorCode.APP_NOT_INSTALLED, "$callingPackage signature is null") + } + val pkgSignSha256 = Base64.encodeToString(pkgSignSha256ByteArray, Base64.NO_WRAP) + Log.d(TAG, "callerToVisitData $callingPackage pkgSignSha256: $pkgSignSha256") + val playIntegrityAppList = VendingPreferences.getPlayIntegrityAppList(context) + val loadDataSet = PlayIntegrityData.loadDataSet(playIntegrityAppList) + if (loadDataSet.isEmpty() || loadDataSet.none { it.packageName == callingPackage && it.pkgSignSha256 == pkgSignSha256 }) { + return PlayIntegrityData(true, callingPackage, pkgSignSha256, System.currentTimeMillis()) + } + return loadDataSet.first { it.packageName == callingPackage && it.pkgSignSha256 == pkgSignSha256 } +} + +fun PlayIntegrityData.updateAppVisitContent(context: Context, time: Long, result: String, status: Boolean = false) { + val playIntegrityAppList = VendingPreferences.getPlayIntegrityAppList(context) + val loadDataSet = PlayIntegrityData.loadDataSet(playIntegrityAppList) + val dataSetString = PlayIntegrityData.updateDataSetString(loadDataSet, apply { + lastTime = time + lastResult = result + lastStatus = status + }) + VendingPreferences.setPlayIntegrityAppList(context, dataSetString) +} + fun IntegrityRequestWrapper.getExpirationTime() = runCatching { val creationTimeStamp = deviceIntegrityWrapper?.creationTime ?: Timestamp(0, 0) val creationTime = (creationTimeStamp.seconds ?: 0) * 1000 + (creationTimeStamp.nanos ?: 0) / 1_000_000 @@ -395,7 +428,8 @@ suspend fun updateExpressAuthTokenWrapper(context: Context, expressIntegritySess private suspend fun regenerateToken( context: Context, authToken: String, packageName: String, clientKey: ClientKey ): AuthTokenWrapper { - try { + val prefs = context.getSharedPreferences("device_integrity_token", Context.MODE_PRIVATE) + val deviceIntegrityToken = try { Log.d(TAG, "regenerateToken authToken:$authToken, packageName:$packageName, clientKey:$clientKey") val droidGuardSessionTokenResponse = requestDroidGuardSessionToken(context, authToken) @@ -425,20 +459,22 @@ private suspend fun regenerateToken( val deviceIntegrityTokenType = deviceIntegrityTokenResponse.tokenWrapper?.tokenContent?.tokenType?.firstOrNull { it.type?.toInt() == 5 } ?: throw RuntimeException("regenerateToken deviceIntegrityTokenType is null!") - val deviceIntegrityToken = deviceIntegrityTokenType.tokenSessionWrapper?.wrapper?.sessionContent?.tokenContent?.tokenWrapper?.token - - return AuthTokenWrapper.Builder().apply { - this.clientKey = clientKey - this.deviceIntegrityWrapper = DeviceIntegrityWrapper.Builder().apply { - this.deviceIntegrityToken = deviceIntegrityToken ?: ByteString.EMPTY - this.creationTime = makeTimestamp(System.currentTimeMillis()) - }.build() - this.lastManualSoftRefreshTime = makeTimestamp(System.currentTimeMillis()) - }.build() + deviceIntegrityTokenType.tokenSessionWrapper?.wrapper?.sessionContent?.tokenContent?.tokenWrapper?.token?.also { + prefs.edit { putString(packageName, it.base64()) } + } } catch (e: Exception) { Log.d(TAG, "regenerateToken: error ", e) - return AuthTokenWrapper() + prefs.getString(packageName, null)?.decodeBase64() } + + return AuthTokenWrapper.Builder().apply { + this.clientKey = clientKey + this.deviceIntegrityWrapper = DeviceIntegrityWrapper.Builder().apply { + this.deviceIntegrityToken = deviceIntegrityToken ?: ByteString.EMPTY + this.creationTime = makeTimestamp(System.currentTimeMillis()) + }.build() + this.lastManualSoftRefreshTime = makeTimestamp(System.currentTimeMillis()) + }.build() } private suspend fun requestDroidGuardSessionToken(context: Context, authToken: String): TokenResponse { diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/expressintegrityservice/ExpressIntegrityService.kt b/vending-app/src/main/kotlin/com/google/android/finsky/expressintegrityservice/ExpressIntegrityService.kt index 524e389f5e..7fd4e24c86 100644 --- a/vending-app/src/main/kotlin/com/google/android/finsky/expressintegrityservice/ExpressIntegrityService.kt +++ b/vending-app/src/main/kotlin/com/google/android/finsky/expressintegrityservice/ExpressIntegrityService.kt @@ -20,23 +20,22 @@ import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleService import androidx.lifecycle.lifecycleScope import com.android.vending.AUTH_TOKEN_SCOPE +import com.android.vending.VendingPreferences import com.android.vending.makeTimestamp import com.google.android.finsky.AuthTokenWrapper import com.google.android.finsky.ClientKey -import com.google.android.finsky.ClientKeyExtend import com.google.android.finsky.DeviceIntegrityWrapper import com.google.android.finsky.ExpressIntegrityResponse -import com.google.android.finsky.IntegrityAdvice import com.google.android.finsky.INTERMEDIATE_INTEGRITY_HARD_EXPIRATION +import com.google.android.finsky.IntegrityAdvice 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_ERROR import com.google.android.finsky.KEY_NONCE -import com.google.android.finsky.KEY_OPT_PACKAGE import com.google.android.finsky.KEY_PACKAGE_NAME import com.google.android.finsky.KEY_REQUEST_MODE -import com.google.android.finsky.KEY_ERROR import com.google.android.finsky.KEY_REQUEST_TOKEN_SID import com.google.android.finsky.KEY_REQUEST_VERDICT_OPT_OUT import com.google.android.finsky.KEY_TOKEN @@ -48,13 +47,14 @@ 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.callerAppToVisitData 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.getPlayCoreVersion import com.google.android.finsky.isNetworkConnected import com.google.android.finsky.md5 import com.google.android.finsky.model.IntegrityErrorCode @@ -63,6 +63,7 @@ import com.google.android.finsky.readAes128GcmBuilderFromClientKey import com.google.android.finsky.requestIntermediateIntegrity import com.google.android.finsky.sha256 import com.google.android.finsky.signaturesCompat +import com.google.android.finsky.updateAppVisitContent import com.google.android.finsky.updateExpressAuthTokenWrapper import com.google.android.finsky.updateExpressClientKey import com.google.android.finsky.updateExpressSessionTime @@ -74,6 +75,7 @@ import com.google.android.play.core.integrity.protocol.IRequestDialogCallback import com.google.crypto.tink.config.TinkConfig import okio.ByteString.Companion.toByteString import org.microg.gms.profile.ProfileManager +import org.microg.gms.vending.PlayIntegrityData import org.microg.vending.billing.DEFAULT_ACCOUNT_TYPE import org.microg.vending.proto.Timestamp import kotlin.random.Random @@ -98,15 +100,30 @@ class ExpressIntegrityService : LifecycleService() { private class ExpressIntegrityServiceImpl(private val context: Context, override val lifecycle: Lifecycle) : IExpressIntegrityService.Stub(), LifecycleOwner { + private var visitData: PlayIntegrityData? = null + override fun warmUpIntegrityToken(bundle: Bundle, callback: IExpressIntegrityServiceCallback?) { lifecycleScope.launchWhenCreated { runCatching { + val callingPackageName = bundle.getString(KEY_PACKAGE_NAME) + if (callingPackageName == null) { + throw StandardIntegrityException(IntegrityErrorCode.INTERNAL_ERROR, "Null packageName.") + } + visitData = callerAppToVisitData(context, callingPackageName) + if (visitData?.allowed != true) { + throw StandardIntegrityException(IntegrityErrorCode.API_NOT_AVAILABLE, "Not allowed visit") + } + val playIntegrityEnabled = VendingPreferences.isDeviceAttestationEnabled(context) + if (!playIntegrityEnabled) { + throw StandardIntegrityException(IntegrityErrorCode.API_NOT_AVAILABLE, "API is disabled") + } + if (!context.isNetworkConnected()) { throw StandardIntegrityException(IntegrityErrorCode.NETWORK_ERROR, "No network is available") } val expressIntegritySession = ExpressIntegritySession( - packageName = bundle.getString(KEY_PACKAGE_NAME) ?: "", + packageName = callingPackageName ?: "", cloudProjectNumber = bundle.getLong(KEY_CLOUD_PROJECT, 0L), sessionId = Random.nextLong(), null, @@ -234,10 +251,12 @@ private class ExpressIntegrityServiceImpl(private val context: Context, override updateLocalExpressFilePB(context, intermediateIntegrityResponseData) + visitData?.updateAppVisitContent(context, System.currentTimeMillis(), "$TAG visited success.", true) callback?.onWarmResult(bundleOf(KEY_WARM_UP_SID to expressIntegritySession.sessionId)) }.onFailure { val exception = it as? StandardIntegrityException ?: StandardIntegrityException(it.message) Log.w(TAG, "warm up has failed: code=${exception.code}, message=${exception.message}", exception) + visitData?.updateAppVisitContent(context, System.currentTimeMillis(), "$TAG visited failed. ${exception.message}") callback?.onWarmResult(bundleOf(KEY_ERROR to exception.code)) } } @@ -247,8 +266,21 @@ private class ExpressIntegrityServiceImpl(private val context: Context, override Log.d(TAG, "requestExpressIntegrityToken bundle:$bundle") lifecycleScope.launchWhenCreated { runCatching { + val callingPackageName = bundle.getString(KEY_PACKAGE_NAME) + if (callingPackageName == null) { + throw StandardIntegrityException(IntegrityErrorCode.INTERNAL_ERROR, "Null packageName.") + } + visitData = callerAppToVisitData(context, callingPackageName) + if (visitData?.allowed != true) { + throw StandardIntegrityException(IntegrityErrorCode.API_NOT_AVAILABLE, "Not allowed visit") + } + val playIntegrityEnabled = VendingPreferences.isDeviceAttestationEnabled(context) + if (!playIntegrityEnabled) { + throw StandardIntegrityException(IntegrityErrorCode.API_NOT_AVAILABLE, "API is disabled") + } + val expressIntegritySession = ExpressIntegritySession( - packageName = bundle.getString(KEY_PACKAGE_NAME) ?: "", + packageName = callingPackageName, cloudProjectNumber = bundle.getLong(KEY_CLOUD_PROJECT, 0L), sessionId = Random.nextLong(), requestHash = bundle.getString(KEY_NONCE), @@ -321,6 +353,7 @@ private class ExpressIntegrityServiceImpl(private val context: Context, override ) Log.d(TAG, "requestExpressIntegrityToken token: $token, sid: ${expressIntegritySession.sessionId}, mode: ${expressIntegritySession.webViewRequestMode}") + visitData?.updateAppVisitContent(context, System.currentTimeMillis(), "$TAG visited success.", true) callback?.onRequestResult( bundleOf( KEY_TOKEN to token, @@ -331,6 +364,7 @@ private class ExpressIntegrityServiceImpl(private val context: Context, override }.onFailure { val exception = it as? StandardIntegrityException ?: StandardIntegrityException(it.message) Log.w(TAG, "requesting token has failed: code=${exception.code}, message=${exception.message}", exception) + visitData?.updateAppVisitContent(context, System.currentTimeMillis(), "$TAG visited failed. ${exception.message}") callback?.onRequestResult(bundleOf(KEY_ERROR to exception.code)) } } diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/integrityservice/IntegrityService.kt b/vending-app/src/main/kotlin/com/google/android/finsky/integrityservice/IntegrityService.kt index 76478e7ade..1b5ce5fda9 100644 --- a/vending-app/src/main/kotlin/com/google/android/finsky/integrityservice/IntegrityService.kt +++ b/vending-app/src/main/kotlin/com/google/android/finsky/integrityservice/IntegrityService.kt @@ -19,6 +19,7 @@ import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleService import androidx.lifecycle.lifecycleScope import com.android.vending.AUTH_TOKEN_SCOPE +import com.android.vending.VendingPreferences import com.android.vending.makeTimestamp import com.google.android.finsky.AccessibilityAbuseSignalDataWrapper import com.google.android.finsky.AppAccessRiskDetailsResponse @@ -44,14 +45,17 @@ import com.google.android.finsky.SIGNING_FLAGS import com.google.android.finsky.ScreenCaptureSignalDataWrapper import com.google.android.finsky.ScreenOverlaySignalDataWrapper import com.google.android.finsky.VersionCodeWrapper +import com.google.android.finsky.callerAppToVisitData import com.google.android.finsky.getPlayCoreVersion import com.google.android.finsky.encodeBase64 import com.google.android.finsky.getAuthToken import com.google.android.finsky.getPackageInfoCompat import com.google.android.finsky.model.IntegrityErrorCode +import com.google.android.finsky.model.StandardIntegrityException import com.google.android.finsky.requestIntegritySyncData import com.google.android.finsky.sha256 import com.google.android.finsky.signaturesCompat +import com.google.android.finsky.updateAppVisitContent import com.google.android.gms.droidguard.DroidGuard import com.google.android.gms.droidguard.internal.DroidGuardResultsRequest import com.google.android.gms.tasks.await @@ -62,6 +66,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import okio.ByteString.Companion.toByteString import org.microg.gms.profile.ProfileManager +import org.microg.gms.vending.PlayIntegrityData private const val TAG = "IntegrityService" @@ -82,6 +87,8 @@ class IntegrityService : LifecycleService() { private class IntegrityServiceImpl(private val context: Context, override val lifecycle: Lifecycle) : IIntegrityService.Stub(), LifecycleOwner { + private var visitData: PlayIntegrityData? = null + override fun requestDialog(bundle: Bundle, callback: IRequestDialogCallback) { Log.d(TAG, "Method (requestDialog) called but not implemented ") requestAndShowDialog(bundle, callback) @@ -93,63 +100,66 @@ private class IntegrityServiceImpl(private val context: Context, override val li override fun requestIntegrityToken(request: Bundle, callback: IIntegrityServiceCallback) { Log.d(TAG, "Method (requestIntegrityToken) called") - val packageName = request.getString(KEY_PACKAGE_NAME) - if (packageName == null) { - callback.onError("", IntegrityErrorCode.INTERNAL_ERROR, "Null packageName.") - return - } - val nonceArr = request.getByteArray(KEY_NONCE) - if (nonceArr == null) { - callback.onError(packageName, IntegrityErrorCode.INTERNAL_ERROR, "Nonce missing.") - return - } - if (nonceArr.size < 16) { - callback.onError(packageName, IntegrityErrorCode.NONCE_TOO_SHORT, "Nonce too short.") - return - } - if (nonceArr.size >= 500) { - callback.onError(packageName, IntegrityErrorCode.NONCE_TOO_LONG, "Nonce too long.") - return - } - val cloudProjectNumber = request.getLong(KEY_CLOUD_PROJECT, 0L) - val playCoreVersion = request.getPlayCoreVersion() - Log.d(TAG, "requestIntegrityToken(packageName: $packageName, nonce: ${nonceArr.encodeBase64(false)}, cloudProjectNumber: $cloudProjectNumber, playCoreVersion: $playCoreVersion)") - - val packageInfo = context.packageManager.getPackageInfoCompat(packageName, SIGNING_FLAGS) - val timestamp = makeTimestamp(System.currentTimeMillis()) - val versionCode = packageInfo.versionCode - - val integrityParams = IntegrityParams( - packageName = PackageNameWrapper(packageName), - versionCode = VersionCodeWrapper(versionCode), - nonce = nonceArr.encodeBase64(noPadding = false, noWrap = true, urlSafe = true), - certificateSha256Digests = packageInfo.signaturesCompat.map { - it.toByteArray().sha256().encodeBase64(noPadding = true, noWrap = true, urlSafe = true) - }, - timestampAtRequest = timestamp, - cloudProjectNumber = cloudProjectNumber.takeIf { it > 0L } - ) - - val data = mutableMapOf( - PARAMS_PKG_KEY to packageName, - PARAMS_VC_KEY to versionCode.toString(), - PARAMS_NONCE_SHA256_KEY to nonceArr.sha256().encodeBase64(noPadding = true, noWrap = true, urlSafe = true), - PARAMS_TM_S_KEY to timestamp.seconds.toString(), - PARAMS_BINDING_KEY to integrityParams.encode().encodeBase64(noPadding = false, noWrap = true, urlSafe = true), - ) - if (cloudProjectNumber > 0L) { - data[PARAMS_GCP_N_KEY] = cloudProjectNumber.toString() - } - - var mapSize = 0 - data.entries.forEach { mapSize += it.key.toByteArray().size + it.value.toByteArray().size } - if (mapSize > 65536) { - callback.onError(packageName, IntegrityErrorCode.INTERNAL_ERROR, "Content binding size exceeded maximum allowed size.") - return - } - lifecycleScope.launchWhenCreated { runCatching { + val packageName = request.getString(KEY_PACKAGE_NAME) + if (packageName == null) { + throw StandardIntegrityException(IntegrityErrorCode.INTERNAL_ERROR, "Null packageName.") + } + visitData = callerAppToVisitData(context, packageName) + if (visitData?.allowed != true) { + throw StandardIntegrityException(IntegrityErrorCode.INTERNAL_ERROR, "Not allowed visit API.") + } + val playIntegrityEnabled = VendingPreferences.isDeviceAttestationEnabled(context) + if (!playIntegrityEnabled) { + throw StandardIntegrityException(IntegrityErrorCode.INTERNAL_ERROR, "API is disabled.") + } + val nonceArr = request.getByteArray(KEY_NONCE) + if (nonceArr == null) { + throw StandardIntegrityException(IntegrityErrorCode.INTERNAL_ERROR, "Nonce missing.") + } + if (nonceArr.size < 16) { + throw StandardIntegrityException(IntegrityErrorCode.NONCE_TOO_SHORT, "Nonce too short.") + } + if (nonceArr.size >= 500) { + throw StandardIntegrityException(IntegrityErrorCode.NONCE_TOO_LONG, "Nonce too long.") + } + val cloudProjectNumber = request.getLong(KEY_CLOUD_PROJECT, 0L) + val playCoreVersion = request.getPlayCoreVersion() + Log.d(TAG, "requestIntegrityToken(packageName: $packageName, nonce: ${nonceArr.encodeBase64(false)}, cloudProjectNumber: $cloudProjectNumber, playCoreVersion: $playCoreVersion)") + + val packageInfo = context.packageManager.getPackageInfoCompat(packageName, SIGNING_FLAGS) + val timestamp = makeTimestamp(System.currentTimeMillis()) + val versionCode = packageInfo.versionCode + + val integrityParams = IntegrityParams( + packageName = PackageNameWrapper(packageName), + versionCode = VersionCodeWrapper(versionCode), + nonce = nonceArr.encodeBase64(noPadding = false, noWrap = true, urlSafe = true), + certificateSha256Digests = packageInfo.signaturesCompat.map { + it.toByteArray().sha256().encodeBase64(noPadding = true, noWrap = true, urlSafe = true) + }, + timestampAtRequest = timestamp, + cloudProjectNumber = cloudProjectNumber.takeIf { it > 0L } + ) + + val data = mutableMapOf( + PARAMS_PKG_KEY to packageName, + PARAMS_VC_KEY to versionCode.toString(), + PARAMS_NONCE_SHA256_KEY to nonceArr.sha256().encodeBase64(noPadding = true, noWrap = true, urlSafe = true), + PARAMS_TM_S_KEY to timestamp.seconds.toString(), + PARAMS_BINDING_KEY to integrityParams.encode().encodeBase64(noPadding = false, noWrap = true, urlSafe = true), + ) + if (cloudProjectNumber > 0L) { + data[PARAMS_GCP_N_KEY] = cloudProjectNumber.toString() + } + + var mapSize = 0 + data.entries.forEach { mapSize += it.key.toByteArray().size + it.value.toByteArray().size } + if (mapSize > 65536) { + throw StandardIntegrityException(IntegrityErrorCode.INTERNAL_ERROR, "Content binding size exceeded maximum allowed size.") + } + val authToken = getAuthToken(context, AUTH_TOKEN_SCOPE) if (TextUtils.isEmpty(authToken)) { Log.w(TAG, "requestIntegrityToken: Got null auth token for type: $AUTH_TOKEN_SCOPE") @@ -167,8 +177,7 @@ private class IntegrityServiceImpl(private val context: Context, override val li if (droidGuardData.utf8().startsWith(INTEGRITY_PREFIX_ERROR)) { Log.w(TAG, "droidGuardData: ${droidGuardData.utf8()}") - callback.onError(packageName, IntegrityErrorCode.NETWORK_ERROR, "DroidGuard failed.") - return@launchWhenCreated + throw StandardIntegrityException(IntegrityErrorCode.NETWORK_ERROR, "DroidGuard failed.") } val integrityRequest = IntegrityRequest( @@ -193,15 +202,16 @@ private class IntegrityServiceImpl(private val context: Context, override val li val integrityToken = integrityResponse.contentWrapper?.content?.token if (integrityToken.isNullOrEmpty()) { - callback.onError(packageName, IntegrityErrorCode.INTERNAL_ERROR, "IntegrityResponse didn't have a token") - return@launchWhenCreated + throw StandardIntegrityException(IntegrityErrorCode.INTERNAL_ERROR,"IntegrityResponse didn't have a token") } Log.d(TAG, "requestIntegrityToken integrityToken: $integrityToken") + visitData?.updateAppVisitContent(context, System.currentTimeMillis(), "$TAG visited success.", true) callback.onSuccess(packageName, integrityToken) }.onFailure { Log.w(TAG, "requestIntegrityToken has exception: ", it) - callback.onError(packageName, IntegrityErrorCode.INTERNAL_ERROR, it.message ?: "Exception") + visitData?.updateAppVisitContent(context, System.currentTimeMillis(), "$TAG visited failed. ${it.message}") + callback.onError(visitData?.packageName, IntegrityErrorCode.INTERNAL_ERROR, it.message ?: "Exception") } } }