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