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.UserIdInt; 21 import android.app.PendingIntent; 22 import android.content.ComponentName; 23 import android.content.Context; 24 import android.content.Intent; 25 import android.credentials.CredentialManager; 26 import android.credentials.CredentialProviderInfo; 27 import android.credentials.flags.Flags; 28 import android.credentials.selection.ProviderData; 29 import android.credentials.selection.UserSelectionDialogResult; 30 import android.os.Binder; 31 import android.os.CancellationSignal; 32 import android.os.Handler; 33 import android.os.IBinder; 34 import android.os.IInterface; 35 import android.os.Looper; 36 import android.os.RemoteException; 37 import android.os.UserHandle; 38 import android.service.credentials.CallingAppInfo; 39 import android.util.Slog; 40 41 import com.android.internal.R; 42 import com.android.server.credentials.metrics.ApiName; 43 import com.android.server.credentials.metrics.ApiStatus; 44 import com.android.server.credentials.metrics.ProviderSessionMetric; 45 import com.android.server.credentials.metrics.ProviderStatusForMetrics; 46 import com.android.server.credentials.metrics.RequestSessionMetric; 47 48 import java.util.ArrayList; 49 import java.util.Map; 50 import java.util.Set; 51 import java.util.concurrent.ConcurrentHashMap; 52 53 /** 54 * Base class of a request session, that listens to UI events. This class must be extended 55 * every time a new response type is expected from the providers. 56 */ 57 abstract class RequestSession<T, U, V> implements CredentialManagerUi.CredentialManagerUiCallback { 58 private static final String TAG = CredentialManager.TAG; 59 60 public interface SessionLifetime { 61 /** Called when the user makes a selection. */ onFinishRequestSession(@serIdInt int userId, IBinder token)62 void onFinishRequestSession(@UserIdInt int userId, IBinder token); 63 } 64 65 // TODO: Revise access levels of attributes 66 @NonNull 67 protected final T mClientRequest; 68 @NonNull 69 protected final U mClientCallback; 70 @NonNull 71 protected final IBinder mRequestId; 72 @NonNull 73 protected final Context mContext; 74 @NonNull 75 protected final CredentialManagerUi mCredentialManagerUi; 76 @NonNull 77 protected final String mRequestType; 78 @NonNull 79 protected final Handler mHandler; 80 @UserIdInt 81 protected final int mUserId; 82 83 protected final int mUniqueSessionInteger; 84 private final int mCallingUid; 85 @NonNull 86 protected final CallingAppInfo mClientAppInfo; 87 @NonNull 88 protected final CancellationSignal mCancellationSignal; 89 90 protected final Map<String, ProviderSession> mProviders = new ConcurrentHashMap<>(); 91 protected final RequestSessionMetric mRequestSessionMetric; 92 protected final String mHybridService; 93 94 protected final Object mLock; 95 96 protected final SessionLifetime mSessionCallback; 97 98 private final Set<ComponentName> mEnabledProviders; 99 100 private final RequestSessionDeathRecipient mDeathRecipient = 101 new RequestSessionDeathRecipient(); 102 103 protected PendingIntent mPendingIntent; 104 105 @NonNull 106 protected RequestSessionStatus mRequestSessionStatus = 107 RequestSessionStatus.IN_PROGRESS; 108 109 /** The status in which a given request session is. */ 110 enum RequestSessionStatus { 111 /** Request is in progress. This is the status a request session is instantiated with. */ 112 IN_PROGRESS, 113 /** Request has been cancelled by the developer. */ 114 CANCELLED, 115 /** Request is complete. */ 116 COMPLETE 117 } 118 RequestSession(@onNull Context context, RequestSession.SessionLifetime sessionCallback, Object lock, @UserIdInt int userId, int callingUid, @NonNull T clientRequest, U clientCallback, @NonNull String requestType, CallingAppInfo callingAppInfo, Set<ComponentName> enabledProviders, CancellationSignal cancellationSignal, long timestampStarted, boolean shouldBindClientToDeath)119 protected RequestSession(@NonNull Context context, 120 RequestSession.SessionLifetime sessionCallback, 121 Object lock, @UserIdInt int userId, int callingUid, 122 @NonNull T clientRequest, U clientCallback, 123 @NonNull String requestType, 124 CallingAppInfo callingAppInfo, 125 Set<ComponentName> enabledProviders, 126 CancellationSignal cancellationSignal, long timestampStarted, 127 boolean shouldBindClientToDeath) { 128 mContext = context; 129 mLock = lock; 130 mSessionCallback = sessionCallback; 131 mUserId = userId; 132 mCallingUid = callingUid; 133 mClientRequest = clientRequest; 134 mClientCallback = clientCallback; 135 mRequestType = requestType; 136 mClientAppInfo = callingAppInfo; 137 mEnabledProviders = enabledProviders; 138 mCancellationSignal = cancellationSignal; 139 mHandler = new Handler(Looper.getMainLooper(), null, true); 140 mRequestId = new Binder(); 141 mCredentialManagerUi = new CredentialManagerUi(mContext, 142 mUserId, this, mEnabledProviders); 143 mHybridService = context.getResources().getString( 144 R.string.config_defaultCredentialManagerHybridService); 145 mUniqueSessionInteger = MetricUtilities.getHighlyUniqueInteger(); 146 mRequestSessionMetric = new RequestSessionMetric(mUniqueSessionInteger, 147 MetricUtilities.getHighlyUniqueInteger()); 148 mRequestSessionMetric.collectInitialPhaseMetricInfo(timestampStarted, 149 mCallingUid, ApiName.getMetricCodeFromRequestInfo(mRequestType)); 150 setCancellationListener(); 151 if (shouldBindClientToDeath && Flags.clearSessionEnabled()) { 152 if (mClientCallback != null && mClientCallback instanceof IInterface) { 153 setUpClientCallbackListener(((IInterface) mClientCallback).asBinder()); 154 } 155 } 156 } 157 setUpClientCallbackListener(IBinder clientBinder)158 protected void setUpClientCallbackListener(IBinder clientBinder) { 159 if (mClientCallback != null && mClientCallback instanceof IInterface) { 160 IInterface callback = (IInterface) mClientCallback; 161 try { 162 clientBinder.linkToDeath(mDeathRecipient, 0); 163 } catch (RemoteException e) { 164 Slog.e(TAG, e.getMessage()); 165 } 166 } 167 } 168 setCancellationListener()169 private void setCancellationListener() { 170 mCancellationSignal.setOnCancelListener( 171 () -> { 172 Slog.d(TAG, "Cancellation invoked from the client - clearing session"); 173 boolean isUiActive = maybeCancelUi(); 174 finishSession(!isUiActive, ApiStatus.CLIENT_CANCELED.getMetricCode()); 175 } 176 ); 177 } 178 maybeCancelUi()179 private boolean maybeCancelUi() { 180 if (mCredentialManagerUi.getStatus() 181 == CredentialManagerUi.UiStatus.USER_INTERACTION) { 182 final long originalCallingUidToken = Binder.clearCallingIdentity(); 183 try { 184 mContext.startActivityAsUser(mCredentialManagerUi.createCancelIntent( 185 mRequestId, mClientAppInfo.getPackageName()) 186 .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK), UserHandle.of(mUserId)); 187 return true; 188 } finally { 189 Binder.restoreCallingIdentity(originalCallingUidToken); 190 } 191 } 192 return false; 193 } 194 isUiWaitingForData()195 private boolean isUiWaitingForData() { 196 // Technically, the status can also be IN_PROGRESS when the user has made a selection 197 // so this an over estimation, but safe to do so as it is used for cancellation 198 // propagation to the provider in a very narrow time frame. If provider has 199 // already responded, cancellation is not an issue as the cancellation listener 200 // is independent of the service binding. 201 // TODO(b/313512500): Do not propagate cancelation if provider has responded in 202 // query phase. 203 return mCredentialManagerUi.getStatus() == CredentialManagerUi.UiStatus.IN_PROGRESS; 204 } 205 initiateProviderSession(CredentialProviderInfo providerInfo, RemoteCredentialService remoteCredentialService)206 public abstract ProviderSession initiateProviderSession(CredentialProviderInfo providerInfo, 207 RemoteCredentialService remoteCredentialService); 208 launchUiWithProviderData(ArrayList<ProviderData> providerDataList)209 protected abstract void launchUiWithProviderData(ArrayList<ProviderData> providerDataList); 210 invokeClientCallbackSuccess(V response)211 protected abstract void invokeClientCallbackSuccess(V response) throws RemoteException; 212 invokeClientCallbackError(String errorType, String errorMsg)213 protected abstract void invokeClientCallbackError(String errorType, String errorMsg) throws 214 RemoteException; 215 addProviderSession(ComponentName componentName, ProviderSession providerSession)216 public void addProviderSession(ComponentName componentName, ProviderSession providerSession) { 217 mProviders.put(componentName.flattenToString(), providerSession); 218 } 219 220 // UI callbacks 221 222 @Override // from CredentialManagerUiCallbacks onUiSelection(UserSelectionDialogResult selection)223 public void onUiSelection(UserSelectionDialogResult selection) { 224 if (mRequestSessionStatus == RequestSessionStatus.COMPLETE) { 225 Slog.w(TAG, "Request has already been completed. This is strange."); 226 return; 227 } 228 if (isSessionCancelled()) { 229 finishSession(/*propagateCancellation=*/true, 230 ApiStatus.CLIENT_CANCELED.getMetricCode()); 231 return; 232 } 233 String providerId = selection.getProviderId(); 234 ProviderSession providerSession = mProviders.get(providerId); 235 if (providerSession == null) { 236 Slog.w(TAG, "providerSession not found in onUiSelection. This is strange."); 237 return; 238 } 239 240 ProviderSessionMetric providerSessionMetric = providerSession.mProviderSessionMetric; 241 int initialAuthMetricsProvider = providerSessionMetric.getBrowsedAuthenticationMetric() 242 .size(); 243 mRequestSessionMetric.collectMetricPerBrowsingSelect(selection, 244 providerSession.mProviderSessionMetric.getCandidatePhasePerProviderMetric()); 245 providerSession.onUiEntrySelected(selection.getEntryKey(), 246 selection.getEntrySubkey(), selection.getPendingIntentProviderResponse()); 247 int numAuthPerProvider = providerSessionMetric.getBrowsedAuthenticationMetric().size(); 248 boolean authMetricLogged = (numAuthPerProvider - initialAuthMetricsProvider) == 1; 249 if (authMetricLogged) { 250 mRequestSessionMetric.logAuthEntry( 251 providerSession.mProviderSessionMetric.getBrowsedAuthenticationMetric() 252 .get(numAuthPerProvider - 1)); 253 } 254 } 255 finishSession(boolean propagateCancellation, int apiStatus)256 protected void finishSession(boolean propagateCancellation, int apiStatus) { 257 Slog.i(TAG, "finishing session with propagateCancellation " + propagateCancellation); 258 if (propagateCancellation) { 259 mProviders.values().forEach(ProviderSession::cancelProviderRemoteSession); 260 } 261 mRequestSessionStatus = RequestSessionStatus.COMPLETE; 262 if (Flags.fixMetricDuplicationEmits()) { 263 logTrackOneCandidatesAndPrepareFinalPhaseLogs(apiStatus); 264 } 265 mRequestSessionMetric.logApiCalledAtFinish(apiStatus); 266 mProviders.clear(); 267 clearRequestSessionLocked(); 268 } 269 270 /** 271 * Ensures all logging done in final phase methods only occur within the 'finishSession'. 272 */ logTrackOneCandidatesAndPrepareFinalPhaseLogs(int apiStatus)273 private void logTrackOneCandidatesAndPrepareFinalPhaseLogs(int apiStatus) { 274 mRequestSessionMetric.logCandidateAggregateMetrics(mProviders); 275 if (isRespondingWithError(apiStatus)) { 276 mRequestSessionMetric.collectFinalPhaseProviderMetricStatus( 277 /*hasException=*/ true, ProviderStatusForMetrics.FINAL_FAILURE); 278 } else if (isRespondingWithUserCanceledError(apiStatus)) { 279 mRequestSessionMetric.collectFinalPhaseProviderMetricStatus( 280 /*hasException=*/false, ProviderStatusForMetrics.FINAL_FAILURE 281 ); 282 } else if (isRespondingWithSuccess(apiStatus)) { 283 mRequestSessionMetric.collectFinalPhaseProviderMetricStatus(/*hasException=*/ false, 284 ProviderStatusForMetrics.FINAL_SUCCESS); 285 } 286 } 287 cancelExistingPendingIntent()288 void cancelExistingPendingIntent() { 289 if (mPendingIntent != null) { 290 try { 291 mPendingIntent.cancel(); 292 mPendingIntent = null; 293 } catch (Exception e) { 294 Slog.e(TAG, "Unable to cancel existing pending intent", e); 295 } 296 } 297 } 298 clearRequestSessionLocked()299 private void clearRequestSessionLocked() { 300 synchronized (mLock) { 301 mSessionCallback.onFinishRequestSession(mUserId, mRequestId); 302 } 303 } 304 isAnyProviderPending()305 protected boolean isAnyProviderPending() { 306 for (ProviderSession session : mProviders.values()) { 307 if (ProviderSession.isStatusWaitingForRemoteResponse(session.getStatus())) { 308 return true; 309 } 310 } 311 return false; 312 } 313 isSessionCancelled()314 protected boolean isSessionCancelled() { 315 return mCancellationSignal.isCanceled(); 316 } 317 318 /** 319 * Returns true if at least one provider is ready for UI invocation, and no 320 * provider is pending a response. 321 */ isUiInvocationNeeded()322 protected boolean isUiInvocationNeeded() { 323 for (ProviderSession session : mProviders.values()) { 324 if (ProviderSession.isUiInvokingStatus(session.getStatus())) { 325 return true; 326 } else if (ProviderSession.isStatusWaitingForRemoteResponse(session.getStatus())) { 327 return false; 328 } 329 } 330 return false; 331 } 332 getProviderDataAndInitiateUi()333 void getProviderDataAndInitiateUi() { 334 ArrayList<ProviderData> providerDataList = getProviderDataForUi(); 335 if (!providerDataList.isEmpty()) { 336 launchUiWithProviderData(providerDataList); 337 } 338 } 339 340 @NonNull getProviderDataForUi()341 protected ArrayList<ProviderData> getProviderDataForUi() { 342 Slog.i(TAG, "For ui, provider data size: " + mProviders.size()); 343 ArrayList<ProviderData> providerDataList = new ArrayList<>(); 344 mRequestSessionMetric.logCandidatePhaseMetrics(mProviders); 345 346 if (isSessionCancelled()) { 347 finishSession(/*propagateCancellation=*/true, 348 ApiStatus.CLIENT_CANCELED.getMetricCode()); 349 return providerDataList; 350 } 351 352 for (ProviderSession session : mProviders.values()) { 353 ProviderData providerData = session.prepareUiData(); 354 if (providerData != null) { 355 providerDataList.add(providerData); 356 } 357 } 358 return providerDataList; 359 } 360 361 /** 362 * Allows subclasses to directly finalize the call and set closing metrics on response. 363 * 364 * @param response the response associated with the API call that just completed 365 */ respondToClientWithResponseAndFinish(V response)366 protected void respondToClientWithResponseAndFinish(V response) { 367 if (!Flags.fixMetricDuplicationEmits()) { 368 mRequestSessionMetric.logCandidateAggregateMetrics(mProviders); 369 mRequestSessionMetric.collectFinalPhaseProviderMetricStatus(/*hasException=*/ false, 370 ProviderStatusForMetrics.FINAL_SUCCESS); 371 } 372 if (mRequestSessionStatus == RequestSessionStatus.COMPLETE) { 373 Slog.w(TAG, "Request has already been completed. This is strange."); 374 return; 375 } 376 if (isSessionCancelled()) { 377 finishSession(/*propagateCancellation=*/true, 378 ApiStatus.CLIENT_CANCELED.getMetricCode()); 379 return; 380 } 381 try { 382 invokeClientCallbackSuccess(response); 383 finishSession(/*propagateCancellation=*/false, 384 ApiStatus.SUCCESS.getMetricCode()); 385 } catch (RemoteException e) { 386 if (!Flags.fixMetricDuplicationEmits()) { 387 mRequestSessionMetric.collectFinalPhaseProviderMetricStatus( 388 /*hasException=*/ true, ProviderStatusForMetrics.FINAL_FAILURE); 389 } 390 Slog.e(TAG, "Issue while responding to client with a response : " + e); 391 finishSession(/*propagateCancellation=*/false, ApiStatus.FAILURE.getMetricCode()); 392 } 393 } 394 395 /** 396 * Allows subclasses to directly finalize the call and set closing metrics on error completion. 397 * 398 * @param errorType the type of error given back in the flow 399 * @param errorMsg the error message given back in the flow 400 */ respondToClientWithErrorAndFinish(String errorType, String errorMsg)401 protected void respondToClientWithErrorAndFinish(String errorType, String errorMsg) { 402 if (!Flags.fixMetricDuplicationEmits()) { 403 mRequestSessionMetric.logCandidateAggregateMetrics(mProviders); 404 mRequestSessionMetric.collectFinalPhaseProviderMetricStatus( 405 /*hasException=*/ true, ProviderStatusForMetrics.FINAL_FAILURE); 406 } 407 if (mRequestSessionStatus == RequestSessionStatus.COMPLETE) { 408 Slog.w(TAG, "Request has already been completed. This is strange."); 409 return; 410 } 411 if (isSessionCancelled()) { 412 finishSession(/*propagateCancellation=*/true, ApiStatus.CLIENT_CANCELED.getMetricCode()); 413 return; 414 } 415 try { 416 invokeClientCallbackError(errorType, errorMsg); 417 } catch (RemoteException e) { 418 Slog.e(TAG, "Issue while responding to client with error : " + e); 419 } 420 boolean isUserCanceled = errorType.contains(MetricUtilities.USER_CANCELED_SUBSTRING); 421 if (isUserCanceled) { 422 if (!Flags.fixMetricDuplicationEmits()) { 423 mRequestSessionMetric.setHasExceptionFinalPhase(/* hasException */ false); 424 } 425 finishSession(/*propagateCancellation=*/false, 426 ApiStatus.USER_CANCELED.getMetricCode()); 427 } else { 428 finishSession(/*propagateCancellation=*/false, 429 ApiStatus.FAILURE.getMetricCode()); 430 } 431 } 432 433 /** 434 * Reveals if a certain provider is primary after ensuring it exists at all in the designated 435 * provider info. 436 * 437 * @param componentName used to identify the provider we want to check primary status for 438 */ isPrimaryProviderViaProviderInfo(ComponentName componentName)439 protected boolean isPrimaryProviderViaProviderInfo(ComponentName componentName) { 440 var chosenProviderSession = mProviders.get(componentName.flattenToString()); 441 return chosenProviderSession != null && chosenProviderSession.mProviderInfo != null 442 && chosenProviderSession.mProviderInfo.isPrimary(); 443 } 444 445 private class RequestSessionDeathRecipient implements IBinder.DeathRecipient { 446 @Override binderDied()447 public void binderDied() { 448 Slog.d(TAG, "Client binder died - clearing session"); 449 finishSession(isUiWaitingForData(), ApiStatus.BINDER_DIED.getMetricCode()); 450 } 451 } 452 453 /** 454 * This captures the final state of the apiStatus as presented in 'finishSession'. 455 */ isRespondingWithError(int apiStatus)456 private boolean isRespondingWithError(int apiStatus) { 457 return apiStatus == ApiStatus.FAILURE.getMetricCode() 458 || apiStatus == ApiStatus.CLIENT_CANCELED.getMetricCode(); 459 } 460 461 /** 462 * A unique failure case, where we do not set the exception bit to be true. 463 */ isRespondingWithUserCanceledError(int apiStatus)464 private boolean isRespondingWithUserCanceledError(int apiStatus) { 465 return apiStatus == ApiStatus.USER_CANCELED.getMetricCode(); 466 } 467 468 /** 469 * This captures the final state of the apiStatus as presented in 'finishSession'. 470 */ isRespondingWithSuccess(int apiStatus)471 private boolean isRespondingWithSuccess(int apiStatus) { 472 return apiStatus == ApiStatus.SUCCESS.getMetricCode(); 473 } 474 } 475