From d6040afdf7e37a1e15e18cec83b1463adb134b53 Mon Sep 17 00:00:00 2001 From: Omer Ozer Date: Thu, 19 Jan 2023 02:45:35 +0000 Subject: [PATCH] Add CredentialDescription get impl. Bug: 260629338 CTS-Coverage-Bug: 265212839 API-Coverage-Bug: 265212839 Test: Local Build & Deployment Change-Id: I44b4fba89cee00b51455d3ddf8696477b98045e6 --- .../CredentialDescriptionRegistry.java | 91 ++++-- .../credentials/CredentialManagerService.java | 105 +++++-- .../credentials/ProviderClearSession.java | 6 +- .../credentials/ProviderCreateSession.java | 8 +- .../credentials/ProviderGetSession.java | 90 +++--- .../ProviderRegistryGetSession.java | 261 ++++++++++++++++++ .../server/credentials/ProviderSession.java | 14 +- 7 files changed, 465 insertions(+), 110 deletions(-) create mode 100644 services/credentials/java/com/android/server/credentials/ProviderRegistryGetSession.java diff --git a/services/credentials/java/com/android/server/credentials/CredentialDescriptionRegistry.java b/services/credentials/java/com/android/server/credentials/CredentialDescriptionRegistry.java index fbdcc441a419c..3d504efd17974 100644 --- a/services/credentials/java/com/android/server/credentials/CredentialDescriptionRegistry.java +++ b/services/credentials/java/com/android/server/credentials/CredentialDescriptionRegistry.java @@ -19,35 +19,72 @@ package com.android.server.credentials; import android.credentials.CredentialDescription; import android.credentials.RegisterCredentialDescriptionRequest; import android.credentials.UnregisterCredentialDescriptionRequest; +import android.service.credentials.CredentialEntry; import android.util.SparseArray; +import com.android.internal.annotations.GuardedBy; + import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.locks.ReentrantLock; /** Contains information on what CredentialProvider has what provisioned Credential. */ -public class CredentialDescriptionRegistry { +public final class CredentialDescriptionRegistry { private static final int MAX_ALLOWED_CREDENTIAL_DESCRIPTIONS = 128; private static final int MAX_ALLOWED_ENTRIES_PER_PROVIDER = 16; - private static SparseArray sCredentialDescriptionSessionPerUser; + @GuardedBy("sLock") + private static final SparseArray + sCredentialDescriptionSessionPerUser; + private static final ReentrantLock sLock; static { sCredentialDescriptionSessionPerUser = new SparseArray<>(); + sLock = new ReentrantLock(); } - // TODO(b/265992655): add a way to update CredentialRegistry when a user is removed. - /** Get and/or create a {@link CredentialDescription} for the given user id. */ - public static CredentialDescriptionRegistry forUser(int userId) { - CredentialDescriptionRegistry session = - sCredentialDescriptionSessionPerUser.get(userId, null); + /** Represents the results of a given query into the registry. */ + public static final class FilterResult { + final String mPackageName; + final List mCredentialEntries; - if (session == null) { - session = new CredentialDescriptionRegistry(); - sCredentialDescriptionSessionPerUser.put(userId, session); + private FilterResult(String packageName, + List credentialEntries) { + mPackageName = packageName; + mCredentialEntries = credentialEntries; + } + } + + /** Get and/or create a {@link CredentialDescription} for the given user id. */ + @GuardedBy("sLock") + public static CredentialDescriptionRegistry forUser(int userId) { + sLock.lock(); + try { + CredentialDescriptionRegistry session = + sCredentialDescriptionSessionPerUser.get(userId, null); + + if (session == null) { + session = new CredentialDescriptionRegistry(); + sCredentialDescriptionSessionPerUser.put(userId, session); + } + return session; + } finally { + sLock.unlock(); + } + } + + /** Clears an existing session for a given user identifier. */ + @GuardedBy("sLock") + public static void clearUserSession(int userId) { + sLock.lock(); + try { + sCredentialDescriptionSessionPerUser.remove(userId); + } finally { + sLock.unlock(); } - return session; } private Map> mCredentialDescriptions; @@ -74,7 +111,7 @@ public class CredentialDescriptionRegistry { int size = mCredentialDescriptions.get(callingPackageName).size(); mCredentialDescriptions.get(callingPackageName) .addAll(descriptions); - mTotalDescriptionCount += size - mCredentialDescriptions.get(callingPackageName).size(); + mTotalDescriptionCount += mCredentialDescriptions.get(callingPackageName).size() - size; } } @@ -93,21 +130,33 @@ public class CredentialDescriptionRegistry { } } + /** Returns package names and entries of a CredentialProviders that can satisfy a given + * {@link CredentialDescription}. */ + public Set getFilteredResultForProvider(String packageName, + List flatRequestStrings) { + Set result = new HashSet<>(); + Set currentSet = mCredentialDescriptions.get(packageName); + for (CredentialDescription containedDescription: currentSet) { + if (flatRequestStrings.contains(containedDescription.getFlattenedRequestString())) { + result.add(new FilterResult(packageName, containedDescription + .getCredentialEntries())); + } + } + return result; + } + /** Returns package names of CredentialProviders that can satisfy a given * {@link CredentialDescription}. */ - public Set filterCredentials(String flatRequestString) { - + public Set getMatchingProviders(Set flatRequestString) { Set result = new HashSet<>(); - - for (String componentName: mCredentialDescriptions.keySet()) { - Set currentSet = mCredentialDescriptions.get(componentName); - for (CredentialDescription containedDescription: currentSet) { - if (flatRequestString.equals(containedDescription.getFlattenedRequestString())) { - result.add(componentName); + for (String packageName: mCredentialDescriptions.keySet()) { + Set currentSet = mCredentialDescriptions.get(packageName); + for (CredentialDescription containedDescription : currentSet) { + if (flatRequestString.contains(containedDescription.getFlattenedRequestString())) { + result.add(packageName); } } } - return result; } diff --git a/services/credentials/java/com/android/server/credentials/CredentialManagerService.java b/services/credentials/java/com/android/server/credentials/CredentialManagerService.java index ff72ed7abf4df..a1e80d0dea6f0 100644 --- a/services/credentials/java/com/android/server/credentials/CredentialManagerService.java +++ b/services/credentials/java/com/android/server/credentials/CredentialManagerService.java @@ -61,13 +61,11 @@ import com.android.server.infra.AbstractMasterSystemService; import com.android.server.infra.SecureSettingsServiceNameResolver; import java.util.ArrayList; -import java.util.Collection; +import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import java.util.function.Consumer; -import java.util.function.Function; import java.util.stream.Collectors; -import java.util.stream.Stream; /** * Entry point service for credential management. @@ -236,6 +234,7 @@ public final class CredentialManagerService concatenatedServices.addAll(getOrConstructSystemServiceListLock(userId)); return concatenatedServices; } + public static boolean isCredentialDescriptionApiEnabled() { return DeviceConfig.getBoolean( DeviceConfig.NAMESPACE_CREDENTIAL, DEVICE_CONFIG_ENABLE_CREDENTIAL_DESC_API, false); @@ -244,44 +243,38 @@ public final class CredentialManagerService @SuppressWarnings("GuardedBy") // ErrorProne requires initiateProviderSessionForRequestLocked // to be guarded by 'service.mLock', which is the same as mLock. private List initiateProviderSessionsWithActiveContainers( - RequestSession session, - List requestOptions, Set activeCredentialContainers) { + GetRequestSession session, + List requestOptions, Set activeCredentialContainers) { List providerSessions = new ArrayList<>(); // Invoke all services of a user to initiate a provider session - runForUser((service) -> { - if (activeCredentialContainers.contains(service.getComponentName())) { - ProviderSession providerSession = service - .initiateProviderSessionForRequestLocked(session, requestOptions); - if (providerSession != null) { - providerSessions.add(providerSession); - } - } - }); + for (String packageName: activeCredentialContainers) { + providerSessions.add(ProviderRegistryGetSession.createNewSession( + mContext, + UserHandle.getCallingUserId(), + session, + packageName, + requestOptions)); + } return providerSessions; } @NonNull - private Set getMatchingProviders(GetCredentialRequest request) { + private Set getFilteredResultFromRegistry(List options) { // Session for active/provisioned credential descriptions; CredentialDescriptionRegistry registry = CredentialDescriptionRegistry .forUser(UserHandle.getCallingUserId()); // All requested credential descriptions based on the given request. Set requestedCredentialDescriptions = - request.getCredentialOptions().stream().map( - credentialOption -> credentialOption + options.stream().map( + getCredentialOption -> getCredentialOption .getCredentialRetrievalData() .getString(CredentialOption .FLATTENED_REQUEST)) .collect(Collectors.toSet()); // All requested credential descriptions based on the given request. - return requestedCredentialDescriptions.stream() - .map(registry::filterCredentials) - .flatMap( - (Function, Stream>) - Collection::stream) - .collect(Collectors.toSet()); + return registry.getMatchingProviders(requestedCredentialDescriptions); } @SuppressWarnings("GuardedBy") // ErrorProne requires initiateProviderSessionForRequestLocked @@ -304,6 +297,13 @@ public final class CredentialManagerService return providerSessions; } + @Override + @GuardedBy("CredentialDescriptionRegistry.sLock") + public void onUserStopped(@NonNull TargetUser user) { + super.onUserStopped(user); + CredentialDescriptionRegistry.clearUserSession(user.getUserIdentifier()); + } + private CallingAppInfo constructCallingAppInfo(String packageName, int userId) { final PackageInfo packageInfo; try { @@ -340,13 +340,57 @@ public final class CredentialManagerService request, constructCallingAppInfo(callingPackage, userId)); - // Initiate all provider sessions - List providerSessions = - initiateProviderSessions( - session, - request.getCredentialOptions().stream() - .map(CredentialOption::getType) - .collect(Collectors.toList())); + List providerSessions; + + if (isCredentialDescriptionApiEnabled()) { + List optionsThatRequireActiveCredentials = + request.getCredentialOptions().stream() + .filter(getCredentialOption -> + !TextUtils.isEmpty(getCredentialOption + .getCredentialRetrievalData().getString( + CredentialOption + .FLATTENED_REQUEST, null))) + .toList(); + + List optionsThatDoNotRequireActiveCredentials = + request.getCredentialOptions().stream() + .filter(getCredentialOption -> + TextUtils.isEmpty(getCredentialOption + .getCredentialRetrievalData().getString( + CredentialOption + .FLATTENED_REQUEST, null))) + .toList(); + + List sessionsWithoutRemoteService = + initiateProviderSessionsWithActiveContainers(session, + optionsThatRequireActiveCredentials + .stream().map(getCredentialOption -> + getCredentialOption.getCredentialRetrievalData() + .getString(CredentialOption + .FLATTENED_REQUEST)) + .collect(Collectors.toList()), + getFilteredResultFromRegistry(optionsThatRequireActiveCredentials)); + + List sessionsWithRemoteService = initiateProviderSessions( + session, + optionsThatDoNotRequireActiveCredentials.stream() + .map(CredentialOption::getType) + .collect(Collectors.toList())); + + Set all = new LinkedHashSet<>(); + all.addAll(sessionsWithRemoteService); + all.addAll(sessionsWithoutRemoteService); + + providerSessions = new ArrayList<>(all); + } else { + // Initiate all provider sessions + providerSessions = + initiateProviderSessions( + session, + request.getCredentialOptions().stream() + .map(CredentialOption::getType) + .collect(Collectors.toList())); + } if (providerSessions.isEmpty()) { try { @@ -363,6 +407,7 @@ public final class CredentialManagerService // Iterate over all provider sessions and invoke the request providerSessions.forEach(ProviderSession::invokeSession); + return cancelTransport; } diff --git a/services/credentials/java/com/android/server/credentials/ProviderClearSession.java b/services/credentials/java/com/android/server/credentials/ProviderClearSession.java index b1126499d56fa..b20f0cd0f9060 100644 --- a/services/credentials/java/com/android/server/credentials/ProviderClearSession.java +++ b/services/credentials/java/com/android/server/credentials/ProviderClearSession.java @@ -118,8 +118,8 @@ public final class ProviderClearSession extends ProviderSession mUiSaveEntries = new HashMap<>(); @@ -199,9 +201,9 @@ public final class ProviderCreateSession extends ProviderSession< @Override protected void invokeSession() { - this.mRemoteCredentialService.onCreateCredential( - this.getProviderRequest(), - /*callback=*/this); + if (mRemoteCredentialService != null) { + mRemoteCredentialService.onCreateCredential(mProviderRequest, this); + } } private List prepareUiSaveEntries(@NonNull List saveEntries) { diff --git a/services/credentials/java/com/android/server/credentials/ProviderGetSession.java b/services/credentials/java/com/android/server/credentials/ProviderGetSession.java index dec34325bbddd..3ccead106075a 100644 --- a/services/credentials/java/com/android/server/credentials/ProviderGetSession.java +++ b/services/credentials/java/com/android/server/credentials/ProviderGetSession.java @@ -59,14 +59,14 @@ public final class ProviderGetSession extends ProviderSession { private static final String TAG = "ProviderGetSession"; - - // Key to be used as an entry key for a credential entry - private static final String CREDENTIAL_ENTRY_KEY = "credential_key"; - // Key to be used as the entry key for an action entry private static final String ACTION_ENTRY_KEY = "action_key"; // Key to be used as the entry key for the authentication entry private static final String AUTHENTICATION_ACTION_ENTRY_KEY = "authentication_action_key"; + // Key to be used as an entry key for a remote entry + private static final String REMOTE_ENTRY_KEY = "remote_entry_key"; + // Key to be used as an entry key for a credential entry + private static final String CREDENTIAL_ENTRY_KEY = "credential_key"; @NonNull private final Map mUiCredentialEntries = new HashMap<>(); @@ -101,23 +101,8 @@ public final class ProviderGetSession extends ProviderSession { - return new BeginGetCredentialOption( - option.getType(), - option.getCandidateQueryData()); - }).collect(Collectors.toList())) - .build(); - } - @Nullable - private static android.credentials.GetCredentialRequest filterOptions( + protected static android.credentials.GetCredentialRequest filterOptions( List providerCapabilities, android.credentials.GetCredentialRequest clientRequest ) { @@ -142,6 +127,21 @@ public final class ProviderGetSession extends ProviderSession { + return new BeginGetCredentialOption( + option.getType(), + option.getCandidateQueryData()); + }).collect(Collectors.toList())) + .build(); + } + public ProviderGetSession(Context context, CredentialProviderInfo info, ProviderInternalCallback callbacks, @@ -232,9 +232,9 @@ public final class ProviderGetSession extends ProviderSession> { + + private static final String TAG = "ProviderRegistryGetSession"; + private static final String CREDENTIAL_ENTRY_KEY = "credential_key"; + + /** Creates a new provider session to be used by the request session. */ + @Nullable + public static ProviderRegistryGetSession createNewSession( + @NonNull Context context, + @UserIdInt int userId, + @NonNull GetRequestSession getRequestSession, + @NonNull String credentialProviderPackageName, + @NonNull List requestOptions) { + return new ProviderRegistryGetSession( + context, + userId, + getRequestSession, + getRequestSession.mClientRequest, + getRequestSession.mClientAppInfo, + credentialProviderPackageName, + requestOptions); + } + + @NonNull + private final Map mUiCredentialEntries = new HashMap<>(); + @NonNull + private final CredentialDescriptionRegistry mCredentialDescriptionRegistry; + @NonNull + private final CallingAppInfo mCallingAppInfo; + @NonNull + private final String mCredentialProviderPackageName; + @NonNull + private final GetRequestSession mGetRequestSession; + @NonNull + private final List mRequestOptions; + private List mCredentialEntries; + + protected ProviderRegistryGetSession(@NonNull Context context, + @NonNull int userId, + @NonNull GetRequestSession session, + @NonNull GetCredentialRequest request, + @NonNull CallingAppInfo callingAppInfo, + @NonNull String servicePackageName, + @NonNull List requestOptions) { + super(context, null, request, session, userId, null); + mGetRequestSession = session; + mCredentialDescriptionRegistry = CredentialDescriptionRegistry.forUser(userId); + mCallingAppInfo = callingAppInfo; + mCredentialProviderPackageName = servicePackageName; + mRequestOptions = requestOptions; + } + + private List prepareUiCredentialEntries( + @NonNull List credentialEntries) { + Log.i(TAG, "in prepareUiProviderDataWithCredentials"); + List credentialUiEntries = new ArrayList<>(); + + // Populate the credential entries + for (CredentialEntry credentialEntry : credentialEntries) { + String entryId = generateEntryId(); + mUiCredentialEntries.put(entryId, credentialEntry); + Log.i(TAG, "in prepareUiProviderData creating ui entry with id " + entryId); + credentialUiEntries.add(new Entry(CREDENTIAL_ENTRY_KEY, entryId, + credentialEntry.getSlice(), + setUpFillInIntent(credentialEntry.getType()))); + } + return credentialUiEntries; + } + + private Intent setUpFillInIntent(String type) { + Intent intent = new Intent(); + for (CredentialOption option : mProviderRequest.getCredentialOptions()) { + if (option.getType().equals(type)) { + intent.putExtra( + CredentialProviderService + .EXTRA_GET_CREDENTIAL_REQUEST, + new android.service.credentials.GetCredentialRequest( + mCallingAppInfo, option)); + return intent; + } + } + return intent; + } + + @Override + protected ProviderData prepareUiData() { + Log.i(TAG, "In prepareUiData"); + if (!ProviderSession.isUiInvokingStatus(getStatus())) { + Log.i(TAG, "In prepareUiData - provider does not want to show UI: " + + mComponentName.flattenToString()); + return null; + } + if (mProviderResponse == null) { + Log.i(TAG, "In prepareUiData response null"); + throw new IllegalStateException("Response must be in completion mode"); + } + return new GetCredentialProviderData.Builder( + mComponentName.flattenToString()).setActionChips(null) + .setCredentialEntries(prepareUiCredentialEntries( + mProviderResponse.stream().flatMap((Function>) filterResult -> + filterResult.mCredentialEntries.stream()) + .collect(Collectors.toList()))) + .build(); + } + + @Override // Selection call from the request provider + protected void onUiEntrySelected(String entryType, String entryKey, + ProviderPendingIntentResponse providerPendingIntentResponse) { + switch (entryType) { + case CREDENTIAL_ENTRY_KEY: + CredentialEntry credentialEntry = mUiCredentialEntries.get(entryKey); + if (credentialEntry == null) { + Log.i(TAG, "Unexpected credential entry key"); + return; + } + onCredentialEntrySelected(credentialEntry, providerPendingIntentResponse); + break; + default: + Log.i(TAG, "Unsupported entry type selected"); + } + } + + private void onCredentialEntrySelected(CredentialEntry credentialEntry, + ProviderPendingIntentResponse providerPendingIntentResponse) { + if (!mCredentialEntries.contains(credentialEntry)) { + invokeCallbackWithError("", + ""); + } + + if (providerPendingIntentResponse != null) { + // Check if pending intent has an error + GetCredentialException exception = maybeGetPendingIntentException( + providerPendingIntentResponse); + if (exception != null) { + invokeCallbackWithError(exception.getType(), + exception.getMessage()); + return; + } + + // Check if pending intent has a credential + GetCredentialResponse getCredentialResponse = PendingIntentResultHandler + .extractGetCredentialResponse( + providerPendingIntentResponse.getResultData()); + if (getCredentialResponse != null) { + if (mCallbacks != null) { + mCallbacks.onFinalResponseReceived(mComponentName, + getCredentialResponse); + } + return; + } + + Log.i(TAG, "Pending intent response contains no credential, or error"); + } + Log.i(TAG, "CredentialEntry does not have a credential or a pending intent result"); + } + + @Override + public void onProviderResponseSuccess( + @Nullable Set response) { + // No need to do anything since this class does not rely on a remote service. + } + + @Override + public void onProviderResponseFailure(int internalErrorCode, @Nullable Exception e) { + // No need to do anything since this class does not rely on a remote service. + } + + @Override + public void onProviderServiceDied(RemoteCredentialService service) { + // No need to do anything since this class does not rely on a remote service. + } + + @Override + protected void invokeSession() { + mProviderResponse = mCredentialDescriptionRegistry + .getFilteredResultForProvider(mCredentialProviderPackageName, + mRequestOptions); + mCredentialEntries = mProviderResponse.stream().flatMap( + (Function>) filterResult + -> filterResult.mCredentialEntries.stream()) + .collect(Collectors.toList()); + setStatus(Status.CREDENTIALS_RECEIVED); + } + + @Nullable + protected GetCredentialException maybeGetPendingIntentException( + ProviderPendingIntentResponse pendingIntentResponse) { + if (pendingIntentResponse == null) { + android.util.Log.i(TAG, "pendingIntentResponse is null"); + return null; + } + if (PendingIntentResultHandler.isValidResponse(pendingIntentResponse)) { + GetCredentialException exception = PendingIntentResultHandler + .extractGetCredentialException(pendingIntentResponse.getResultData()); + if (exception != null) { + android.util.Log.i(TAG, "Pending intent contains provider exception"); + return exception; + } + } else if (PendingIntentResultHandler.isCancelledResponse(pendingIntentResponse)) { + return new GetCredentialException(GetCredentialException.TYPE_USER_CANCELED); + } else { + return new GetCredentialException(GetCredentialException.TYPE_NO_CREDENTIAL); + } + return null; + } +} diff --git a/services/credentials/java/com/android/server/credentials/ProviderSession.java b/services/credentials/java/com/android/server/credentials/ProviderSession.java index 8e0d6f86a1991..c479c4ef2ac8b 100644 --- a/services/credentials/java/com/android/server/credentials/ProviderSession.java +++ b/services/credentials/java/com/android/server/credentials/ProviderSession.java @@ -38,16 +38,14 @@ public abstract class ProviderSession implements RemoteCredentialService.ProviderCallbacks { private static final String TAG = "ProviderSession"; - // Key to be used as an entry key for a remote entry - protected static final String REMOTE_ENTRY_KEY = "remote_entry_key"; @NonNull protected final Context mContext; @NonNull protected final ComponentName mComponentName; - @NonNull protected final CredentialProviderInfo mProviderInfo; - @NonNull protected final RemoteCredentialService mRemoteCredentialService; + @Nullable protected final CredentialProviderInfo mProviderInfo; + @Nullable protected final RemoteCredentialService mRemoteCredentialService; @NonNull protected final int mUserId; @NonNull protected Status mStatus = Status.NOT_STARTED; - @NonNull protected final ProviderInternalCallback mCallbacks; + @Nullable protected final ProviderInternalCallback mCallbacks; @Nullable protected Credential mFinalCredentialResponse; @NonNull protected final T mProviderRequest; @Nullable protected R mProviderResponse; @@ -109,9 +107,9 @@ public abstract class ProviderSession protected ProviderSession(@NonNull Context context, @NonNull CredentialProviderInfo info, @NonNull T providerRequest, - @NonNull ProviderInternalCallback callbacks, + @Nullable ProviderInternalCallback callbacks, @NonNull int userId, - @NonNull RemoteCredentialService remoteCredentialService) { + @Nullable RemoteCredentialService remoteCredentialService) { mContext = context; mProviderInfo = info; mProviderRequest = providerRequest; @@ -165,7 +163,7 @@ public abstract class ProviderSession return mComponentName; } - @NonNull + @Nullable protected RemoteCredentialService getRemoteCredentialService() { return mRemoteCredentialService; }