diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d45230acf1..0ae962d846 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -26,7 +26,7 @@ jobs: # Log available space df -h - name: "Checkout sources" - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: fetch-depth: 0 - name: "Setup Java" diff --git a/.github/workflows/dependency-submission.yml b/.github/workflows/dependency-submission.yml index 923e95793e..55d11e560f 100644 --- a/.github/workflows/dependency-submission.yml +++ b/.github/workflows/dependency-submission.yml @@ -13,7 +13,7 @@ jobs: steps: - name: "Checkout sources" - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: fetch-depth: 0 - name: "Setup Java" diff --git a/build.gradle b/build.gradle index ae1cd09be2..c05a2b956f 100644 --- a/build.gradle +++ b/build.gradle @@ -24,6 +24,7 @@ buildscript { ext.preferenceVersion = '1.2.0' ext.recyclerviewVersion = '1.3.2' ext.webkitVersion = '1.10.0' + ext.workVersion = '2.7.0' ext.slf4jVersion = '1.7.36' ext.volleyVersion = '1.2.1' diff --git a/play-services-auth/src/main/aidl/com/google/android/gms/auth/api/identity/ClearTokenRequest.aidl b/play-services-auth/src/main/aidl/com/google/android/gms/auth/api/identity/ClearTokenRequest.aidl new file mode 100644 index 0000000000..a2b736fb20 --- /dev/null +++ b/play-services-auth/src/main/aidl/com/google/android/gms/auth/api/identity/ClearTokenRequest.aidl @@ -0,0 +1,8 @@ +/* + * SPDX-FileCopyrightText: 2025 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.auth.api.identity; + +parcelable ClearTokenRequest; \ No newline at end of file diff --git a/play-services-auth/src/main/aidl/com/google/android/gms/auth/api/identity/RevokeAccessRequest.aidl b/play-services-auth/src/main/aidl/com/google/android/gms/auth/api/identity/RevokeAccessRequest.aidl new file mode 100644 index 0000000000..25526d9a51 --- /dev/null +++ b/play-services-auth/src/main/aidl/com/google/android/gms/auth/api/identity/RevokeAccessRequest.aidl @@ -0,0 +1,8 @@ +/* + * SPDX-FileCopyrightText: 2025 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.auth.api.identity; + +parcelable RevokeAccessRequest; \ No newline at end of file diff --git a/play-services-auth/src/main/aidl/com/google/android/gms/auth/api/identity/internal/IAuthorizationService.aidl b/play-services-auth/src/main/aidl/com/google/android/gms/auth/api/identity/internal/IAuthorizationService.aidl index db1fc73f2a..656f0c0f2d 100644 --- a/play-services-auth/src/main/aidl/com/google/android/gms/auth/api/identity/internal/IAuthorizationService.aidl +++ b/play-services-auth/src/main/aidl/com/google/android/gms/auth/api/identity/internal/IAuthorizationService.aidl @@ -9,8 +9,13 @@ import com.google.android.gms.auth.api.identity.internal.IAuthorizationCallback; import com.google.android.gms.auth.api.identity.internal.IVerifyWithGoogleCallback; import com.google.android.gms.auth.api.identity.AuthorizationRequest; import com.google.android.gms.auth.api.identity.VerifyWithGoogleRequest; +import com.google.android.gms.auth.api.identity.RevokeAccessRequest; +import com.google.android.gms.auth.api.identity.ClearTokenRequest; +import com.google.android.gms.common.api.internal.IStatusCallback; interface IAuthorizationService { void authorize(in IAuthorizationCallback callback, in AuthorizationRequest request) = 0; void verifyWithGoogle(in IVerifyWithGoogleCallback callback, in VerifyWithGoogleRequest request) = 1; + void revokeAccess(in IStatusCallback callback, in RevokeAccessRequest request) = 2; + void clearToken(in IStatusCallback callback, in ClearTokenRequest request) = 3; } \ No newline at end of file diff --git a/play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/RevokeAccessRequest.java b/play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/RevokeAccessRequest.java index 71c3140f9b..0019e5cf0a 100644 --- a/play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/RevokeAccessRequest.java +++ b/play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/RevokeAccessRequest.java @@ -143,4 +143,13 @@ public static abstract class Builder { public void writeToParcel(@NonNull Parcel parcel, int flags) { CREATOR.writeToParcel(this, parcel, flags); } + + @Override + public String toString() { + return "RevokeAccessRequest{" + + "scopes=" + scopes + + ", account=" + account + + ", sessionId='" + sessionId + '\'' + + '}'; + } } diff --git a/play-services-base/core/src/main/kotlin/org/microg/gms/common/AccountUtils.kt b/play-services-base/core/src/main/kotlin/org/microg/gms/common/AccountUtils.kt new file mode 100644 index 0000000000..51a6637a30 --- /dev/null +++ b/play-services-base/core/src/main/kotlin/org/microg/gms/common/AccountUtils.kt @@ -0,0 +1,50 @@ +/** + * SPDX-FileCopyrightText: 2025 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.common + +import android.accounts.Account +import android.annotation.SuppressLint +import android.content.Context +import android.content.Context.MODE_PRIVATE +import androidx.core.content.edit +import org.microg.gms.auth.AuthConstants + +class AccountUtils(val context: Context) { + + private val prefs = context.getSharedPreferences("common.selected_account_prefs", MODE_PRIVATE) + + companion object { + private const val TYPE = "selected_account_type:" + @SuppressLint("StaticFieldLeak") + @Volatile + private var instance: AccountUtils? = null + fun get(context: Context): AccountUtils = instance ?: synchronized(this) { + instance ?: AccountUtils(context.applicationContext).also { instance = it } + } + } + + fun saveSelectedAccount(packageName: String, account: Account?) { + if (account != null) { + prefs.edit(true) { + putString(packageName, account.name) + putString(TYPE.plus(packageName), account.type) + } + } + } + + fun getSelectedAccount(packageName: String): Account? { + val name = prefs.getString(packageName, null) ?: return null + val type = prefs.getString(TYPE.plus(packageName), AuthConstants.DEFAULT_ACCOUNT_TYPE) ?: return null + return Account(name, type) + } + + fun removeSelectedAccount(packageName: String) { + prefs.edit { + remove(packageName) + remove(TYPE.plus(packageName)) + } + } +} \ No newline at end of file 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/build.gradle b/play-services-core/build.gradle index 5648333872..8fc2896bbc 100644 --- a/play-services-core/build.gradle +++ b/play-services-core/build.gradle @@ -102,6 +102,8 @@ dependencies { implementation "androidx.lifecycle:lifecycle-service:$lifecycleVersion" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion" + + implementation "androidx.work:work-runtime-ktx:$workVersion" } android { diff --git a/play-services-core/microg-ui-tools/src/main/res/values-ja/strings.xml b/play-services-core/microg-ui-tools/src/main/res/values-ja/strings.xml index 2b6a1d512b..6c396dbd32 100644 --- a/play-services-core/microg-ui-tools/src/main/res/values-ja/strings.xml +++ b/play-services-core/microg-ui-tools/src/main/res/values-ja/strings.xml @@ -30,6 +30,7 @@ 権限付与 %1$sの権限: + 仕事用プロファイルを操作する権限: ここをタップして権限を付与してください。 権限を付与しないと、アプリが正しく動作しない可能性があります。 microG UIデモ diff --git a/play-services-core/microg-ui-tools/src/main/res/values-th/strings.xml b/play-services-core/microg-ui-tools/src/main/res/values-th/strings.xml new file mode 100644 index 0000000000..cfc9669d4c --- /dev/null +++ b/play-services-core/microg-ui-tools/src/main/res/values-th/strings.xml @@ -0,0 +1,45 @@ + + + + + เครื่องมือส่วนต่อประสานกับผู้ใช้ของ microG + Apache License 2.0, ทีม microG + + เวอร์ชัน %1$s + %1$s %2$s + สงวนลิขสิทธิ์ทุกประการ + + ตั้งค่า + + โมดูลการตรวจสอบด้วยตนเอง + ตรวจสอบว่าระบบได้รับการตั้งค่าอย่างถูกต้องเพื่อใช้งาน microG หรือไม่ + + ได้รับสิทธิ์แล้ว + สิทธิ์ในการเข้าถึง %1$s: + สิทธิ์ในการโต้ตอบกับโปรไฟล์งานของบริษัท: + แตะที่นี้เพื่อทำการให้สิทธิ์ การไม่ให้สิทธิ์อาจจะส่งผลให้เกิดพฤติกรรมไม่เหมาะสม + + สาธิตส่วนต่อประสานกับผู้ใช้ของ microG + สรุป + เวอร์ชัน v0.1.0 + ไลบรารีสนับสนุน + + ไลบรารีสนับสนุนเวอร์ชัน 4 + ไลบรารีสนับสนุน appcompat เวอร์ชัน 7 + ไลบรารีสนับสนุนการตั้งค่า เวอร์ชัน 7 + Apache License 2.0, The Android Open Source Project + \ No newline at end of file diff --git a/play-services-core/src/main/AndroidManifest.xml b/play-services-core/src/main/AndroidManifest.xml index c030acb41f..c6b36cfa6d 100644 --- a/play-services-core/src/main/AndroidManifest.xml +++ b/play-services-core/src/main/AndroidManifest.xml @@ -376,8 +376,9 @@ android:exported="false" android:process=":persistent"> - + + @@ -420,8 +421,7 @@ + android:name="org.microg.gms.common.PersistentTrustedReceiver"> diff --git a/play-services-core/src/main/java/org/microg/gms/gcm/McsService.java b/play-services-core/src/main/java/org/microg/gms/gcm/McsService.java index 5c298ae418..7bce08dbd3 100644 --- a/play-services-core/src/main/java/org/microg/gms/gcm/McsService.java +++ b/play-services-core/src/main/java/org/microg/gms/gcm/McsService.java @@ -81,8 +81,8 @@ import static android.app.AlarmManager.ELAPSED_REALTIME_WAKEUP; import static android.os.Build.VERSION.SDK_INT; import static org.microg.gms.common.PackageUtils.warnIfNotPersistentProcess; +import static org.microg.gms.gcm.ExtensionsKt.ACTION_GCM_CONNECTED; import static org.microg.gms.gcm.GcmConstants.*; -import static org.microg.gms.gcm.ExtensionsKt.ACTION_GCM_REGISTERED; import static org.microg.gms.gcm.McsConstants.*; @ForegroundServiceInfo(value = "Cloud messaging", resName = "service_name_mcs", resPackage = "com.google.android.gms") @@ -497,15 +497,15 @@ private void handleLoginResponse(LoginResponse loginResponse) { if (loginResponse.error == null) { GcmPrefs.clearLastPersistedId(this); logd(this, "Logged in"); - notifyGcmRegistered(); + notifyGcmConnected(); wakeLock.release(); } else { throw new RuntimeException("Could not login: " + loginResponse.error); } } - private void notifyGcmRegistered() { - Intent intent = new Intent(ACTION_GCM_REGISTERED); + private void notifyGcmConnected() { + Intent intent = new Intent(ACTION_GCM_CONNECTED); intent.setPackage(Constants.GMS_PACKAGE_NAME); sendBroadcast(intent); } diff --git a/play-services-core/src/main/java/org/microg/gms/gcm/UnregisterReceiver.java b/play-services-core/src/main/java/org/microg/gms/gcm/UnregisterReceiver.java deleted file mode 100644 index bba2e414c4..0000000000 --- a/play-services-core/src/main/java/org/microg/gms/gcm/UnregisterReceiver.java +++ /dev/null @@ -1,50 +0,0 @@ -package org.microg.gms.gcm; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.util.Log; - -import java.util.List; - -import static android.content.Intent.ACTION_PACKAGE_REMOVED; -import static android.content.Intent.ACTION_PACKAGE_DATA_CLEARED; -import static android.content.Intent.ACTION_PACKAGE_FULLY_REMOVED; -import static android.content.Intent.EXTRA_DATA_REMOVED; -import static android.content.Intent.EXTRA_REPLACING; - -public class UnregisterReceiver extends BroadcastReceiver { - private static final String TAG = "GmsGcmUnregisterRcvr"; - - @Override - public void onReceive(final Context context, Intent intent) { - Log.d(TAG, "Package changed: " + intent); - if ((ACTION_PACKAGE_REMOVED.contains(intent.getAction()) && intent.getBooleanExtra(EXTRA_DATA_REMOVED, false) && - !intent.getBooleanExtra(EXTRA_REPLACING, false)) || - ACTION_PACKAGE_FULLY_REMOVED.contains(intent.getAction()) || - ACTION_PACKAGE_DATA_CLEARED.contains(intent.getAction())) { - final GcmDatabase database = new GcmDatabase(context); - final String packageName = intent.getData().getSchemeSpecificPart(); - Log.d(TAG, "Package removed or data cleared: " + packageName); - final GcmDatabase.App app = database.getApp(packageName); - if (app != null) { - new Thread(new Runnable() { - @Override - public void run() { - List registrations = database.getRegistrationsByApp(packageName); - boolean deletedAll = true; - for (GcmDatabase.Registration registration : registrations) { - deletedAll &= PushRegisterManager.unregister(context, registration.packageName, registration.signature, null, null).deleted != null; - } - if (deletedAll) { - database.removeApp(packageName); - } - database.close(); - } - }).start(); - } else { - database.close(); - } - } - } -} diff --git a/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/bridge/OcUiBridge.kt b/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/bridge/OcUiBridge.kt index a18429194d..c7169fcb18 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/bridge/OcUiBridge.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/bridge/OcUiBridge.kt @@ -18,6 +18,7 @@ import org.microg.gms.accountsettings.ui.EXTRA_SCREEN_ID import org.microg.gms.accountsettings.ui.KEY_UPDATED_PHOTO_URL import org.microg.gms.accountsettings.ui.MainActivity import org.microg.gms.accountsettings.ui.finishActivity +import org.microg.gms.accountsettings.ui.runOnMainLooper class OcUiBridge(val activity: MainActivity, val accountName:String?, val webView: WebView?) { @@ -105,7 +106,7 @@ class OcUiBridge(val activity: MainActivity, val accountName:String?, val webVie @JavascriptInterface fun setBackStop() { Log.d(TAG, "setBackStop: ") - webView?.clearHistory() + runOnMainLooper { webView?.clearHistory() } } @JavascriptInterface diff --git a/play-services-core/src/main/kotlin/org/microg/gms/auth/credentials/identity/AuthorizationService.kt b/play-services-core/src/main/kotlin/org/microg/gms/auth/credentials/identity/AuthorizationService.kt index 2b96ddc12d..286b543485 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/auth/credentials/identity/AuthorizationService.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/auth/credentials/identity/AuthorizationService.kt @@ -5,6 +5,7 @@ package org.microg.gms.auth.credentials.identity +import android.accounts.AccountManager import android.app.PendingIntent import android.app.PendingIntent.FLAG_IMMUTABLE import android.app.PendingIntent.FLAG_UPDATE_CURRENT @@ -16,6 +17,8 @@ import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope import com.google.android.gms.auth.api.identity.AuthorizationRequest import com.google.android.gms.auth.api.identity.AuthorizationResult +import com.google.android.gms.auth.api.identity.ClearTokenRequest +import com.google.android.gms.auth.api.identity.RevokeAccessRequest import com.google.android.gms.auth.api.identity.VerifyWithGoogleRequest import com.google.android.gms.auth.api.identity.VerifyWithGoogleResult import com.google.android.gms.auth.api.identity.internal.IAuthorizationCallback @@ -26,21 +29,26 @@ import com.google.android.gms.auth.api.signin.internal.SignInConfiguration import com.google.android.gms.common.ConnectionResult import com.google.android.gms.common.api.Scope import com.google.android.gms.common.api.Status +import com.google.android.gms.common.api.internal.IStatusCallback import com.google.android.gms.common.internal.ConnectionInfo import com.google.android.gms.common.internal.GetServiceRequest import com.google.android.gms.common.internal.IGmsCallbacks import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.microg.gms.BaseService +import org.microg.gms.auth.AuthConstants import org.microg.gms.auth.credentials.FEATURES import org.microg.gms.auth.signin.AuthSignInActivity import org.microg.gms.auth.signin.SignInConfigurationService +import org.microg.gms.auth.signin.getOAuthManager import org.microg.gms.auth.signin.getServerAuthTokenManager import org.microg.gms.auth.signin.performSignIn import org.microg.gms.auth.signin.scopeUris +import org.microg.gms.common.AccountUtils import org.microg.gms.common.Constants import org.microg.gms.common.GmsService import org.microg.gms.common.PackageUtils +import java.util.concurrent.atomic.AtomicInteger private const val TAG = "AuthorizationService" @@ -60,42 +68,67 @@ class AuthorizationService : BaseService(TAG, GmsService.AUTH_API_IDENTITY_AUTHO class AuthorizationServiceImpl(val context: Context, val packageName: String, override val lifecycle: Lifecycle) : IAuthorizationService.Stub(), LifecycleOwner { + companion object{ + private val nextRequestCode = AtomicInteger(0) + } + override fun authorize(callback: IAuthorizationCallback?, request: AuthorizationRequest?) { - Log.d(TAG, "Method: authorize called, request:$request") + Log.d(TAG, "Method: authorize called, packageName:$packageName request:$request") lifecycleScope.launchWhenStarted { - val account = request?.account ?: SignInConfigurationService.getDefaultAccount(context, packageName) - if (account == null) { - Log.d(TAG, "Method: authorize called, but account is null") - callback?.onAuthorized(Status.CANCELED, null) - return@launchWhenStarted - } + val requestAccount = request?.account + val account = requestAccount ?: AccountUtils.get(context).getSelectedAccount(packageName) val googleSignInOptions = GoogleSignInOptions.Builder().apply { - setAccountName(account.name) request?.requestedScopes?.forEach { requestScopes(it) } - if (request?.idTokenRequested == true && request.serverClientId != null) requestIdToken(request.serverClientId) + if (request?.idTokenRequested == true && request.serverClientId != null) { + if (account?.name != requestAccount?.name) { + requestEmail().requestProfile() + } + requestIdToken(request.serverClientId) + } if (request?.serverAuthCodeRequested == true && request.serverClientId != null) requestServerAuthCode(request.serverClientId, request.forceCodeForRefreshToken) }.build() - val intent = Intent(context, AuthSignInActivity::class.java).apply { - `package` = Constants.GMS_PACKAGE_NAME - putExtra("config", SignInConfiguration(packageName, googleSignInOptions)) - } - val signInAccount = performSignIn(context, packageName, googleSignInOptions, account, false) - callback?.onAuthorized(Status.SUCCESS, + Log.d(TAG, "authorize: account: ${account?.name}") + val result = if (account != null) { + val (accessToken, signInAccount) = performSignIn(context, packageName, googleSignInOptions, account, false) + if (requestAccount != null) { + AccountUtils.get(context).saveSelectedAccount(packageName, requestAccount) + } AuthorizationResult( signInAccount?.serverAuthCode, - signInAccount?.idToken, + accessToken, signInAccount?.idToken, signInAccount?.grantedScopes?.toList().orEmpty().map { it.scopeUri }, signInAccount, - PendingIntent.getActivity(context, account.hashCode(), intent, FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE) - ).also { Log.d(TAG, "authorize: result:$it") }) + null + ) + } else { + val options = GoogleSignInOptions.Builder(googleSignInOptions).apply { + val defaultAccount = SignInConfigurationService.getDefaultAccount(context, packageName) + defaultAccount?.name?.let { setAccountName(it) } + }.build() + val intent = Intent(context, AuthSignInActivity::class.java).apply { + `package` = Constants.GMS_PACKAGE_NAME + putExtra("config", SignInConfiguration(packageName, options)) + } + AuthorizationResult( + null, + null, + null, + request?.requestedScopes.orEmpty().map { it.scopeUri }, + null, + PendingIntent.getActivity(context, nextRequestCode.incrementAndGet(), intent, FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE) + ) + } + runCatching { + callback?.onAuthorized(Status.SUCCESS, result.also { Log.d(TAG, "authorize: result:$it") }) + } } } override fun verifyWithGoogle(callback: IVerifyWithGoogleCallback?, request: VerifyWithGoogleRequest?) { Log.d(TAG, "unimplemented Method: verifyWithGoogle: request:$request") lifecycleScope.launchWhenStarted { - val account = SignInConfigurationService.getDefaultAccount(context, packageName) + val account = AccountUtils.get(context).getSelectedAccount(packageName) ?: SignInConfigurationService.getDefaultAccount(context, packageName) if (account == null) { Log.d(TAG, "Method: authorize called, but account is null") callback?.onVerifed(Status.CANCELED, null) @@ -119,4 +152,31 @@ class AuthorizationServiceImpl(val context: Context, val packageName: String, ov } } + override fun revokeAccess(callback: IStatusCallback?, request: RevokeAccessRequest?) { + Log.d(TAG, "Method: revokeAccess called, request:$request") + lifecycleScope.launchWhenStarted { + val authOptions = SignInConfigurationService.getAuthOptions(context, packageName) + val authAccount = request?.account + if (authOptions.isNotEmpty() && authAccount != null) { + val authManager = getOAuthManager(context, packageName, authOptions.first(), authAccount) + val token = authManager.peekAuthToken() + if (token != null) { + // todo "https://oauth2.googleapis.com/revoke" + authManager.invalidateAuthToken(token) + authManager.isPermitted = false + } + } + AccountUtils.get(context).removeSelectedAccount(packageName) + runCatching { callback?.onResult(Status.SUCCESS) } + } + } + + override fun clearToken(callback: IStatusCallback?, request: ClearTokenRequest?) { + Log.d(TAG, "Method: clearToken called, request:$request") + request?.token?.let { + AccountManager.get(context).invalidateAuthToken(AuthConstants.DEFAULT_ACCOUNT_TYPE, it) + } + runCatching { callback?.onResult(Status.SUCCESS) } + } + } \ No newline at end of file diff --git a/play-services-core/src/main/kotlin/org/microg/gms/auth/credentials/identity/IdentitySignInService.kt b/play-services-core/src/main/kotlin/org/microg/gms/auth/credentials/identity/IdentitySignInService.kt index 4ed9bcdc02..815c3c4b60 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/auth/credentials/identity/IdentitySignInService.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/auth/credentials/identity/IdentitySignInService.kt @@ -5,7 +5,6 @@ package org.microg.gms.auth.credentials.identity -import android.accounts.AccountManager import android.app.PendingIntent import android.content.Context import android.content.Intent @@ -14,6 +13,9 @@ import android.util.Base64 import android.util.Log import androidx.core.app.PendingIntentCompat import androidx.core.os.bundleOf +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope import com.google.android.gms.auth.api.identity.BeginSignInRequest import com.google.android.gms.auth.api.identity.BeginSignInResult import com.google.android.gms.auth.api.identity.GetPhoneNumberHintIntentRequest @@ -34,17 +36,19 @@ import com.google.android.gms.fido.common.Transport import com.google.android.gms.fido.fido2.api.common.PublicKeyCredentialDescriptor import com.google.android.gms.fido.fido2.api.common.PublicKeyCredentialRequestOptions import com.google.android.gms.fido.fido2.api.common.UserVerificationRequirement +import kotlinx.coroutines.launch import org.json.JSONArray import org.json.JSONObject import org.microg.gms.BaseService -import org.microg.gms.auth.AuthConstants import org.microg.gms.auth.signin.ACTION_ASSISTED_SIGN_IN import org.microg.gms.auth.signin.BEGIN_SIGN_IN_REQUEST import org.microg.gms.auth.signin.GET_SIGN_IN_INTENT_REQUEST import org.microg.gms.auth.credentials.FEATURES import org.microg.gms.auth.signin.CLIENT_PACKAGE_NAME import org.microg.gms.auth.signin.GOOGLE_SIGN_IN_OPTIONS +import org.microg.gms.auth.signin.SignInConfigurationService import org.microg.gms.auth.signin.performSignOut +import org.microg.gms.common.AccountUtils import org.microg.gms.common.GmsService import org.microg.gms.fido.core.Database import org.microg.gms.fido.core.ui.AuthenticatorActivity.Companion.KEY_OPTIONS @@ -61,13 +65,13 @@ class IdentitySignInService : BaseService(TAG, GmsService.AUTH_API_IDENTITY_SIGN val connectionInfo = ConnectionInfo() connectionInfo.features = FEATURES callback.onPostInitCompleteWithConnectionInfo( - ConnectionResult.SUCCESS, IdentitySignInServiceImpl(this, request.packageName).asBinder(), connectionInfo + ConnectionResult.SUCCESS, IdentitySignInServiceImpl(this, request.packageName, lifecycle).asBinder(), connectionInfo ) } } -class IdentitySignInServiceImpl(private val context: Context, private val clientPackageName: String) : - ISignInService.Stub() { +class IdentitySignInServiceImpl(private val context: Context, private val clientPackageName: String, override val lifecycle: Lifecycle) : + ISignInService.Stub(), LifecycleOwner { private val requestMap = mutableMapOf() @@ -130,11 +134,15 @@ class IdentitySignInServiceImpl(private val context: Context, private val client override fun signOut(callback: IStatusCallback, requestTag: String) { Log.d(TAG, "method signOut called, requestTag=$requestTag") - if (requestMap.containsKey(requestTag)) { - val accounts = AccountManager.get(context).getAccountsByType(AuthConstants.DEFAULT_ACCOUNT_TYPE) - if (accounts.isNotEmpty()) { - accounts.forEach { performSignOut(context, clientPackageName, requestMap[requestTag], it) } + lifecycleScope.launch { + val signInAccount = SignInConfigurationService.getDefaultAccount(context, clientPackageName) + val authOptions = SignInConfigurationService.getAuthOptions(context, clientPackageName).plus(requestMap[requestTag]) + if (signInAccount != null && authOptions.isNotEmpty()) { + authOptions.forEach { + performSignOut(context, clientPackageName, it, signInAccount) + } } + AccountUtils.get(context).removeSelectedAccount(clientPackageName) } callback.onResult(Status.SUCCESS) } diff --git a/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/AssistedSignInFragment.kt b/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/AssistedSignInFragment.kt index 75711452af..72ba905ce0 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/AssistedSignInFragment.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/AssistedSignInFragment.kt @@ -42,6 +42,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.microg.gms.auth.AuthConstants import org.microg.gms.auth.login.LoginActivity +import org.microg.gms.common.AccountUtils import org.microg.gms.people.PeopleManager import org.microg.gms.utils.getApplicationLabel @@ -75,6 +76,7 @@ class AssistedSignInFragment : BottomSheetDialogFragment() { private var container: FrameLayout? = null private var loginJob: Job? = null private var isSigningIn = false + private var signInBack = false private val authStatusList = arraySetOf>() private var lastChooseAccount: Account? = null @@ -91,14 +93,18 @@ class AssistedSignInFragment : BottomSheetDialogFragment() { fun initView() { accounts = accountManager.getAccountsByType(AuthConstants.DEFAULT_ACCOUNT_TYPE) lifecycleScope.launch { - if (accounts.isEmpty()) { - addGoogleAccount() - } else { - filterAccountsLogin({ - prepareMultiSignIn(it) - }, { accountName, permitted -> - autoSingleSignIn(accountName, permitted) - }) + runCatching { + if (accounts.isEmpty()) { + addGoogleAccount() + } else { + filterAccountsLogin({ + prepareMultiSignIn(it) + }, { accountName, permitted -> + autoSingleSignIn(accountName, permitted) + }) + } + }.onFailure { + errorResult() } } } @@ -292,8 +298,10 @@ class AssistedSignInFragment : BottomSheetDialogFragment() { } override fun onDismiss(dialog: DialogInterface) { - cancelLogin() - errorResult(Status.CANCELED) + if (!signInBack) { + cancelLogin() + errorResult(Status.CANCELED) + } super.onDismiss(dialog) } @@ -315,7 +323,7 @@ class AssistedSignInFragment : BottomSheetDialogFragment() { isSigningIn = true delay(3000) runCatching { - val googleSignInAccount = withContext(Dispatchers.IO) { + val (_, googleSignInAccount) = withContext(Dispatchers.IO) { performSignIn(requireContext(), clientPackageName, options, lastChooseAccount!!, true, beginSignInRequest.googleIdTokenRequestOptions.nonce) } loginResult(googleSignInAccount) @@ -345,8 +353,12 @@ class AssistedSignInFragment : BottomSheetDialogFragment() { private fun loginResult(googleSignInAccount: GoogleSignInAccount?) { if (activity != null && activity is AssistedSignInActivity) { - val assistedSignInActivity = activity as AssistedSignInActivity - assistedSignInActivity.loginResult(googleSignInAccount) + signInBack = true + runCatching { + val assistedSignInActivity = activity as AssistedSignInActivity + AccountUtils.get(requireContext()).saveSelectedAccount(clientPackageName, googleSignInAccount?.account) + assistedSignInActivity.loginResult(googleSignInAccount) + } } activity?.finish() } diff --git a/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/AuthSignInActivity.kt b/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/AuthSignInActivity.kt index 3f4a5f5bc7..dd7044d1dc 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/AuthSignInActivity.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/AuthSignInActivity.kt @@ -179,7 +179,7 @@ class AuthSignInActivity : AppCompatActivity() { } private suspend fun signIn(account: Account) { - val googleSignInAccount = performSignIn(this, config?.packageName!!, config?.options, account, true, idNonce) + val (_, googleSignInAccount) = performSignIn(this, config?.packageName!!, config?.options, account, true, idNonce) if (googleSignInAccount != null) { finishResult(CommonStatusCodes.SUCCESS, account = account, googleSignInAccount = googleSignInAccount) } else { diff --git a/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/AuthSignInService.kt b/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/AuthSignInService.kt index 672bb27e40..104a6b658f 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/AuthSignInService.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/AuthSignInService.kt @@ -39,6 +39,7 @@ import com.google.android.gms.common.internal.GetServiceRequest import com.google.android.gms.common.internal.IGmsCallbacks import org.microg.gms.BaseService import org.microg.gms.auth.AuthPrefs +import org.microg.gms.common.AccountUtils import org.microg.gms.common.GmsService import org.microg.gms.common.PackageUtils import org.microg.gms.games.GAMES_PACKAGE_NAME @@ -95,7 +96,7 @@ class AuthSignInServiceImpl( Log.d(TAG, "silentSignIn: account -> ${account?.name}") if (account != null && options?.isForceCodeForRefreshToken != true) { if (getOAuthManager(context, packageName, options, account).isPermitted || AuthPrefs.isTrustGooglePermitted(context)) { - val googleSignInAccount = performSignIn(context, packageName, options, account) + val (_, googleSignInAccount) = performSignIn(context, packageName, options, account) if (googleSignInAccount != null) { sendResult(googleSignInAccount, Status(CommonStatusCodes.SUCCESS)) } else { @@ -120,14 +121,16 @@ class AuthSignInServiceImpl( try { val account = account ?: options?.account ?: SignInConfigurationService.getDefaultAccount(context, packageName) if (account != null) { - val defaultOptions = SignInConfigurationService.getDefaultOptions(context, packageName) - Log.d(TAG, "$packageName:signOut defaultOptions:($defaultOptions)") - performSignOut(context, packageName, defaultOptions ?: options, account) + SignInConfigurationService.getAuthOptions(context, packageName).forEach { + Log.d(TAG, "$packageName:signOut authOption:($it)") + performSignOut(context, packageName, it, account) + } } if (options?.scopes?.any { it.scopeUri.contains(Scopes.GAMES) } == true) { GamesConfigurationService.setDefaultAccount(context, packageName, null) } - SignInConfigurationService.setDefaultSignInInfo(context, packageName, null, null) + AccountUtils.get(context).removeSelectedAccount(packageName) + SignInConfigurationService.setAuthInfo(context, packageName, null, null) runCatching { callbacks.onSignOut(Status.SUCCESS) } } catch (e: Exception) { Log.w(TAG, e) @@ -160,7 +163,7 @@ class AuthSignInServiceImpl( authManager.invalidateAuthToken(token) authManager.isPermitted = false } - SignInConfigurationService.setDefaultSignInInfo(context, packageName, account, options?.toJson()) + SignInConfigurationService.setAuthInfo(context, packageName, account, options?.toJson()) runCatching { callbacks.onRevokeAccess(Status.SUCCESS) } } catch (e: Exception) { Log.w(TAG, e) diff --git a/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/SignInConfigurationService.kt b/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/SignInConfigurationService.kt index 5cc24f7994..8035f323fc 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/SignInConfigurationService.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/SignInConfigurationService.kt @@ -54,7 +54,7 @@ class SignInConfigurationService : Service() { val packageName = msg.data?.getString(MSG_DATA_PACKAGE_NAME) val account = msg.data?.getParcelable(MSG_DATA_ACCOUNT) val googleSignInOptions = msg.data?.getString(MSG_DATA_SIGN_IN_OPTIONS) - packageName?.let { setDefaultSignInInfo(it, account, googleSignInOptions) } + packageName?.let { setAuthInfo(it, account, googleSignInOptions) } bundleOf( MSG_DATA_PACKAGE_NAME to packageName, MSG_DATA_ACCOUNT to account, @@ -64,10 +64,10 @@ class SignInConfigurationService : Service() { MSG_GET_DEFAULT_OPTIONS -> { val packageName = msg.data?.getString(MSG_DATA_PACKAGE_NAME) - val googleSignInOptions = packageName?.let { getDefaultOptions(it) } + val googleSignInOptions = packageName?.let { getAuthOptions(it) } bundleOf( MSG_DATA_PACKAGE_NAME to packageName, - MSG_DATA_SIGN_IN_OPTIONS to googleSignInOptions + MSG_DATA_SIGN_IN_OPTIONS to googleSignInOptions?.toTypedArray() ) } @@ -95,23 +95,32 @@ class SignInConfigurationService : Service() { return null } - private fun getDefaultOptions(packageName: String): String? { - val data = preferences.getString(DEFAULT_SIGN_IN_OPTIONS_PREFIX + getPackageNameSuffix(packageName), null) - if (data.isNullOrBlank()) return null + private fun getAuthOptions(packageName: String): Set? { + val data = preferences.getStringSet(DEFAULT_SIGN_IN_OPTIONS_PREFIX + getPackageNameSuffix(packageName), null) + if (data.isNullOrEmpty()) return null return data } - private fun setDefaultSignInInfo(packageName: String, account: Account?, optionsJson: String?) { + private fun setAuthInfo(packageName: String, account: Account?, optionsJson: String?) { val editor: SharedPreferences.Editor = preferences.edit() + val accountPrefix = DEFAULT_ACCOUNT_PREFIX + getPackageNameSuffix(packageName) + val optionsPrefix = DEFAULT_SIGN_IN_OPTIONS_PREFIX + getPackageNameSuffix(packageName) if (account == null || account.name == AuthConstants.DEFAULT_ACCOUNT) { - editor.remove(DEFAULT_ACCOUNT_PREFIX + getPackageNameSuffix(packageName)) + editor.remove(accountPrefix) + editor.remove(optionsPrefix) } else { - editor.putString(DEFAULT_ACCOUNT_PREFIX + getPackageNameSuffix(packageName), account.name) + editor.putString(accountPrefix, account.name) } - if (optionsJson == null) { - editor.remove(DEFAULT_SIGN_IN_OPTIONS_PREFIX + getPackageNameSuffix(packageName)) - } else { - editor.putString(DEFAULT_SIGN_IN_OPTIONS_PREFIX + getPackageNameSuffix(packageName), optionsJson) + if (optionsJson != null) { + val oldOptions = runCatching { preferences.getString(optionsPrefix, null) }.getOrNull() + if (oldOptions != null) { + editor.putStringSet(optionsPrefix, setOf(oldOptions, optionsJson)) + } else { + val savedOptions = preferences.getStringSet(optionsPrefix, emptySet()) ?: emptySet() + val newSet = HashSet(savedOptions) + newSet.add(optionsJson) + editor.putStringSet(optionsPrefix, newSet) + } } editor.apply() } @@ -156,16 +165,16 @@ class SignInConfigurationService : Service() { }).data?.getParcelable(MSG_DATA_ACCOUNT) } - suspend fun getDefaultOptions(context: Context, packageName: String): GoogleSignInOptions? { + suspend fun getAuthOptions(context: Context, packageName: String): Set { return singleRequest(context, Message.obtain().apply { what = MSG_GET_DEFAULT_OPTIONS data = bundleOf( MSG_DATA_PACKAGE_NAME to packageName ) - }).data?.getString(MSG_DATA_SIGN_IN_OPTIONS)?.let { GoogleSignInOptions.fromJson(it) } + }).data?.getStringArray(MSG_DATA_SIGN_IN_OPTIONS)?.map { GoogleSignInOptions.fromJson(it) }?.toSet() ?: emptySet() } - suspend fun setDefaultSignInInfo(context: Context, packageName: String, account: Account?, optionsJson: String?) { + suspend fun setAuthInfo(context: Context, packageName: String, account: Account?, optionsJson: String?) { singleRequest(context, Message.obtain().apply { what = MSG_SET_DEFAULT_SIGN_IN_INFO data = bundleOf( diff --git a/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/extensions.kt b/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/extensions.kt index a3a2ae23a0..76de77e71f 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/extensions.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/extensions.kt @@ -107,7 +107,7 @@ suspend fun checkAccountAuthStatus(context: Context, packageName: String, scopeL return withContext(Dispatchers.IO) { authManager.requestAuth(true) }.auth != null } -suspend fun performSignIn(context: Context, packageName: String, options: GoogleSignInOptions?, account: Account, permitted: Boolean = false, idNonce: String? = null): GoogleSignInAccount? { +suspend fun performSignIn(context: Context, packageName: String, options: GoogleSignInOptions?, account: Account, permitted: Boolean = false, idNonce: String? = null): Pair { val authManager = getOAuthManager(context, packageName, options, account) val authResponse = withContext(Dispatchers.IO) { if (options?.includeUnacceptableScope == true || !permitted) { @@ -119,9 +119,9 @@ suspend fun performSignIn(context: Context, packageName: String, options: Google var consentResult:String ?= null if ("remote_consent" == authResponse.issueAdvice && authResponse.resolutionDataBase64 != null){ consentResult = performConsentView(context, packageName, account, authResponse.resolutionDataBase64) - if (consentResult == null) return null + if (consentResult == null) return Pair(null, null) } else { - if (authResponse.auth == null) return null + if (authResponse.auth == null) return Pair(null, null) } Log.d(TAG, "id token requested: ${options?.isIdTokenRequested == true}, serverClientId = ${options?.serverClientId}, permitted = ${authManager.isPermitted}") val idTokenResponse = getIdTokenManager(context, packageName, options, account)?.let { @@ -169,8 +169,8 @@ suspend fun performSignIn(context: Context, packageName: String, options: Google if (options?.includeGame == true) { GamesConfigurationService.setDefaultAccount(context, packageName, account) } - SignInConfigurationService.setDefaultSignInInfo(context, packageName, account, options?.toJson()) - return GoogleSignInAccount( + SignInConfigurationService.setAuthInfo(context, packageName, account, options?.toJson()) + val googleSignInAccount = GoogleSignInAccount( id, tokenId, account.name, @@ -183,6 +183,7 @@ suspend fun performSignIn(context: Context, packageName: String, options: Google givenName, familyName ) + return Pair(authResponse.auth, googleSignInAccount) } suspend fun performConsentView(context: Context, packageName: String, account: Account, dataBase64: String): String? { diff --git a/play-services-core/src/main/kotlin/org/microg/gms/common/PackageIntentOpWorker.kt b/play-services-core/src/main/kotlin/org/microg/gms/common/PackageIntentOpWorker.kt new file mode 100644 index 0000000000..56679a40bb --- /dev/null +++ b/play-services-core/src/main/kotlin/org/microg/gms/common/PackageIntentOpWorker.kt @@ -0,0 +1,70 @@ +/** + * SPDX-FileCopyrightText: 2025 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.common + +import android.content.Context +import android.util.Log +import androidx.work.CoroutineWorker +import androidx.work.WorkerParameters +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.microg.gms.auth.signin.SignInConfigurationService +import org.microg.gms.auth.signin.performSignOut +import org.microg.gms.gcm.GcmDatabase +import org.microg.gms.gcm.PushRegisterManager + +class PackageIntentOpWorker( + val appContext: Context, + params: WorkerParameters +) : CoroutineWorker(appContext, params) { + + companion object { + private const val TAG = "PackageIntentOpWorker" + const val PACKAGE_NAME = "packageName" + } + + override suspend fun doWork(): Result { + val packageName = inputData.getString(PACKAGE_NAME) ?: return Result.failure() + Log.d(TAG, "doWork: $packageName clearing.") + + clearGcmData(packageName) + clearAuthInfo(packageName) + + Log.d(TAG, "doWork: $packageName cleared.") + return Result.success() + } + + private suspend fun clearGcmData(packageName: String) = withContext(Dispatchers.IO) { + val database = GcmDatabase(appContext) + val app = database.getApp(packageName) + if (app != null) { + val registrations = database.getRegistrationsByApp(packageName) + var deletedAll = true + for (registration in registrations) { + deletedAll = deletedAll and (PushRegisterManager.unregister(appContext, registration.packageName, registration.signature, null, null).deleted != null) + } + if (deletedAll) { + database.removeApp(packageName) + } + database.close() + } else { + database.close() + } + } + + private suspend fun clearAuthInfo(packageName: String) = withContext(Dispatchers.IO) { + val authOptions = SignInConfigurationService.getAuthOptions(appContext, packageName) + val authAccount = SignInConfigurationService.getDefaultAccount(appContext, packageName) + if (authOptions.isNotEmpty() && authAccount != null) { + authOptions.forEach { + Log.d(TAG, "$packageName:clear authAccount: ${authAccount.name} authOption:($it)") + performSignOut(appContext, packageName, it, authAccount) + } + } + SignInConfigurationService.setAuthInfo(appContext, packageName, null, null) + AccountUtils.get(appContext).removeSelectedAccount(packageName) + } +} \ No newline at end of file diff --git a/play-services-core/src/main/kotlin/org/microg/gms/common/PersistentTrustedReceiver.kt b/play-services-core/src/main/kotlin/org/microg/gms/common/PersistentTrustedReceiver.kt new file mode 100644 index 0000000000..62631912ba --- /dev/null +++ b/play-services-core/src/main/kotlin/org/microg/gms/common/PersistentTrustedReceiver.kt @@ -0,0 +1,44 @@ +/** + * SPDX-FileCopyrightText: 2025 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.common + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.util.Log +import androidx.work.Data +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.WorkManager + +class PersistentTrustedReceiver : BroadcastReceiver() { + + companion object { + private const val TAG = "TrustedReceiver" + } + + override fun onReceive(context: Context, intent: Intent?) { + Log.d(TAG, "Package changed: $intent") + val action = intent?.action ?: return + val pkg = intent.data?.schemeSpecificPart ?: return + + if ((Intent.ACTION_PACKAGE_REMOVED.contains(action) + && intent.getBooleanExtra(Intent.EXTRA_DATA_REMOVED, false) + && !intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) + || Intent.ACTION_PACKAGE_FULLY_REMOVED.contains(action) + || Intent.ACTION_PACKAGE_DATA_CLEARED.contains(action) + ) { + Log.d(TAG, "Package removed or data cleared: $pkg") + val data = Data.Builder() + .putString(PackageIntentOpWorker.PACKAGE_NAME, pkg) + .build() + val request = OneTimeWorkRequestBuilder() + .setInputData(data) + .build() + WorkManager.getInstance(context).enqueue(request) + } + } + +} \ No newline at end of file diff --git a/play-services-core/src/main/kotlin/org/microg/gms/gcm/GcmInGmsService.kt b/play-services-core/src/main/kotlin/org/microg/gms/gcm/GcmInGmsService.kt index 8304c24463..36b0c36994 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/gcm/GcmInGmsService.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/gcm/GcmInGmsService.kt @@ -155,7 +155,8 @@ class GcmInGmsService : LifecycleService() { Log.d(TAG, "start handle gcm message") intent.extras?.let { notifyVerificationInfo(it) } } - ACTION_GCM_REGISTERED -> { + ACTION_GCM_REGISTER_ALL_ACCOUNTS, + ACTION_GCM_CONNECTED -> { updateLocalAccountGroups() } ACTION_GCM_REGISTER_ACCOUNT -> { @@ -370,6 +371,10 @@ class GcmInGmsService : LifecycleService() { completeRegisterRequest(context, gcmDatabase, request).getString(GcmConstants.EXTRA_REGISTRATION_ID) } Log.d(TAG, "GCM IN GMS regId: $regId") + if (regId == null) { + Log.w(TAG, "registerGcmInGms reg id is null") + return + } val sharedPreferencesEditor = sp?.edit() sharedPreferencesEditor?.putLong(KEY_GCM_ANDROID_ID, LastCheckinInfo.read(context).androidId) sharedPreferencesEditor?.putString(KEY_GCM_REG_ID, regId) @@ -510,4 +515,4 @@ class GcmRegistrationReceiver : WakefulBroadcastReceiver() { } ForegroundServiceContext(context).startService(callIntent) } -} \ No newline at end of file +} diff --git a/play-services-core/src/main/kotlin/org/microg/gms/gcm/extensions.kt b/play-services-core/src/main/kotlin/org/microg/gms/gcm/extensions.kt index 428a9bdd7a..8491604554 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/gcm/extensions.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/gcm/extensions.kt @@ -12,8 +12,9 @@ import okhttp3.OkHttpClient import okhttp3.Response const val ACTION_GCM_RECONNECT = "org.microg.gms.gcm.RECONNECT" -const val ACTION_GCM_REGISTERED = "org.microg.gms.gcm.REGISTERED" +const val ACTION_GCM_CONNECTED = "org.microg.gms.gcm.CONNECTED" const val ACTION_GCM_REGISTER_ACCOUNT = "org.microg.gms.gcm.REGISTER_ACCOUNT" +const val ACTION_GCM_REGISTER_ALL_ACCOUNTS = "org.microg.gms.gcm.REGISTER_ALL_ACCOUNTS" const val ACTION_GCM_NOTIFY_COMPLETE = "org.microg.gms.gcm.NOTIFY_COMPLETE" const val KEY_GCM_REGISTER_ACCOUNT_NAME = "register_account_name" const val EXTRA_NOTIFICATION_ACCOUNT = "notification_account" @@ -44,4 +45,4 @@ inline fun createGrpcClient( .minMessageToCompress(minMessageToCompress) .build() return grpcClient.create(S::class) -} \ No newline at end of file +} diff --git a/play-services-core/src/main/kotlin/org/microg/gms/phenotype/PhenotypeService.kt b/play-services-core/src/main/kotlin/org/microg/gms/phenotype/PhenotypeService.kt index a767ba19fd..fab3e4638c 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/phenotype/PhenotypeService.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/phenotype/PhenotypeService.kt @@ -70,6 +70,11 @@ private val CONFIGURATION_OPTIONS = mapOf( Flag("45661535", encodeSupportedLanguageList(), 0), Flag("45700179", encodeSupportedLanguageList(), 0) ), + "gmail_android.user#com.google.android.gm" to arrayOf( + Flag("45624002", true, 0), + Flag("45668769", true, 0), + Flag("45633067", true, 0), + ), ) class PhenotypeServiceImpl(val packageName: String?) : IPhenotypeService.Stub() { diff --git a/play-services-core/src/main/kotlin/org/microg/gms/ui/AccountsFragment.kt b/play-services-core/src/main/kotlin/org/microg/gms/ui/AccountsFragment.kt index 4102e2c1c7..2be044bd99 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/ui/AccountsFragment.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/ui/AccountsFragment.kt @@ -26,7 +26,8 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.microg.gms.auth.AuthConstants import org.microg.gms.common.Constants -import org.microg.gms.gcm.ACTION_GCM_REGISTERED +import org.microg.gms.gcm.ACTION_GCM_CONNECTED +import org.microg.gms.gcm.ACTION_GCM_REGISTER_ALL_ACCOUNTS import org.microg.gms.people.DatabaseHelper import org.microg.gms.people.PeopleManager import org.microg.gms.settings.SettingsContract @@ -65,7 +66,7 @@ class AccountsFragment : PreferenceFragmentCompat() { }).also { it.isCircular = true } else null private fun registerGcmInGms() { - Intent(ACTION_GCM_REGISTERED).apply { + Intent(ACTION_GCM_REGISTER_ALL_ACCOUNTS).apply { `package` = Constants.GMS_PACKAGE_NAME }.let { requireContext().sendBroadcast(it) } } 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..f3c1a1c1d1 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 allowRequests: 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 + allowRequests = preferenceScreen.findPreference("pref_device_attestation_app_allow_requests") ?: allowRequests + allowRequests.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 = summaries.isEmpty() && piContent == null + allowRequests.isChecked = piContent?.allowed == true } } 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..22e6e1e010 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,13 +232,14 @@ 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() } - apps.map { app -> + apps.sortedByDescending { it.second }.map { app -> app to context.packageManager.getApplicationInfoIfExists(app.first) }.mapNotNull { (app, info) -> if (info == null) null else app to info 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/navigation/nav_settings.xml b/play-services-core/src/main/res/navigation/nav_settings.xml index 4e202752d1..7aacdbf48f 100644 --- a/play-services-core/src/main/res/navigation/nav_settings.xml +++ b/play-services-core/src/main/res/navigation/nav_settings.xml @@ -125,7 +125,7 @@ + android:label="@string/prefcat_device_attestation_apps_title"> @@ -156,7 +156,7 @@ 自定义:%s 自动:%s 系统:%s + 允许请求 + 允许应用程序请求设备身份验证 "测试 SafetyNet 认证" "Google SafetyNet 是一套设备认证系统,旨在确认设备具有适当安全性,并与 Android CTS 兼容。某些应用会出于安全考虑或是防篡改目的而使用 SafetyNet。 @@ -154,6 +156,7 @@ microG GmsCore 内置一套自由的 SafetyNet 实现,但是官方服务器要 选择配置信息 设备配置信息 使用 SafetyNet 的应用 + 使用设备认证的应用 清除近期的 SafetyNet 请求 最近使用于%1$s 评估类型 @@ -226,6 +229,7 @@ microG GmsCore 内置一套自由的 SafetyNet 实现,但是官方服务器要 添加和管理 Google 账号 读取Google服务配置 Google SafetyNet + 设备认证 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..42209aa75a 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 @@ -160,6 +160,7 @@ 執行中… 運作模式 使用 SafetyNet 的應用程式 + 使用設備認證的應用程式 清除最近的請求 原生 實機 @@ -196,6 +197,9 @@ 車用廠商通訊通道 存取您車輛的車廠專屬通道,以交換與車輛相關的專屬資訊 Google SafetyNet + 設備認證 + 允許請求 + 允許應用程式請求裝置身份驗證 啟用此功能後,驗證請求中將不包含裝置名稱,這可能允許未授權的裝置登入,但也可能導致不可預期的後果。 狀態 更多 diff --git a/play-services-core/src/main/res/values/strings.xml b/play-services-core/src/main/res/values/strings.xml index e1d7ecf16a..c71f247fd3 100644 --- a/play-services-core/src/main/res/values/strings.xml +++ b/play-services-core/src/main/res/values/strings.xml @@ -96,6 +96,7 @@ Please set up a password, PIN, or pattern lock screen." Google device registration Cloud Messaging Google SafetyNet + Device Attestation Play Store services Work profile @@ -239,6 +240,9 @@ Please set up a password, PIN, or pattern lock screen." Operation mode DroidGuard execution is unsupported on this device. SafetyNet services may misbehave. Apps using SafetyNet + Apps using Device Attestation + Allow requests + Allow the app to request device attestation 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..131e2e4335 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,14 @@ tools:title="@tools:sample/lorem" app:allowDividerBelow="false" /> + + + android:title="@string/service_name_device_attestation" /> ()?.connectionInfo - if (SDK_INT >= 31 && connectionInfo != null) { - onWifiDetailsAvailable(listOf(connectionInfo.toWifiDetails())) + if (SDK_INT >= 31 && connectionInfo != null && connectionInfo.toWifiDetails() != null) { + onWifiDetailsAvailable(listOfNotNull(connectionInfo.toWifiDetails())) } else if (currentLocalMovingWifi != null && connectionInfo?.bssid == currentLocalMovingWifi.macAddress) { onWifiDetailsAvailable(listOf(currentLocalMovingWifi.copy(timestamp = System.currentTimeMillis()))) } else { @@ -205,7 +207,7 @@ class NetworkLocationService : LifecycleService(), WifiDetailsCallback, CellDeta override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { if (intent?.action == ACTION_NETWORK_LOCATION_SERVICE) { handler.post { - val pendingIntent = intent.getParcelableExtra(EXTRA_PENDING_INTENT) ?: return@post + val pendingIntent = IntentCompat.getParcelableExtra(intent, EXTRA_PENDING_INTENT, PendingIntent::class.java) ?: return@post val enable = intent.getBooleanExtra(EXTRA_ENABLE, false) if (enable) { val intervalMillis = intent.getLongExtra(EXTRA_INTERVAL_MILLIS, -1L) @@ -213,7 +215,7 @@ class NetworkLocationService : LifecycleService(), WifiDetailsCallback, CellDeta var forceNow = intent.getBooleanExtra(EXTRA_FORCE_NOW, false) val lowPower = intent.getBooleanExtra(EXTRA_LOW_POWER, true) val bypass = intent.getBooleanExtra(EXTRA_BYPASS, false) - val workSource = intent.getParcelableExtra(EXTRA_WORK_SOURCE) ?: WorkSource() + val workSource = IntentCompat.getParcelableExtra(intent, EXTRA_WORK_SOURCE, WorkSource::class.java) ?: WorkSource() synchronized(activeRequests) { if (activeRequests.any { it.pendingIntent == pendingIntent }) { forceNow = false @@ -231,7 +233,7 @@ class NetworkLocationService : LifecycleService(), WifiDetailsCallback, CellDeta } } else if (intent?.action == ACTION_NETWORK_IMPORT_EXPORT) { handler.post { - val callback = intent.getParcelableExtra(EXTRA_MESSENGER) + val callback = IntentCompat.getParcelableExtra(intent, EXTRA_MESSENGER, Messenger::class.java) val replyWhat = intent.getIntExtra(EXTRA_REPLY_WHAT, 0) when (intent.getStringExtra(EXTRA_DIRECTION)) { DIRECTION_EXPORT -> { @@ -247,7 +249,7 @@ class NetworkLocationService : LifecycleService(), WifiDetailsCallback, CellDeta }) } DIRECTION_IMPORT -> { - val uri = intent.getParcelableExtra(EXTRA_URI) + val uri = IntentCompat.getParcelableExtra(intent, EXTRA_URI, Uri::class.java) val counter = uri?.let { database.importLearned(it) } ?: 0 callback?.send(Message.obtain().apply { what = replyWhat @@ -266,7 +268,7 @@ class NetworkLocationService : LifecycleService(), WifiDetailsCallback, CellDeta } override fun onDestroy() { - handlerThread.stop() + handlerThread.quitSafely() wifiDetailsSource?.disable() wifiDetailsSource = null cellDetailsSource?.disable() diff --git a/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/ichnaea/IchnaeaServiceClient.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/ichnaea/IchnaeaServiceClient.kt index a4471a29d3..2ecc435803 100644 --- a/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/ichnaea/IchnaeaServiceClient.kt +++ b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/ichnaea/IchnaeaServiceClient.kt @@ -6,6 +6,7 @@ package org.microg.gms.location.network.ichnaea import android.content.Context +import android.content.pm.PackageManager import android.location.Location import android.net.Uri import android.os.Bundle @@ -41,6 +42,10 @@ class IchnaeaServiceClient(private val context: Context) { private val cache = LruCache(REQUEST_CACHE_SIZE) private val start = SystemClock.elapsedRealtime() + private val hasTelephony by lazy { + context.packageManager.hasSystemFeature(PackageManager.FEATURE_TELEPHONY) + } + private fun GeolocateRequest.hash(): ByteArray? { if (cellTowers.isNullOrEmpty() && (wifiAccessPoints?.size ?: 0) < 3 || bluetoothBeacons?.isNotEmpty() == true) return null val minAge = min( @@ -74,7 +79,7 @@ class IchnaeaServiceClient(private val context: Context) { suspend fun retrieveMultiWifiLocation(wifis: List, rawHandler: ((WifiDetails, Location) -> Unit)? = null): Location? = geoLocate( GeolocateRequest( - considerIp = false, + considerIp = !hasTelephony, wifiAccessPoints = wifis.filter { isRequestable(it) }.map(WifiDetails::toWifiAccessPoint), fallbacks = Fallback(lacf = false, ipf = false) ), diff --git a/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/wifi/extensions.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/wifi/extensions.kt index f991762bf0..d0be1c7376 100644 --- a/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/wifi/extensions.kt +++ b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/wifi/extensions.kt @@ -23,15 +23,17 @@ internal fun ScanResult.toWifiDetails(): WifiDetails = WifiDetails( ) @RequiresApi(31) -internal fun WifiInfo.toWifiDetails(): WifiDetails = WifiDetails( - macAddress = bssid, - ssid = ssid.takeIf { it != WifiManager.UNKNOWN_SSID && it.startsWith("\"") && it.endsWith("\"") } - ?.let { it.substring(1, it.length - 1) }, - timestamp = System.currentTimeMillis(), - frequency = frequency, - signalStrength = rssi, - open = currentSecurityType == WifiInfo.SECURITY_TYPE_OPEN -) +internal fun WifiInfo.toWifiDetails(): WifiDetails? { + return WifiDetails( + macAddress = bssid ?: return null, + ssid = ssid?.removeSurrounding("\"") + ?.takeIf { it != WifiManager.UNKNOWN_SSID }, + timestamp = System.currentTimeMillis(), + frequency = frequency, + signalStrength = rssi, + open = currentSecurityType == WifiInfo.SECURITY_TYPE_OPEN + ) +} private const val BAND_24_GHZ_FIRST_CH_NUM = 1 private const val BAND_24_GHZ_LAST_CH_NUM = 14 diff --git a/play-services-maps/core/hms/src/main/kotlin/org/microg/gms/maps/hms/GoogleMap.kt b/play-services-maps/core/hms/src/main/kotlin/org/microg/gms/maps/hms/GoogleMap.kt index 0f239266f2..223fac9992 100644 --- a/play-services-maps/core/hms/src/main/kotlin/org/microg/gms/maps/hms/GoogleMap.kt +++ b/play-services-maps/core/hms/src/main/kotlin/org/microg/gms/maps/hms/GoogleMap.kt @@ -441,7 +441,7 @@ class GoogleMapImpl(private val context: Context, var options: GoogleMapOptions) } override fun getUiSettings(): IUiSettingsDelegate = - map?.uiSettings?.let { UiSettingsImpl(it, view) } ?: UiSettingsCache().also { + map?.uiSettings?.let { UiSettingsImpl(it, view) } ?: UiSettingsCache(view).also { internalOnInitializedCallbackList.add(it.getMapReadyCallback()) } @@ -661,13 +661,7 @@ class GoogleMapImpl(private val context: Context, var options: GoogleMapOptions) if (SDK_INT >= 26) { mapView?.let { it.parent?.onDescendantInvalidated(it, it) } } - map?.let { - val cameraPosition = it.cameraPosition - val tilt = cameraPosition.tilt - val bearing = cameraPosition.bearing - val useFast = tilt < 1f && (bearing % 360f < 1f || bearing % 360f > 359f) - projectionImpl?.updateProjectionState(it.projection, useFast) - } + map?.let { projectionImpl?.updateProjectionState(it.cameraPosition, it.projection) } cameraMoveListener?.onCameraMove() cameraChangeListener?.onCameraChange(map?.cameraPosition?.toGms()) } catch (e: Exception) { @@ -784,7 +778,12 @@ class GoogleMapImpl(private val context: Context, var options: GoogleMapOptions) map.setOnCameraMoveListener { Log.d(TAG, "initMap: onCameraMove: ") try { + if (SDK_INT >= 26) { + mapView?.let { it.parent?.onDescendantInvalidated(it, it) } + } + map.let { projectionImpl?.updateProjectionState(it.cameraPosition, it.projection) } cameraMoveListener?.onCameraMove() + cameraChangeListener?.onCameraChange(map.cameraPosition?.toGms()) } catch (e: Exception) { Log.w(TAG, e) } diff --git a/play-services-maps/core/hms/src/main/kotlin/org/microg/gms/maps/hms/Projection.kt b/play-services-maps/core/hms/src/main/kotlin/org/microg/gms/maps/hms/Projection.kt index b054c95e21..a83370608d 100644 --- a/play-services-maps/core/hms/src/main/kotlin/org/microg/gms/maps/hms/Projection.kt +++ b/play-services-maps/core/hms/src/main/kotlin/org/microg/gms/maps/hms/Projection.kt @@ -15,6 +15,7 @@ import com.google.android.gms.maps.model.LatLng import com.google.android.gms.maps.model.LatLngBounds import com.google.android.gms.maps.model.VisibleRegion import com.huawei.hms.maps.Projection +import com.huawei.hms.maps.model.CameraPosition import org.microg.gms.maps.hms.utils.toGms import org.microg.gms.maps.hms.utils.toHms import kotlin.math.roundToInt @@ -38,11 +39,14 @@ class ProjectionImpl(private var projection: Projection, private var withoutTilt private var farRightX = farRight?.x ?: (farLeftX + 1) private var nearLeftY = nearLeft?.y ?: (farLeftY + 1) - fun updateProjectionState(newProjection: Projection, useFastMode: Boolean) { - Log.d(TAG, "updateProjectionState: useFastMode: $useFastMode") - projection = newProjection - visibleRegion = newProjection.visibleRegion - withoutTiltOrBearing = useFastMode + fun updateProjectionState(cameraPosition: CameraPosition, projection: Projection) { + val tilt = cameraPosition.tilt + val bearing = cameraPosition.bearing + val useFast = tilt < 1f && (bearing % 360f < 1f || bearing % 360f > 359f) + Log.d(TAG, "updateProjectionState: useFastMode: $useFast") + + visibleRegion = projection.visibleRegion + withoutTiltOrBearing = useFast farLeft = visibleRegion.farLeft?.let { projection.toScreenLocation(it) } farRight = visibleRegion.farRight?.let { projection.toScreenLocation(it) } diff --git a/play-services-maps/core/hms/src/main/kotlin/org/microg/gms/maps/hms/UiSettings.kt b/play-services-maps/core/hms/src/main/kotlin/org/microg/gms/maps/hms/UiSettings.kt index cb2f59492b..945b06e539 100644 --- a/play-services-maps/core/hms/src/main/kotlin/org/microg/gms/maps/hms/UiSettings.kt +++ b/play-services-maps/core/hms/src/main/kotlin/org/microg/gms/maps/hms/UiSettings.kt @@ -19,7 +19,20 @@ private const val TAG = "GmsMapsUiSettings" /** * This class "implements" unimplemented methods to avoid duplication in subclasses */ -abstract class AbstractUiSettings : IUiSettingsDelegate.Stub() { +abstract class AbstractUiSettings(rootView: ViewGroup) : IUiSettingsDelegate.Stub() { + + protected val mapUiController = MapUiController(rootView) + + init { + mapUiController.initUiStates( + mapOf( + MapUiElement.MyLocationButton to false, + MapUiElement.ZoomView to false, + MapUiElement.CompassView to false + ) + ) + } + override fun setZoomControlsEnabled(zoom: Boolean) { Log.d(TAG, "unimplemented Method: setZoomControlsEnabled") } @@ -66,22 +79,12 @@ abstract class AbstractUiSettings : IUiSettingsDelegate.Stub() { } } -class UiSettingsImpl(private val uiSettings: UiSettings, rootView: ViewGroup) : IUiSettingsDelegate.Stub() { - - private val mapUiController = MapUiController(rootView) +class UiSettingsImpl(private val uiSettings: UiSettings, rootView: ViewGroup) : AbstractUiSettings(rootView) { init { uiSettings.isZoomControlsEnabled = false uiSettings.isCompassEnabled = false - uiSettings.isMapToolbarEnabled = false uiSettings.isMyLocationButtonEnabled = false - mapUiController.initUiStates( - mapOf( - MapUiElement.MyLocationButton to false, - MapUiElement.ZoomView to false, - MapUiElement.CompassView to false - ) - ) } override fun setZoomControlsEnabled(zoom: Boolean) { @@ -180,7 +183,7 @@ class UiSettingsImpl(private val uiSettings: UiSettings, rootView: ViewGroup) : } } -class UiSettingsCache : AbstractUiSettings() { +class UiSettingsCache(rootView: ViewGroup) : AbstractUiSettings(rootView) { private var compass: Boolean? = null private var scrollGestures: Boolean? = null @@ -300,15 +303,28 @@ class UiSettingsCache : AbstractUiSettings() { fun getMapReadyCallback(): OnMapReadyCallback = OnMapReadyCallback { map -> val uiSettings = map.uiSettings - compass?.let { uiSettings.isCompassEnabled = it } + uiSettings.isZoomControlsEnabled = false + uiSettings.isCompassEnabled = false + uiSettings.isMyLocationButtonEnabled = false + + compass?.let { + uiSettings.isCompassEnabled = it + mapUiController.setUiEnabled(MapUiElement.CompassView, it) + } scrollGestures?.let { uiSettings.isScrollGesturesEnabled = it } zoomGestures?.let { uiSettings.isZoomGesturesEnabled = it } tiltGestures?.let { uiSettings.isTiltGesturesEnabled = it } rotateGestures?.let { uiSettings.isRotateGesturesEnabled = it } isAllGesturesEnabled?.let { uiSettings.setAllGesturesEnabled(it) } - isZoomControlsEnabled?.let { uiSettings.isZoomControlsEnabled = it } - isMyLocationButtonEnabled?.let { uiSettings.isMyLocationButtonEnabled = it } + isZoomControlsEnabled?.let { + uiSettings.isZoomControlsEnabled = it + mapUiController.setUiEnabled(MapUiElement.ZoomView, it) + } + isMyLocationButtonEnabled?.let { + uiSettings.isMyLocationButtonEnabled = it + mapUiController.setUiEnabled(MapUiElement.MyLocationButton, it) + } isIndoorLevelPickerEnabled?.let { uiSettings.isIndoorLevelPickerEnabled = it } isMapToolbarEnabled?.let { uiSettings.isMapToolbarEnabled = it } isScrollGesturesEnabledDuringRotateOrZoom?.let { uiSettings.isScrollGesturesEnabledDuringRotateOrZoom = it } diff --git a/play-services-wearable/core/src/main/AndroidManifest.xml b/play-services-wearable/core/src/main/AndroidManifest.xml index 311528eaa7..e254e84af0 100644 --- a/play-services-wearable/core/src/main/AndroidManifest.xml +++ b/play-services-wearable/core/src/main/AndroidManifest.xml @@ -6,6 +6,8 @@ + + = android.os.Build.VERSION_CODES.S) { + if (context.checkSelfPermission(android.Manifest.permission.BLUETOOTH_CONNECT) + != android.content.pm.PackageManager.PERMISSION_GRANTED) { + Log.w(TAG, "BLUETOOTH_CONNECT permission not granted, skipping device scan"); + Thread.sleep(10000); + continue; + } + } + Set bondedDevices = adapter.getBondedDevices(); if (bondedDevices != null) { for (BluetoothDevice device : bondedDevices) { @@ -765,7 +775,7 @@ public void run() { Log.d(TAG, "Successfully connected via Bluetooth to " + device.getName()); // Create wearable connection wrapper - ConnectionConfiguration config = new ConnectionConfiguration(null, device.getAddress(), device.getName(), 3, true); + ConnectionConfiguration config = new ConnectionConfiguration(device.getName(), device.getAddress(), 3, 0, true); MessageHandler messageHandler = new MessageHandler(context, WearableImpl.this, config); BluetoothWearableConnection connection = new BluetoothWearableConnection(socket, messageHandler); @@ -787,7 +797,7 @@ public void run() { .id(localId) .name("Phone") .networkId(localId) - .peerAndroidId(localId) + .peerAndroidId(0L) .peerVersion(2) // Need at least version 2 for modern WearOS .build()) .build() diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableSettingsActivity.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableSettingsActivity.java index 6fa6b0d336..7dabc5abe7 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableSettingsActivity.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableSettingsActivity.java @@ -46,6 +46,15 @@ private void refreshList() { return; } + // Check BLUETOOTH_CONNECT permission for Android 12+ + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) { + if (checkSelfPermission(android.Manifest.permission.BLUETOOTH_CONNECT) + != android.content.pm.PackageManager.PERMISSION_GRANTED) { + emptyView.setText("Bluetooth permission not granted"); + return; + } + } + Set bondedDevices = adapter.getBondedDevices(); if (bondedDevices != null) { deviceList.addAll(bondedDevices); @@ -133,6 +142,7 @@ public WearableDeviceAdapter(android.content.Context context, ArrayList + + + + + + + + + + + + + + + diff --git a/vending-app/src/main/AndroidManifest.xml b/vending-app/src/main/AndroidManifest.xml index 6cb46bed4e..3bb7d80b28 100644 --- a/vending-app/src/main/AndroidManifest.xml +++ b/vending-app/src/main/AndroidManifest.xml @@ -299,6 +299,13 @@ + + + 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..8c6d0fab50 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 callerAppToIntegrityData(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.updateAppIntegrityContent(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,18 +428,27 @@ suspend fun updateExpressAuthTokenWrapper(context: Context, expressIntegritySess private suspend fun regenerateToken( context: Context, authToken: String, packageName: String, clientKey: ClientKey ): AuthTokenWrapper { + Log.d(TAG, "regenerateToken authToken:$authToken, packageName:$packageName, clientKey:$clientKey") try { - Log.d(TAG, "regenerateToken authToken:$authToken, packageName:$packageName, clientKey:$clientKey") - val droidGuardSessionTokenResponse = requestDroidGuardSessionToken(context, authToken) - - if (droidGuardSessionTokenResponse.tokenWrapper == null) { - throw RuntimeException("regenerateToken droidGuardSessionTokenResponse.tokenWrapper is Empty!") + val prefs = context.getSharedPreferences("droid_guard_token_session_id", Context.MODE_PRIVATE) + val droidGuardTokenSession = try { + val droidGuardSessionTokenResponse = requestDroidGuardSessionToken(context, authToken) + if (droidGuardSessionTokenResponse.tokenWrapper == null) { + throw RuntimeException("regenerateToken droidGuardSessionTokenResponse.tokenWrapper is Empty!") + } + val droidGuardTokenType = droidGuardSessionTokenResponse.tokenWrapper.tokenContent?.tokenType?.firstOrNull { it.type?.toInt() == 5 } + ?: throw RuntimeException("regenerateToken droidGuardTokenType is null!") + val sessionId = droidGuardTokenType.tokenSessionWrapper?.wrapper?.sessionContent?.session?.id + if (sessionId.isNullOrEmpty()) { + throw RuntimeException("regenerateToken sessionId is null") + } + sessionId.also { prefs.edit { putString(packageName, it) } } + } catch (e: Exception) { + Log.d(TAG, "regenerateToken: error ", e) + prefs.getString(packageName, null) } - val droidGuardTokenType = droidGuardSessionTokenResponse.tokenWrapper.tokenContent?.tokenType?.firstOrNull { it.type?.toInt() == 5 } - ?: throw RuntimeException("regenerateToken droidGuardTokenType is null!") - - val droidGuardTokenSession = droidGuardTokenType.tokenSessionWrapper?.wrapper?.sessionContent?.session?.id + Log.d(TAG, "regenerateToken: sessionId: $droidGuardTokenSession") if (droidGuardTokenSession.isNullOrEmpty()) { throw RuntimeException("regenerateToken droidGuardTokenSession is null") } diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AssetModuleService.kt b/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AssetModuleService.kt index efc6cb7a54..d8bb72eb2c 100644 --- a/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AssetModuleService.kt +++ b/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AssetModuleService.kt @@ -56,22 +56,32 @@ class AssetModuleServiceImpl( private val packageDownloadData: MutableMap ) : AbstractAssetModuleServiceImpl(context, lifecycle) { private val fileDescriptorMap = mutableMapOf() + private val lock = Any() private fun checkSessionValid(packageName: String, sessionId: Int) { + Log.d(TAG, "checkSessionValid: $packageName $sessionId ${packageDownloadData[packageName]?.sessionIds}") if (packageDownloadData[packageName]?.sessionIds?.values?.contains(sessionId) != true) { Log.w(TAG, "No active session with id $sessionId in $packageName") throw AssetPackException(AssetPackErrorCode.ACCESS_DENIED) } } - override fun getDefaultSessionId(packageName: String, moduleName: String): Int = + override fun getDefaultSessionId(packageName: String, moduleName: String): Int = synchronized(lock) { packageDownloadData[packageName]?.sessionIds?.get(moduleName) ?: 0 + } override suspend fun startDownload(params: StartDownloadParameters, packageName: String, callback: IAssetModuleServiceCallback?) { - if (packageDownloadData[packageName] == null || - packageDownloadData[packageName]?.packageName != packageName || - packageDownloadData[packageName]?.moduleNames?.intersect(params.moduleNames.toSet())?.isEmpty() == true) { - packageDownloadData[packageName] = httpClient.initAssetModuleData(context, packageName, accountManager, params.moduleNames, params.options) + val needInit = synchronized(lock) { + packageDownloadData[packageName] == null || + packageDownloadData[packageName]?.packageName != packageName || + packageDownloadData[packageName]?.moduleNames?.intersect(params.moduleNames.toSet())?.isEmpty() == true + } + + if (needInit) { + val newData = httpClient.initAssetModuleData(context, packageName, accountManager, params.moduleNames, params.options) + synchronized(lock) { + packageDownloadData[packageName] = packageDownloadData[packageName].merge(newData) + } if (packageDownloadData[packageName] == null) { throw AssetPackException(AssetPackErrorCode.API_NOT_AVAILABLE) } @@ -114,24 +124,26 @@ class AssetModuleServiceImpl( override suspend fun getSessionStates(params: GetSessionStatesParameters, packageName: String, callback: IAssetModuleServiceCallback?) { val listBundleData: MutableList = mutableListOf() - if (packageDownloadData[packageName] != null && packageDownloadData[packageName]?.moduleNames?.all { - packageDownloadData[packageName]?.getModuleData(it)?.status == AssetPackStatus.COMPLETED - } == true && params.installedAssetModules.isEmpty()) { - Log.d(TAG, "getSessionStates: resetAllModuleStatus: $listBundleData") - packageDownloadData[packageName]?.resetAllModuleStatus() - callback?.onGetSessionStates(listBundleData) - return - } + synchronized(lock) { + if (packageDownloadData[packageName] != null && packageDownloadData[packageName]?.moduleNames?.all { + packageDownloadData[packageName]?.getModuleData(it)?.status == AssetPackStatus.COMPLETED + } == true && params.installedAssetModules.isEmpty()) { + Log.d(TAG, "getSessionStates: resetAllModuleStatus: $listBundleData") + packageDownloadData[packageName]?.resetAllModuleStatus() + callback?.onGetSessionStates(listBundleData) + return + } - packageDownloadData[packageName]?.moduleNames?.forEach { moduleName -> - if (moduleName in params.installedAssetModules) return@forEach + packageDownloadData[packageName]?.moduleNames?.forEach { moduleName -> + if (moduleName in params.installedAssetModules) return@forEach - listBundleData.add(sendBroadcastForExistingFile(context, packageDownloadData[packageName]!!, moduleName, null, null)) + listBundleData.add(sendBroadcastForExistingFile(context, packageDownloadData[packageName]!!, moduleName, null, null)) - packageDownloadData[packageName]?.getModuleData(moduleName)?.chunks?.forEach { chunkData -> - val destination = chunkData.getChunkFile(context) - if (destination.exists() && destination.length() == chunkData.chunkBytesToDownload) { - sendBroadcastForExistingFile(context, packageDownloadData[packageName]!!, moduleName, chunkData, destination) + packageDownloadData[packageName]?.getModuleData(moduleName)?.chunks?.forEach { chunkData -> + val destination = chunkData.getChunkFile(context) + if (destination.exists() && destination.length() == chunkData.chunkBytesToDownload) { + sendBroadcastForExistingFile(context, packageDownloadData[packageName]!!, moduleName, chunkData, destination) + } } } } @@ -143,9 +155,11 @@ class AssetModuleServiceImpl( override suspend fun notifyChunkTransferred(params: NotifyChunkTransferredParameters, packageName: String, callback: IAssetModuleServiceCallback?) { checkSessionValid(packageName, params.sessionId) - val downLoadFile = context.getChunkFile(params.sessionId, params.moduleName, params.sliceId, params.chunkNumber) - fileDescriptorMap[downLoadFile]?.close() - fileDescriptorMap.remove(downLoadFile) + synchronized(lock) { + val downLoadFile = context.getChunkFile(params.sessionId, params.moduleName, params.sliceId, params.chunkNumber) + fileDescriptorMap[downLoadFile]?.close() + fileDescriptorMap.remove(downLoadFile) + } // TODO: Remove chunk after successful transfer of chunk or only with module? callback?.onNotifyChunkTransferred( bundleOf(BundleKeys.MODULE_NAME to params.moduleName) + @@ -159,8 +173,10 @@ class AssetModuleServiceImpl( override suspend fun notifyModuleCompleted(params: NotifyModuleCompletedParameters, packageName: String, callback: IAssetModuleServiceCallback?) { checkSessionValid(packageName, params.sessionId) - packageDownloadData[packageName]?.updateDownloadStatus(params.moduleName, AssetPackStatus.COMPLETED) - sendBroadcastForExistingFile(context, packageDownloadData[packageName]!!, params.moduleName, null, null) + synchronized(lock) { + packageDownloadData[packageName]?.updateDownloadStatus(params.moduleName, AssetPackStatus.COMPLETED) + sendBroadcastForExistingFile(context, packageDownloadData[packageName]!!, params.moduleName, null, null) + } val directory = context.getModuleDir(params.sessionId, params.moduleName) if (directory.exists()) { @@ -192,9 +208,11 @@ class AssetModuleServiceImpl( override suspend fun getChunkFileDescriptor(params: GetChunkFileDescriptorParameters, packageName: String, callback: IAssetModuleServiceCallback?) { checkSessionValid(packageName, params.sessionId) - val downLoadFile = context.getChunkFile(params.sessionId, params.moduleName, params.sliceId, params.chunkNumber) - val parcelFileDescriptor = ParcelFileDescriptor.open(downLoadFile, ParcelFileDescriptor.MODE_READ_ONLY).also { - fileDescriptorMap[downLoadFile] = it + val parcelFileDescriptor = synchronized(lock) { + val downLoadFile = context.getChunkFile(params.sessionId, params.moduleName, params.sliceId, params.chunkNumber) + ParcelFileDescriptor.open(downLoadFile, ParcelFileDescriptor.MODE_READ_ONLY).also { + fileDescriptorMap[downLoadFile] = it + } } Log.d(TAG, "getChunkFileDescriptor -> $parcelFileDescriptor") @@ -205,10 +223,17 @@ class AssetModuleServiceImpl( } override suspend fun requestDownloadInfo(params: RequestDownloadInfoParameters, packageName: String, callback: IAssetModuleServiceCallback?) { - if (packageDownloadData[packageName] == null || - packageDownloadData[packageName]?.packageName != packageName || - packageDownloadData[packageName]?.moduleNames?.intersect(params.moduleNames.toSet())?.isEmpty() == true) { - packageDownloadData[packageName] = httpClient.initAssetModuleData(context, packageName, accountManager, params.moduleNames, params.options) + val needInit = synchronized(lock) { + packageDownloadData[packageName] == null || + packageDownloadData[packageName]?.packageName != packageName || + packageDownloadData[packageName]?.moduleNames?.intersect(params.moduleNames.toSet())?.isEmpty() == true + } + + if (needInit) { + val newData = httpClient.initAssetModuleData(context, packageName, accountManager, params.moduleNames, params.options) + synchronized(lock) { + packageDownloadData[packageName] = packageDownloadData[packageName].merge(newData) + } if (packageDownloadData[packageName] == null) { throw AssetPackException(AssetPackErrorCode.API_NOT_AVAILABLE) } 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..276a1ebdc9 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.callerAppToIntegrityData 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.updateAppIntegrityContent 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 = callerAppToIntegrityData(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?.updateAppIntegrityContent(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?.updateAppIntegrityContent(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 = callerAppToIntegrityData(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?.updateAppIntegrityContent(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?.updateAppIntegrityContent(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/inappreviewdialog/InAppReviewActivity.kt b/vending-app/src/main/kotlin/com/google/android/finsky/inappreviewdialog/InAppReviewActivity.kt new file mode 100644 index 0000000000..93f40ad00c --- /dev/null +++ b/vending-app/src/main/kotlin/com/google/android/finsky/inappreviewdialog/InAppReviewActivity.kt @@ -0,0 +1,20 @@ +/** + * SPDX-FileCopyrightText: 2025 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.finsky.inappreviewdialog + +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity + +class InAppReviewActivity: AppCompatActivity() { + companion object { + const val CALLING_PACKAGE = "calling_package" + } + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setResult(RESULT_OK) + finish() + } +} \ No newline at end of file diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/inappreviewservice/InAppReviewService.kt b/vending-app/src/main/kotlin/com/google/android/finsky/inappreviewservice/InAppReviewService.kt index 543f218af0..49248ddf75 100644 --- a/vending-app/src/main/kotlin/com/google/android/finsky/inappreviewservice/InAppReviewService.kt +++ b/vending-app/src/main/kotlin/com/google/android/finsky/inappreviewservice/InAppReviewService.kt @@ -5,13 +5,16 @@ package com.google.android.finsky.inappreviewservice +import android.app.PendingIntent.FLAG_UPDATE_CURRENT import android.content.Context import android.content.Intent import android.os.Bundle import android.os.IBinder import android.os.Parcel import android.util.Log +import androidx.core.app.PendingIntentCompat import androidx.lifecycle.LifecycleService +import com.google.android.finsky.inappreviewdialog.InAppReviewActivity import com.google.android.play.core.inappreview.protocol.IInAppReviewService import com.google.android.play.core.inappreview.protocol.IInAppReviewServiceCallback import org.microg.gms.utils.warnOnTransactionIssues @@ -35,10 +38,22 @@ class InAppReviewService : LifecycleService() { class InAppReviewServiceImpl(val context: Context) : IInAppReviewService.Stub() { override fun requestInAppReview(packageName: String?, bundle: Bundle?, callback: IInAppReviewServiceCallback?) { - bundle?.keySet() Log.d(TAG, "requestInAppReview: packageName: $packageName bundle:$bundle") - callback?.onResult(Bundle.EMPTY) + if (packageName == null) return + val pendingIntent = Intent(context, InAppReviewActivity::class.java).apply { + putExtra(InAppReviewActivity.CALLING_PACKAGE, packageName) + }.let { + PendingIntentCompat.getActivity(context, 0, it, FLAG_UPDATE_CURRENT, false) + } + val bundle = Bundle() + bundle.putBoolean("is_review_no_op", false) + bundle.putParcelable("confirmation_intent", pendingIntent) + try { + callback?.onResult(bundle) + } catch (e: Exception) { + Log.w(TAG, "Exception on in-app review service", e) + } } override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int) = warnOnTransactionIssues(code, reply, flags, TAG) { super.onTransact(code, data, reply, flags) } -} \ No newline at end of file +} diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/installservice/DevTriggeredUpdateService.kt b/vending-app/src/main/kotlin/com/google/android/finsky/installservice/DevTriggeredUpdateService.kt index 193ba46662..cab4f9047d 100644 --- a/vending-app/src/main/kotlin/com/google/android/finsky/installservice/DevTriggeredUpdateService.kt +++ b/vending-app/src/main/kotlin/com/google/android/finsky/installservice/DevTriggeredUpdateService.kt @@ -40,7 +40,7 @@ class DevTriggeredUpdateServiceImpl(private val context: Context, override val l override fun requestUpdateInfo(packageName: String?, bundle: Bundle?, callback: IAppUpdateServiceCallback?) { bundle?.keySet() Log.d(TAG, "requestUpdateInfo: packageName: $packageName bundle: $bundle") - callback?.onUpdateResult(bundleOf("error.code" to 0)) + callback?.onUpdateResult(bundleOf("error.code" to 0, "update.availability" to 1)) } override fun completeUpdate(packageName: String?, bundle: Bundle?, callback: IAppUpdateServiceCallback?) { 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..d24283b978 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.callerAppToIntegrityData 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.updateAppIntegrityContent 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 integrityData: 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.") + } + integrityData = callerAppToIntegrityData(context, packageName) + if (integrityData?.allowed != true) { + throw StandardIntegrityException(IntegrityErrorCode.INTERNAL_ERROR, "Not allowed to request integrity token.") + } + 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,19 @@ 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 + if (integrityResponse.integrityResponseError?.error != null) { + throw StandardIntegrityException(IntegrityErrorCode.INTERNAL_ERROR, integrityResponse.integrityResponseError.error) + } + throw StandardIntegrityException(IntegrityErrorCode.INTERNAL_ERROR, "No token in response.") } Log.d(TAG, "requestIntegrityToken integrityToken: $integrityToken") + integrityData?.updateAppIntegrityContent(context, System.currentTimeMillis(), "Delivered encrypted integrity token.", true) callback.onSuccess(packageName, integrityToken) }.onFailure { Log.w(TAG, "requestIntegrityToken has exception: ", it) - callback.onError(packageName, IntegrityErrorCode.INTERNAL_ERROR, it.message ?: "Exception") + integrityData?.updateAppIntegrityContent(context, System.currentTimeMillis(), "Integrity check failed: ${it.message}") + callback.onError(integrityData?.packageName, IntegrityErrorCode.INTERNAL_ERROR, it.message ?: "Exception") } } }