blob: 6f79852df02f4dd519dc2c646b352ff04a191858 [file] [log] [blame]
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.credentials;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.credentials.CreateCredentialException;
import android.credentials.CreateCredentialResponse;
import android.credentials.CredentialProviderInfo;
import android.credentials.ui.CreateCredentialProviderData;
import android.credentials.ui.Entry;
import android.credentials.ui.ProviderPendingIntentResponse;
import android.os.Bundle;
import android.os.ICancellationSignal;
import android.service.credentials.BeginCreateCredentialRequest;
import android.service.credentials.BeginCreateCredentialResponse;
import android.service.credentials.CallingAppInfo;
import android.service.credentials.CreateCredentialRequest;
import android.service.credentials.CreateEntry;
import android.service.credentials.CredentialProviderService;
import android.service.credentials.RemoteEntry;
import android.util.Pair;
import android.util.Slog;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Central provider session that listens for provider callbacks, and maintains provider state.
* Will likely split this into remote response state and UI state.
*/
public final class ProviderCreateSession extends ProviderSession<
BeginCreateCredentialRequest, BeginCreateCredentialResponse> {
private static final String TAG = "ProviderCreateSession";
// Key to be used as an entry key for a save entry
public static final String SAVE_ENTRY_KEY = "save_entry_key";
// Key to be used as an entry key for a remote entry
private static final String REMOTE_ENTRY_KEY = "remote_entry_key";
private final CreateCredentialRequest mCompleteRequest;
private CreateCredentialException mProviderException;
private final ProviderResponseDataHandler mProviderResponseDataHandler;
/** Creates a new provider session to be used by the request session. */
@Nullable
public static ProviderCreateSession createNewSession(
Context context,
@UserIdInt int userId,
CredentialProviderInfo providerInfo,
CreateRequestSession createRequestSession,
RemoteCredentialService remoteCredentialService) {
CreateCredentialRequest providerCreateRequest =
createProviderRequest(providerInfo.getCapabilities(),
createRequestSession.mClientRequest,
createRequestSession.mClientAppInfo,
providerInfo.isSystemProvider());
if (providerCreateRequest != null) {
return new ProviderCreateSession(
context,
providerInfo,
createRequestSession,
userId,
remoteCredentialService,
constructQueryPhaseRequest(createRequestSession.mClientRequest.getType(),
createRequestSession.mClientRequest.getCandidateQueryData(),
createRequestSession.mClientAppInfo,
createRequestSession
.mClientRequest.alwaysSendAppInfoToProvider()),
providerCreateRequest,
createRequestSession.mHybridService
);
}
Slog.i(TAG, "Unable to create provider session for: "
+ providerInfo.getComponentName());
return null;
}
private static BeginCreateCredentialRequest constructQueryPhaseRequest(
String type, Bundle candidateQueryData, CallingAppInfo callingAppInfo,
boolean propagateToProvider) {
if (propagateToProvider) {
return new BeginCreateCredentialRequest(
type,
candidateQueryData,
callingAppInfo
);
}
return new BeginCreateCredentialRequest(
type,
candidateQueryData
);
}
@Nullable
private static CreateCredentialRequest createProviderRequest(
List<String> providerCapabilities,
android.credentials.CreateCredentialRequest clientRequest,
CallingAppInfo callingAppInfo,
boolean isSystemProvider) {
if (clientRequest.isSystemProviderRequired() && !isSystemProvider) {
// Request requires system provider but this session does not correspond to a
// system service
return null;
}
String capability = clientRequest.getType();
if (providerCapabilities.contains(capability)) {
return new CreateCredentialRequest(callingAppInfo, capability,
clientRequest.getCredentialData());
}
return null;
}
private ProviderCreateSession(
@NonNull Context context,
@NonNull CredentialProviderInfo info,
@NonNull ProviderInternalCallback<CreateCredentialResponse> callbacks,
@UserIdInt int userId,
@NonNull RemoteCredentialService remoteCredentialService,
@NonNull BeginCreateCredentialRequest beginCreateRequest,
@NonNull CreateCredentialRequest completeCreateRequest,
String hybridService) {
super(context, beginCreateRequest, callbacks, info.getComponentName(), userId,
remoteCredentialService);
mCompleteRequest = completeCreateRequest;
setStatus(Status.PENDING);
mProviderResponseDataHandler = new ProviderResponseDataHandler(
ComponentName.unflattenFromString(hybridService));
}
@Override
public void onProviderResponseSuccess(
@Nullable BeginCreateCredentialResponse response) {
Slog.i(TAG, "Remote provider responded with a valid response: " + mComponentName);
onSetInitialRemoteResponse(response);
}
/** Called when the provider response resulted in a failure. */
@Override
public void onProviderResponseFailure(int errorCode, @Nullable Exception exception) {
if (exception instanceof CreateCredentialException) {
// Store query phase exception for aggregation with final response
mProviderException = (CreateCredentialException) exception;
// TODO(b/271135048) : Decide on exception type length
mProviderSessionMetric.collectCandidateFrameworkException(mProviderException.getType());
}
mProviderSessionMetric.collectCandidateExceptionStatus(/*hasException=*/true);
updateStatusAndInvokeCallback(Status.CANCELED,
/*source=*/ CredentialsSource.REMOTE_PROVIDER);
}
/** Called when provider service dies. */
@Override
public void onProviderServiceDied(RemoteCredentialService service) {
if (service.getComponentName().equals(mComponentName)) {
updateStatusAndInvokeCallback(Status.SERVICE_DEAD,
/*source=*/ CredentialsSource.REMOTE_PROVIDER);
} else {
Slog.w(TAG, "Component names different in onProviderServiceDied - "
+ "this should not happen");
}
}
@Override
public void onProviderCancellable(ICancellationSignal cancellation) {
mProviderCancellationSignal = cancellation;
}
private void onSetInitialRemoteResponse(BeginCreateCredentialResponse response) {
mProviderResponse = response;
mProviderResponseDataHandler.addResponseContent(response.getCreateEntries(),
response.getRemoteCreateEntry());
if (mProviderResponseDataHandler.isEmptyResponse(response)) {
mProviderSessionMetric.collectCandidateEntryMetrics(response, /*isAuthEntry*/false,
((RequestSession) mCallbacks).mRequestSessionMetric.getInitialPhaseMetric());
updateStatusAndInvokeCallback(Status.EMPTY_RESPONSE,
/*source=*/ CredentialsSource.REMOTE_PROVIDER);
} else {
mProviderSessionMetric.collectCandidateEntryMetrics(response, /*isAuthEntry*/false,
((RequestSession) mCallbacks).mRequestSessionMetric.getInitialPhaseMetric());
updateStatusAndInvokeCallback(Status.SAVE_ENTRIES_RECEIVED,
/*source=*/ CredentialsSource.REMOTE_PROVIDER);
}
}
@Override
@Nullable
protected CreateCredentialProviderData prepareUiData()
throws IllegalArgumentException {
if (!ProviderSession.isUiInvokingStatus(getStatus())) {
Slog.i(TAG, "No data for UI from: " + mComponentName.flattenToString());
return null;
}
if (mProviderResponse != null && !mProviderResponseDataHandler.isEmptyResponse()) {
return mProviderResponseDataHandler.toCreateCredentialProviderData();
}
return null;
}
@Override
public void onUiEntrySelected(String entryType, String entryKey,
ProviderPendingIntentResponse providerPendingIntentResponse) {
switch (entryType) {
case SAVE_ENTRY_KEY:
if (mProviderResponseDataHandler.getCreateEntry(entryKey) == null) {
Slog.i(TAG, "Unexpected save entry key");
invokeCallbackOnInternalInvalidState();
return;
}
onCreateEntrySelected(providerPendingIntentResponse);
break;
case REMOTE_ENTRY_KEY:
if (mProviderResponseDataHandler.getRemoteEntry(entryKey) == null) {
Slog.i(TAG, "Unexpected remote entry key");
invokeCallbackOnInternalInvalidState();
return;
}
onRemoteEntrySelected(providerPendingIntentResponse);
break;
default:
Slog.i(TAG, "Unsupported entry type selected");
invokeCallbackOnInternalInvalidState();
}
}
@Override
protected void invokeSession() {
if (mRemoteCredentialService != null) {
startCandidateMetrics();
mRemoteCredentialService.setCallback(this);
mRemoteCredentialService.onBeginCreateCredential(mProviderRequest);
}
}
private Intent setUpFillInIntent() {
Intent intent = new Intent();
intent.putExtra(CredentialProviderService.EXTRA_CREATE_CREDENTIAL_REQUEST,
mCompleteRequest);
return intent;
}
private void onCreateEntrySelected(ProviderPendingIntentResponse pendingIntentResponse) {
CreateCredentialException exception = maybeGetPendingIntentException(
pendingIntentResponse);
if (exception != null) {
invokeCallbackWithError(
exception.getType(),
exception.getMessage());
return;
}
android.credentials.CreateCredentialResponse credentialResponse =
PendingIntentResultHandler.extractCreateCredentialResponse(
pendingIntentResponse.getResultData());
if (credentialResponse != null) {
mCallbacks.onFinalResponseReceived(mComponentName, credentialResponse);
} else {
Slog.i(TAG, "onSaveEntrySelected - no response or error found in pending "
+ "intent response");
invokeCallbackOnInternalInvalidState();
}
}
private void onRemoteEntrySelected(ProviderPendingIntentResponse pendingIntentResponse) {
// Response from remote entry should be dealt with similar to a response from a
// create entry
onCreateEntrySelected(pendingIntentResponse);
}
@Nullable
private CreateCredentialException maybeGetPendingIntentException(
ProviderPendingIntentResponse pendingIntentResponse) {
if (pendingIntentResponse == null) {
Slog.i(TAG, "pendingIntentResponse is null");
return new CreateCredentialException(CreateCredentialException.TYPE_NO_CREATE_OPTIONS);
}
if (PendingIntentResultHandler.isValidResponse(pendingIntentResponse)) {
CreateCredentialException exception = PendingIntentResultHandler
.extractCreateCredentialException(pendingIntentResponse.getResultData());
if (exception != null) {
Slog.i(TAG, "Pending intent contains provider exception");
return exception;
}
} else if (PendingIntentResultHandler.isCancelledResponse(pendingIntentResponse)) {
return new CreateCredentialException(CreateCredentialException.TYPE_USER_CANCELED);
} else {
return new CreateCredentialException(CreateCredentialException.TYPE_NO_CREATE_OPTIONS);
}
return null;
}
/**
* When an invalid state occurs, e.g. entry mismatch or no response from provider,
* we send back a TYPE_UNKNOWN error as to the developer.
*/
private void invokeCallbackOnInternalInvalidState() {
mCallbacks.onFinalErrorReceived(mComponentName,
CreateCredentialException.TYPE_UNKNOWN,
null);
}
private class ProviderResponseDataHandler {
@Nullable
private final ComponentName mExpectedRemoteEntryProviderService;
@NonNull
private final Map<String, Pair<CreateEntry, Entry>> mUiCreateEntries = new HashMap<>();
@Nullable
private Pair<String, Pair<RemoteEntry, Entry>> mUiRemoteEntry = null;
ProviderResponseDataHandler(@Nullable ComponentName expectedRemoteEntryProviderService) {
mExpectedRemoteEntryProviderService = expectedRemoteEntryProviderService;
}
public void addResponseContent(List<CreateEntry> createEntries,
RemoteEntry remoteEntry) {
createEntries.forEach(this::addCreateEntry);
if (remoteEntry != null) {
setRemoteEntry(remoteEntry);
}
}
public void addCreateEntry(CreateEntry createEntry) {
String id = generateUniqueId();
Entry entry = new Entry(SAVE_ENTRY_KEY,
id, createEntry.getSlice(), setUpFillInIntent());
mUiCreateEntries.put(id, new Pair<>(createEntry, entry));
}
public void setRemoteEntry(@Nullable RemoteEntry remoteEntry) {
if (!enforceRemoteEntryRestrictions(mExpectedRemoteEntryProviderService)) {
Slog.w(TAG, "Remote entry being dropped as it does not meet the restriction"
+ "checks.");
return;
}
if (remoteEntry == null) {
mUiRemoteEntry = null;
return;
}
String id = generateUniqueId();
Entry entry = new Entry(REMOTE_ENTRY_KEY,
id, remoteEntry.getSlice(), setUpFillInIntent());
mUiRemoteEntry = new Pair<>(id, new Pair<>(remoteEntry, entry));
}
public CreateCredentialProviderData toCreateCredentialProviderData() {
return new CreateCredentialProviderData.Builder(
mComponentName.flattenToString())
.setSaveEntries(prepareUiCreateEntries())
.setRemoteEntry(prepareRemoteEntry())
.build();
}
private List<Entry> prepareUiCreateEntries() {
List<Entry> createEntries = new ArrayList<>();
for (String key : mUiCreateEntries.keySet()) {
createEntries.add(mUiCreateEntries.get(key).second);
}
return createEntries;
}
private Entry prepareRemoteEntry() {
if (mUiRemoteEntry == null || mUiRemoteEntry.first == null
|| mUiRemoteEntry.second == null) {
return null;
}
return mUiRemoteEntry.second.second;
}
private boolean isEmptyResponse() {
return mUiCreateEntries.isEmpty() && mUiRemoteEntry == null;
}
@Nullable
public RemoteEntry getRemoteEntry(String entryKey) {
return mUiRemoteEntry == null || mUiRemoteEntry
.first == null || !mUiRemoteEntry.first.equals(entryKey)
|| mUiRemoteEntry.second == null
? null : mUiRemoteEntry.second.first;
}
@Nullable
public CreateEntry getCreateEntry(String entryKey) {
return mUiCreateEntries.get(entryKey) == null
? null : mUiCreateEntries.get(entryKey).first;
}
public boolean isEmptyResponse(BeginCreateCredentialResponse response) {
return (response.getCreateEntries() == null || response.getCreateEntries().isEmpty())
&& response.getRemoteCreateEntry() == null;
}
}
}