From a2fd6a89913e2889f8adade3b402d59c1ce42fd1 Mon Sep 17 00:00:00 2001 From: inthewaves Date: Mon, 9 Jun 2025 00:24:22 -0700 Subject: [PATCH] allow GMS remote credential service to be used Currently, GMS allows passkeys via hardware keys ("remote credentials" from a "remote device") by its RemoteService, but this is being filtered as sandboxed Google Play doesn't result in system services. The RemoteService is also expected to be set as an OEM config value in frameworks-res (config_defaultCredentialManagerHybridService). This will make it so that external devices (e.g. FIDO2 with NFC / USB) can be used with sandboxed Google Play to register hardware keys in Vanadium / Chrome, along with improving hardware key functionality in other apps that make credential requests with android.credentials.CredentialManager (Context.CREDENTIAL_SERVICE) directly instead of contacting Google Play services. It still comes with the caveat that for some apps, Google has to be enabled as a credential service under Settings > Passwords, passkeys & accounts, as sandboxed GMS services are not system credential providers and have to be explicitly enabled as user credential providers. Apps that contact GMS directly for credentials / passkeys still work without needing to enable Google as a credential service (e.g., Vanadium / Chrome will fall back to contacting Play services directly if the framework GetCredentialRequest fails which allows authentication to work without this patch, but they don't such a fallback when creating credentials). Test: atest CtsCredentialManagerTestCases There's also FrameworksServicesTests:com.android.server.credentials, but currently it has some Mockito failures --- .../android/server/pm/ext/GmsCoreUtils.java | 55 +++++++++++++++++++ .../credentials/ProviderGetSession.java | 17 +++++- .../server/credentials/ProviderSession.java | 10 ++++ 3 files changed, 79 insertions(+), 3 deletions(-) create mode 100644 services/core/java/com/android/server/pm/ext/GmsCoreUtils.java diff --git a/services/core/java/com/android/server/pm/ext/GmsCoreUtils.java b/services/core/java/com/android/server/pm/ext/GmsCoreUtils.java new file mode 100644 index 0000000000000..f6dafb722a0fe --- /dev/null +++ b/services/core/java/com/android/server/pm/ext/GmsCoreUtils.java @@ -0,0 +1,55 @@ +package com.android.server.pm.ext; + +import android.annotation.UserIdInt; +import android.content.ComponentName; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.ext.PackageId; +import android.util.Slog; + +public class GmsCoreUtils { + private static final String TAG = "GmsCoreUtils"; + + public static boolean isGmsRemoteCredentialsServiceComponent(ComponentName componentName) { + // FIDO2 is from "remote devices", so it's handed by the RemoteService + return componentName != null + && PackageId.GMS_CORE_NAME.equals(componentName.getPackageName()) + && "com.google.android.gms.auth.api.credentials.credman.service.RemoteService" + .equals(componentName.getClassName()); + } + + public static boolean shouldBypassRemoteEntryCredentialProviderRestrictions( + Context context, ComponentName remoteCredentialProvider, @UserIdInt int userId) { + if (!isGmsRemoteCredentialsServiceComponent(remoteCredentialProvider)) { + return false; + } + + // Ensure GMS is installed for the user that the credential request is for + final ApplicationInfo gmsAppInfo; + try { + gmsAppInfo = context.getPackageManager().getApplicationInfoAsUser( + remoteCredentialProvider.getPackageName(), 0, userId); + } catch (PackageManager.NameNotFoundException e) { + Slog.w(TAG, "failed to resolve " + remoteCredentialProvider, e); + return false; + } + // getApplicationInfoAsUser is @NonNull, but just mimicking upstream code from + // ProviderSession + if (gmsAppInfo != null) { + final int packageId = gmsAppInfo.ext().getPackageId(); + // ensure it's from verified GMS core + if (packageId == PackageId.GMS_CORE) { + // Note: Not checking for Manifest.permission.PROVIDE_REMOTE_CREDENTIALS + // (signature|privileged|role); it seems FIDO2 works fine without granting that + // permission + return true; + } else { + Slog.w(TAG,"bad gmsAppInfo packageId " + packageId + " for " + + remoteCredentialProvider); + } + } + + return false; + } +} diff --git a/services/credentials/java/com/android/server/credentials/ProviderGetSession.java b/services/credentials/java/com/android/server/credentials/ProviderGetSession.java index e18ef2b3230da..a44fca2e8db12 100644 --- a/services/credentials/java/com/android/server/credentials/ProviderGetSession.java +++ b/services/credentials/java/com/android/server/credentials/ProviderGetSession.java @@ -44,6 +44,8 @@ import android.util.Pair; import android.util.Slog; +import com.android.server.pm.ext.GmsCoreUtils; + import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; @@ -94,7 +96,7 @@ public static ProviderGetSession createNewSession( android.credentials.GetCredentialRequest filteredRequest = filterOptions(providerInfo.getCapabilities(), getRequestSession.mClientRequest, - providerInfo, getRequestSession.mHybridService); + providerInfo, getRequestSession.mHybridService, context, userId); if (filteredRequest != null) { Map beginGetOptionToCredentialOptionMap = new HashMap<>(); @@ -130,7 +132,7 @@ public static ProviderGetSession createNewSession( android.credentials.GetCredentialRequest filteredRequest = filterOptions(providerInfo.getCapabilities(), getRequestSession.mClientRequest, - providerInfo, getRequestSession.mHybridService); + providerInfo, getRequestSession.mHybridService, context, userId); if (filteredRequest != null) { Map beginGetOptionToCredentialOptionMap = new HashMap<>(); @@ -181,7 +183,9 @@ private static android.credentials.GetCredentialRequest filterOptions( List providerCapabilities, android.credentials.GetCredentialRequest clientRequest, CredentialProviderInfo info, - String hybridService) { + String hybridService, + Context context, + @UserIdInt int userId) { Slog.i(TAG, "Filtering request options for: " + info.getComponentName()); if (android.credentials.flags.Flags.hybridFilterOptFixEnabled()) { ComponentName hybridComponentName = ComponentName.unflattenFromString(hybridService); @@ -190,6 +194,13 @@ private static android.credentials.GetCredentialRequest filterOptions( Slog.i(TAG, "Skipping filtering of options for hybrid service"); return clientRequest; } + // Filter options are skipped on stock OS, since hybridComponentName corresponds to + // what's set in OEM config (GMS's RemoteService on stock Pixel) + if (GmsCoreUtils.shouldBypassRemoteEntryCredentialProviderRestrictions( + context, info.getComponentName(), userId)) { + Slog.i(TAG, "Skipping filtering of options for hybrid service due to GMS core"); + return clientRequest; + } Slog.w(TAG, "Could not parse hybrid service while filtering options"); } diff --git a/services/credentials/java/com/android/server/credentials/ProviderSession.java b/services/credentials/java/com/android/server/credentials/ProviderSession.java index 8f0ae90588144..4379ea144f0f5 100644 --- a/services/credentials/java/com/android/server/credentials/ProviderSession.java +++ b/services/credentials/java/com/android/server/credentials/ProviderSession.java @@ -33,6 +33,7 @@ import android.util.Slog; import com.android.server.credentials.metrics.ProviderSessionMetric; +import com.android.server.pm.ext.GmsCoreUtils; import java.util.UUID; @@ -253,6 +254,15 @@ protected R getProviderResponse() { protected boolean enforceRemoteEntryRestrictions( @Nullable ComponentName expectedRemoteEntryProviderService) { + if (GmsCoreUtils.shouldBypassRemoteEntryCredentialProviderRestrictions( + mContext, mComponentName, mUserId)) { + // Bypassing a frameworks OEM config check and the permission grant check for + // Manifest.permission.PROVIDE_REMOTE_CREDENTIALS. GMS doesn't seem to require + // Manifest.permission.PROVIDE_REMOTE_CREDENTIALS for FIDO2 (NFC and USB) to work. + Slog.w(TAG, "Remote entry accepted from GmsCoreUtils bypass"); + return true; + } + // Check if the service is the one set by the OEM. If not silently reject this entry if (!mComponentName.equals(expectedRemoteEntryProviderService)) { Slog.w(TAG, "Remote entry being dropped as it is not from the service "