1 /* 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.server.credentials; 18 19 import android.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.annotation.UserIdInt; 22 import android.content.ComponentName; 23 import android.content.Context; 24 import android.content.Intent; 25 import android.credentials.CreateCredentialException; 26 import android.credentials.CreateCredentialResponse; 27 import android.credentials.CredentialManager; 28 import android.credentials.CredentialProviderInfo; 29 import android.credentials.selection.CreateCredentialProviderData; 30 import android.credentials.selection.Entry; 31 import android.credentials.selection.ProviderPendingIntentResponse; 32 import android.os.Bundle; 33 import android.os.ICancellationSignal; 34 import android.service.credentials.BeginCreateCredentialRequest; 35 import android.service.credentials.BeginCreateCredentialResponse; 36 import android.service.credentials.CallingAppInfo; 37 import android.service.credentials.CreateCredentialRequest; 38 import android.service.credentials.CreateEntry; 39 import android.service.credentials.CredentialProviderService; 40 import android.service.credentials.RemoteEntry; 41 import android.util.Pair; 42 import android.util.Slog; 43 44 import java.util.ArrayList; 45 import java.util.HashMap; 46 import java.util.List; 47 import java.util.Map; 48 49 /** 50 * Central provider session that listens for provider callbacks, and maintains provider state. 51 * Will likely split this into remote response state and UI state. 52 */ 53 public final class ProviderCreateSession extends ProviderSession< 54 BeginCreateCredentialRequest, BeginCreateCredentialResponse> { 55 private static final String TAG = CredentialManager.TAG; 56 57 // Key to be used as an entry key for a save entry 58 public static final String SAVE_ENTRY_KEY = "save_entry_key"; 59 // Key to be used as an entry key for a remote entry 60 private static final String REMOTE_ENTRY_KEY = "remote_entry_key"; 61 62 private final CreateCredentialRequest mCompleteRequest; 63 64 private CreateCredentialException mProviderException; 65 66 private final ProviderResponseDataHandler mProviderResponseDataHandler; 67 68 /** Creates a new provider session to be used by the request session. */ 69 @Nullable createNewSession( Context context, @UserIdInt int userId, CredentialProviderInfo providerInfo, CreateRequestSession createRequestSession, RemoteCredentialService remoteCredentialService)70 public static ProviderCreateSession createNewSession( 71 Context context, 72 @UserIdInt int userId, 73 CredentialProviderInfo providerInfo, 74 CreateRequestSession createRequestSession, 75 RemoteCredentialService remoteCredentialService) { 76 CreateCredentialRequest providerCreateRequest = 77 createProviderRequest(providerInfo.getCapabilities(), 78 createRequestSession.mClientRequest, 79 createRequestSession.mClientAppInfo, 80 providerInfo.isSystemProvider()); 81 if (providerCreateRequest != null) { 82 return new ProviderCreateSession( 83 context, 84 providerInfo, 85 createRequestSession, 86 userId, 87 remoteCredentialService, 88 constructQueryPhaseRequest(createRequestSession.mClientRequest.getType(), 89 createRequestSession.mClientRequest.getCandidateQueryData(), 90 createRequestSession.mClientAppInfo, 91 createRequestSession 92 .mClientRequest.alwaysSendAppInfoToProvider()), 93 providerCreateRequest, 94 createRequestSession.mHybridService 95 ); 96 } 97 Slog.i(TAG, "Unable to create provider session for: " 98 + providerInfo.getComponentName()); 99 return null; 100 } 101 constructQueryPhaseRequest( String type, Bundle candidateQueryData, CallingAppInfo callingAppInfo, boolean propagateToProvider)102 private static BeginCreateCredentialRequest constructQueryPhaseRequest( 103 String type, Bundle candidateQueryData, CallingAppInfo callingAppInfo, 104 boolean propagateToProvider) { 105 if (propagateToProvider) { 106 return new BeginCreateCredentialRequest( 107 type, 108 candidateQueryData, 109 callingAppInfo 110 ); 111 } 112 return new BeginCreateCredentialRequest( 113 type, 114 candidateQueryData 115 ); 116 } 117 118 @Nullable createProviderRequest( List<String> providerCapabilities, android.credentials.CreateCredentialRequest clientRequest, CallingAppInfo callingAppInfo, boolean isSystemProvider)119 private static CreateCredentialRequest createProviderRequest( 120 List<String> providerCapabilities, 121 android.credentials.CreateCredentialRequest clientRequest, 122 CallingAppInfo callingAppInfo, 123 boolean isSystemProvider) { 124 if (clientRequest.isSystemProviderRequired() && !isSystemProvider) { 125 // Request requires system provider but this session does not correspond to a 126 // system service 127 return null; 128 } 129 String capability = clientRequest.getType(); 130 if (providerCapabilities.contains(capability)) { 131 return new CreateCredentialRequest(callingAppInfo, capability, 132 clientRequest.getCredentialData()); 133 } 134 return null; 135 } 136 ProviderCreateSession( @onNull Context context, @NonNull CredentialProviderInfo info, @NonNull ProviderInternalCallback<CreateCredentialResponse> callbacks, @UserIdInt int userId, @NonNull RemoteCredentialService remoteCredentialService, @NonNull BeginCreateCredentialRequest beginCreateRequest, @NonNull CreateCredentialRequest completeCreateRequest, String hybridService)137 private ProviderCreateSession( 138 @NonNull Context context, 139 @NonNull CredentialProviderInfo info, 140 @NonNull ProviderInternalCallback<CreateCredentialResponse> callbacks, 141 @UserIdInt int userId, 142 @NonNull RemoteCredentialService remoteCredentialService, 143 @NonNull BeginCreateCredentialRequest beginCreateRequest, 144 @NonNull CreateCredentialRequest completeCreateRequest, 145 String hybridService) { 146 super(context, beginCreateRequest, callbacks, info.getComponentName(), userId, 147 remoteCredentialService); 148 mCompleteRequest = completeCreateRequest; 149 setStatus(Status.PENDING); 150 mProviderResponseDataHandler = new ProviderResponseDataHandler( 151 ComponentName.unflattenFromString(hybridService)); 152 } 153 154 @Override onProviderResponseSuccess( @ullable BeginCreateCredentialResponse response)155 public void onProviderResponseSuccess( 156 @Nullable BeginCreateCredentialResponse response) { 157 Slog.i(TAG, "Remote provider responded with a valid response: " + mComponentName); 158 onSetInitialRemoteResponse(response); 159 } 160 161 /** Called when the provider response resulted in a failure. */ 162 @Override onProviderResponseFailure(int errorCode, @Nullable Exception exception)163 public void onProviderResponseFailure(int errorCode, @Nullable Exception exception) { 164 if (exception instanceof CreateCredentialException) { 165 // Store query phase exception for aggregation with final response 166 mProviderException = (CreateCredentialException) exception; 167 // TODO(b/271135048) : Decide on exception type length 168 mProviderSessionMetric.collectCandidateFrameworkException(mProviderException.getType()); 169 } 170 mProviderSessionMetric.collectCandidateExceptionStatus(/*hasException=*/true); 171 updateStatusAndInvokeCallback(Status.CANCELED, 172 /*source=*/ CredentialsSource.REMOTE_PROVIDER); 173 } 174 175 /** Called when provider service dies. */ 176 @Override onProviderServiceDied(RemoteCredentialService service)177 public void onProviderServiceDied(RemoteCredentialService service) { 178 if (service.getComponentName().equals(mComponentName)) { 179 updateStatusAndInvokeCallback(Status.SERVICE_DEAD, 180 /*source=*/ CredentialsSource.REMOTE_PROVIDER); 181 } else { 182 Slog.w(TAG, "Component names different in onProviderServiceDied - " 183 + "this should not happen"); 184 } 185 } 186 187 @Override onProviderCancellable(ICancellationSignal cancellation)188 public void onProviderCancellable(ICancellationSignal cancellation) { 189 mProviderCancellationSignal = cancellation; 190 } 191 onSetInitialRemoteResponse(BeginCreateCredentialResponse response)192 private void onSetInitialRemoteResponse(BeginCreateCredentialResponse response) { 193 mProviderResponse = response; 194 mProviderResponseDataHandler.addResponseContent(response.getCreateEntries(), 195 response.getRemoteCreateEntry()); 196 if (mProviderResponseDataHandler.isEmptyResponse(response)) { 197 mProviderSessionMetric.collectCandidateEntryMetrics(response, /*isAuthEntry*/false, 198 ((RequestSession) mCallbacks).mRequestSessionMetric.getInitialPhaseMetric()); 199 updateStatusAndInvokeCallback(Status.EMPTY_RESPONSE, 200 /*source=*/ CredentialsSource.REMOTE_PROVIDER); 201 } else { 202 mProviderSessionMetric.collectCandidateEntryMetrics(response, /*isAuthEntry*/false, 203 ((RequestSession) mCallbacks).mRequestSessionMetric.getInitialPhaseMetric()); 204 updateStatusAndInvokeCallback(Status.SAVE_ENTRIES_RECEIVED, 205 /*source=*/ CredentialsSource.REMOTE_PROVIDER); 206 } 207 } 208 209 @Override 210 @Nullable prepareUiData()211 protected CreateCredentialProviderData prepareUiData() 212 throws IllegalArgumentException { 213 if (!ProviderSession.isUiInvokingStatus(getStatus())) { 214 Slog.i(TAG, "No data for UI from: " + mComponentName.flattenToString()); 215 return null; 216 } 217 218 if (mProviderResponse != null && !mProviderResponseDataHandler.isEmptyResponse()) { 219 return mProviderResponseDataHandler.toCreateCredentialProviderData(); 220 } 221 return null; 222 } 223 224 @Override onUiEntrySelected(String entryType, String entryKey, ProviderPendingIntentResponse providerPendingIntentResponse)225 public void onUiEntrySelected(String entryType, String entryKey, 226 ProviderPendingIntentResponse providerPendingIntentResponse) { 227 switch (entryType) { 228 case SAVE_ENTRY_KEY: 229 if (mProviderResponseDataHandler.getCreateEntry(entryKey) == null) { 230 Slog.i(TAG, "Unexpected save entry key"); 231 invokeCallbackOnInternalInvalidState(); 232 return; 233 } 234 onCreateEntrySelected(providerPendingIntentResponse); 235 break; 236 case REMOTE_ENTRY_KEY: 237 if (mProviderResponseDataHandler.getRemoteEntry(entryKey) == null) { 238 Slog.i(TAG, "Unexpected remote entry key"); 239 invokeCallbackOnInternalInvalidState(); 240 return; 241 } 242 onRemoteEntrySelected(providerPendingIntentResponse); 243 break; 244 default: 245 Slog.i(TAG, "Unsupported entry type selected"); 246 invokeCallbackOnInternalInvalidState(); 247 } 248 } 249 250 @Override invokeSession()251 protected void invokeSession() { 252 if (mRemoteCredentialService != null) { 253 startCandidateMetrics(); 254 mRemoteCredentialService.setCallback(this); 255 mRemoteCredentialService.onBeginCreateCredential(mProviderRequest); 256 } 257 } 258 setUpFillInIntent()259 private Intent setUpFillInIntent() { 260 Intent intent = new Intent(); 261 intent.putExtra(CredentialProviderService.EXTRA_CREATE_CREDENTIAL_REQUEST, 262 mCompleteRequest); 263 return intent; 264 } 265 onCreateEntrySelected(ProviderPendingIntentResponse pendingIntentResponse)266 private void onCreateEntrySelected(ProviderPendingIntentResponse pendingIntentResponse) { 267 CreateCredentialException exception = maybeGetPendingIntentException( 268 pendingIntentResponse); 269 if (exception != null) { 270 invokeCallbackWithError( 271 exception.getType(), 272 exception.getMessage()); 273 return; 274 } 275 android.credentials.CreateCredentialResponse credentialResponse = 276 PendingIntentResultHandler.extractCreateCredentialResponse( 277 pendingIntentResponse.getResultData()); 278 if (credentialResponse != null) { 279 mCallbacks.onFinalResponseReceived(mComponentName, credentialResponse); 280 } else { 281 Slog.i(TAG, "onSaveEntrySelected - no response or error found in pending " 282 + "intent response"); 283 invokeCallbackOnInternalInvalidState(); 284 } 285 } 286 onRemoteEntrySelected(ProviderPendingIntentResponse pendingIntentResponse)287 private void onRemoteEntrySelected(ProviderPendingIntentResponse pendingIntentResponse) { 288 // Response from remote entry should be dealt with similar to a response from a 289 // create entry 290 onCreateEntrySelected(pendingIntentResponse); 291 } 292 293 @Nullable maybeGetPendingIntentException( ProviderPendingIntentResponse pendingIntentResponse)294 private CreateCredentialException maybeGetPendingIntentException( 295 ProviderPendingIntentResponse pendingIntentResponse) { 296 if (pendingIntentResponse == null) { 297 Slog.i(TAG, "pendingIntentResponse is null"); 298 return new CreateCredentialException(CreateCredentialException.TYPE_NO_CREATE_OPTIONS); 299 } 300 if (PendingIntentResultHandler.isValidResponse(pendingIntentResponse)) { 301 CreateCredentialException exception = PendingIntentResultHandler 302 .extractCreateCredentialException(pendingIntentResponse.getResultData()); 303 if (exception != null) { 304 Slog.i(TAG, "Pending intent contains provider exception"); 305 return exception; 306 } 307 } else if (PendingIntentResultHandler.isCancelledResponse(pendingIntentResponse)) { 308 return new CreateCredentialException(CreateCredentialException.TYPE_USER_CANCELED); 309 } else { 310 return new CreateCredentialException(CreateCredentialException.TYPE_NO_CREATE_OPTIONS); 311 } 312 return null; 313 } 314 315 /** 316 * When an invalid state occurs, e.g. entry mismatch or no response from provider, 317 * we send back a TYPE_UNKNOWN error as to the developer. 318 */ invokeCallbackOnInternalInvalidState()319 private void invokeCallbackOnInternalInvalidState() { 320 mCallbacks.onFinalErrorReceived(mComponentName, 321 CreateCredentialException.TYPE_UNKNOWN, 322 null); 323 } 324 325 private class ProviderResponseDataHandler { 326 @Nullable 327 private final ComponentName mExpectedRemoteEntryProviderService; 328 329 @NonNull 330 private final Map<String, Pair<CreateEntry, Entry>> mUiCreateEntries = new HashMap<>(); 331 332 @Nullable 333 private Pair<String, Pair<RemoteEntry, Entry>> mUiRemoteEntry = null; 334 ProviderResponseDataHandler(@ullable ComponentName expectedRemoteEntryProviderService)335 ProviderResponseDataHandler(@Nullable ComponentName expectedRemoteEntryProviderService) { 336 mExpectedRemoteEntryProviderService = expectedRemoteEntryProviderService; 337 } 338 addResponseContent(List<CreateEntry> createEntries, RemoteEntry remoteEntry)339 public void addResponseContent(List<CreateEntry> createEntries, 340 RemoteEntry remoteEntry) { 341 createEntries.forEach(this::addCreateEntry); 342 if (remoteEntry != null) { 343 setRemoteEntry(remoteEntry); 344 } 345 } 346 addCreateEntry(CreateEntry createEntry)347 public void addCreateEntry(CreateEntry createEntry) { 348 String id = generateUniqueId(); 349 Entry entry = new Entry(SAVE_ENTRY_KEY, 350 id, createEntry.getSlice(), setUpFillInIntent()); 351 mUiCreateEntries.put(id, new Pair<>(createEntry, entry)); 352 } 353 setRemoteEntry(@ullable RemoteEntry remoteEntry)354 public void setRemoteEntry(@Nullable RemoteEntry remoteEntry) { 355 if (!enforceRemoteEntryRestrictions(mExpectedRemoteEntryProviderService)) { 356 Slog.w(TAG, "Remote entry being dropped as it does not meet the restriction" 357 + "checks."); 358 return; 359 } 360 if (remoteEntry == null) { 361 mUiRemoteEntry = null; 362 return; 363 } 364 String id = generateUniqueId(); 365 Entry entry = new Entry(REMOTE_ENTRY_KEY, 366 id, remoteEntry.getSlice(), setUpFillInIntent()); 367 mUiRemoteEntry = new Pair<>(id, new Pair<>(remoteEntry, entry)); 368 } 369 toCreateCredentialProviderData()370 public CreateCredentialProviderData toCreateCredentialProviderData() { 371 return new CreateCredentialProviderData.Builder( 372 mComponentName.flattenToString()) 373 .setSaveEntries(prepareUiCreateEntries()) 374 .setRemoteEntry(prepareRemoteEntry()) 375 .build(); 376 } 377 prepareUiCreateEntries()378 private List<Entry> prepareUiCreateEntries() { 379 List<Entry> createEntries = new ArrayList<>(); 380 for (String key : mUiCreateEntries.keySet()) { 381 createEntries.add(mUiCreateEntries.get(key).second); 382 } 383 return createEntries; 384 } 385 prepareRemoteEntry()386 private Entry prepareRemoteEntry() { 387 if (mUiRemoteEntry == null || mUiRemoteEntry.first == null 388 || mUiRemoteEntry.second == null) { 389 return null; 390 } 391 return mUiRemoteEntry.second.second; 392 } 393 isEmptyResponse()394 private boolean isEmptyResponse() { 395 return mUiCreateEntries.isEmpty() && mUiRemoteEntry == null; 396 } 397 398 @Nullable getRemoteEntry(String entryKey)399 public RemoteEntry getRemoteEntry(String entryKey) { 400 return mUiRemoteEntry == null || mUiRemoteEntry 401 .first == null || !mUiRemoteEntry.first.equals(entryKey) 402 || mUiRemoteEntry.second == null 403 ? null : mUiRemoteEntry.second.first; 404 } 405 406 @Nullable getCreateEntry(String entryKey)407 public CreateEntry getCreateEntry(String entryKey) { 408 return mUiCreateEntries.get(entryKey) == null 409 ? null : mUiCreateEntries.get(entryKey).first; 410 } 411 isEmptyResponse(BeginCreateCredentialResponse response)412 public boolean isEmptyResponse(BeginCreateCredentialResponse response) { 413 return (response.getCreateEntries() == null || response.getCreateEntries().isEmpty()) 414 && response.getRemoteCreateEntry() == null; 415 } 416 } 417 } 418