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 mRequestSessionMetric.logApiCalledAtFinish(apiStatus); 262 mRequestSessionStatus = RequestSessionStatus.COMPLETE; 263 mProviders.clear(); 264 clearRequestSessionLocked(); 265 } 266 cancelExistingPendingIntent()267 void cancelExistingPendingIntent() { 268 if (mPendingIntent != null) { 269 try { 270 mPendingIntent.cancel(); 271 mPendingIntent = null; 272 } catch (Exception e) { 273 Slog.e(TAG, "Unable to cancel existing pending intent", e); 274 } 275 } 276 } 277 clearRequestSessionLocked()278 private void clearRequestSessionLocked() { 279 synchronized (mLock) { 280 mSessionCallback.onFinishRequestSession(mUserId, mRequestId); 281 } 282 } 283 isAnyProviderPending()284 protected boolean isAnyProviderPending() { 285 for (ProviderSession session : mProviders.values()) { 286 if (ProviderSession.isStatusWaitingForRemoteResponse(session.getStatus())) { 287 return true; 288 } 289 } 290 return false; 291 } 292 isSessionCancelled()293 protected boolean isSessionCancelled() { 294 return mCancellationSignal.isCanceled(); 295 } 296 297 /** 298 * Returns true if at least one provider is ready for UI invocation, and no 299 * provider is pending a response. 300 */ isUiInvocationNeeded()301 protected boolean isUiInvocationNeeded() { 302 for (ProviderSession session : mProviders.values()) { 303 if (ProviderSession.isUiInvokingStatus(session.getStatus())) { 304 return true; 305 } else if (ProviderSession.isStatusWaitingForRemoteResponse(session.getStatus())) { 306 return false; 307 } 308 } 309 return false; 310 } 311 getProviderDataAndInitiateUi()312 void getProviderDataAndInitiateUi() { 313 ArrayList<ProviderData> providerDataList = getProviderDataForUi(); 314 if (!providerDataList.isEmpty()) { 315 launchUiWithProviderData(providerDataList); 316 } 317 } 318 319 @NonNull getProviderDataForUi()320 protected ArrayList<ProviderData> getProviderDataForUi() { 321 Slog.i(TAG, "For ui, provider data size: " + mProviders.size()); 322 ArrayList<ProviderData> providerDataList = new ArrayList<>(); 323 mRequestSessionMetric.logCandidatePhaseMetrics(mProviders); 324 325 if (isSessionCancelled()) { 326 finishSession(/*propagateCancellation=*/true, 327 ApiStatus.CLIENT_CANCELED.getMetricCode()); 328 return providerDataList; 329 } 330 331 for (ProviderSession session : mProviders.values()) { 332 ProviderData providerData = session.prepareUiData(); 333 if (providerData != null) { 334 providerDataList.add(providerData); 335 } 336 } 337 return providerDataList; 338 } 339 340 /** 341 * Allows subclasses to directly finalize the call and set closing metrics on response. 342 * 343 * @param response the response associated with the API call that just completed 344 */ respondToClientWithResponseAndFinish(V response)345 protected void respondToClientWithResponseAndFinish(V response) { 346 mRequestSessionMetric.logCandidateAggregateMetrics(mProviders); 347 mRequestSessionMetric.collectFinalPhaseProviderMetricStatus(/*has_exception=*/ false, 348 ProviderStatusForMetrics.FINAL_SUCCESS); 349 if (mRequestSessionStatus == RequestSessionStatus.COMPLETE) { 350 Slog.w(TAG, "Request has already been completed. This is strange."); 351 return; 352 } 353 if (isSessionCancelled()) { 354 finishSession(/*propagateCancellation=*/true, 355 ApiStatus.CLIENT_CANCELED.getMetricCode()); 356 return; 357 } 358 try { 359 invokeClientCallbackSuccess(response); 360 finishSession(/*propagateCancellation=*/false, 361 ApiStatus.SUCCESS.getMetricCode()); 362 } catch (RemoteException e) { 363 mRequestSessionMetric.collectFinalPhaseProviderMetricStatus( 364 /*has_exception=*/ true, ProviderStatusForMetrics.FINAL_FAILURE); 365 Slog.e(TAG, "Issue while responding to client with a response : " + e); 366 finishSession(/*propagateCancellation=*/false, ApiStatus.FAILURE.getMetricCode()); 367 } 368 } 369 370 /** 371 * Allows subclasses to directly finalize the call and set closing metrics on error completion. 372 * 373 * @param errorType the type of error given back in the flow 374 * @param errorMsg the error message given back in the flow 375 */ respondToClientWithErrorAndFinish(String errorType, String errorMsg)376 protected void respondToClientWithErrorAndFinish(String errorType, String errorMsg) { 377 mRequestSessionMetric.logCandidateAggregateMetrics(mProviders); 378 mRequestSessionMetric.collectFinalPhaseProviderMetricStatus( 379 /*has_exception=*/ true, ProviderStatusForMetrics.FINAL_FAILURE); 380 if (mRequestSessionStatus == RequestSessionStatus.COMPLETE) { 381 Slog.w(TAG, "Request has already been completed. This is strange."); 382 return; 383 } 384 if (isSessionCancelled()) { 385 finishSession(/*propagateCancellation=*/true, ApiStatus.CLIENT_CANCELED.getMetricCode()); 386 return; 387 } 388 389 try { 390 invokeClientCallbackError(errorType, errorMsg); 391 } catch (RemoteException e) { 392 Slog.e(TAG, "Issue while responding to client with error : " + e); 393 } 394 boolean isUserCanceled = errorType.contains(MetricUtilities.USER_CANCELED_SUBSTRING); 395 if (isUserCanceled) { 396 mRequestSessionMetric.setHasExceptionFinalPhase(/* has_exception */ false); 397 finishSession(/*propagateCancellation=*/false, 398 ApiStatus.USER_CANCELED.getMetricCode()); 399 } else { 400 finishSession(/*propagateCancellation=*/false, 401 ApiStatus.FAILURE.getMetricCode()); 402 } 403 } 404 405 /** 406 * Reveals if a certain provider is primary after ensuring it exists at all in the designated 407 * provider info. 408 * 409 * @param componentName used to identify the provider we want to check primary status for 410 */ isPrimaryProviderViaProviderInfo(ComponentName componentName)411 protected boolean isPrimaryProviderViaProviderInfo(ComponentName componentName) { 412 var chosenProviderSession = mProviders.get(componentName.flattenToString()); 413 return chosenProviderSession != null && chosenProviderSession.mProviderInfo != null 414 && chosenProviderSession.mProviderInfo.isPrimary(); 415 } 416 417 private class RequestSessionDeathRecipient implements IBinder.DeathRecipient { 418 @Override binderDied()419 public void binderDied() { 420 Slog.d(TAG, "Client binder died - clearing session"); 421 finishSession(isUiWaitingForData(), ApiStatus.CLIENT_CANCELED.getMetricCode()); 422 } 423 } 424 } 425