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