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.Manifest;
20 import android.annotation.Nullable;
21 import android.content.ComponentName;
22 import android.content.Context;
23 import android.credentials.CredentialManager;
24 import android.credentials.CredentialOption;
25 import android.credentials.CredentialProviderInfo;
26 import android.credentials.GetCredentialException;
27 import android.credentials.GetCredentialRequest;
28 import android.credentials.GetCredentialResponse;
29 import android.credentials.IGetCredentialCallback;
30 import android.credentials.selection.ProviderData;
31 import android.credentials.selection.RequestInfo;
32 import android.os.Binder;
33 import android.os.CancellationSignal;
34 import android.os.RemoteException;
35 import android.service.credentials.CallingAppInfo;
36 import android.service.credentials.PermissionUtils;
37 import android.util.Slog;
38 
39 import com.android.server.credentials.metrics.ProviderStatusForMetrics;
40 
41 import java.util.ArrayList;
42 import java.util.Set;
43 
44 /**
45  * Central session for a single getCredentials request. This class listens to the
46  * responses from providers, and the UX app, and updates the provider(S) state.
47  */
48 public class GetRequestSession extends RequestSession<GetCredentialRequest,
49         IGetCredentialCallback, GetCredentialResponse>
50         implements ProviderSession.ProviderInternalCallback<GetCredentialResponse> {
51     private static final String TAG = CredentialManager.TAG;
52 
GetRequestSession(Context context, RequestSession.SessionLifetime sessionCallback, Object lock, int userId, int callingUid, IGetCredentialCallback callback, GetCredentialRequest request, CallingAppInfo callingAppInfo, Set<ComponentName> enabledProviders, CancellationSignal cancellationSignal, long startedTimestamp)53     public GetRequestSession(Context context, RequestSession.SessionLifetime sessionCallback,
54             Object lock, int userId, int callingUid,
55             IGetCredentialCallback callback, GetCredentialRequest request,
56             CallingAppInfo callingAppInfo, Set<ComponentName> enabledProviders,
57             CancellationSignal cancellationSignal,
58             long startedTimestamp) {
59         super(context, sessionCallback, lock, userId, callingUid, request, callback,
60                 getRequestInfoFromRequest(request), callingAppInfo, enabledProviders,
61                 cancellationSignal, startedTimestamp, /*shouldBindClientToDeath=*/ true);
62         mRequestSessionMetric.collectGetFlowInitialMetricInfo(request);
63     }
64 
getRequestInfoFromRequest(GetCredentialRequest request)65     private static String getRequestInfoFromRequest(GetCredentialRequest request) {
66         for (CredentialOption option : request.getCredentialOptions()) {
67             if (option.getCredentialRetrievalData().getStringArrayList(
68                     CredentialOption
69                             .SUPPORTED_ELEMENT_KEYS) != null) {
70                 return RequestInfo.TYPE_GET_VIA_REGISTRY;
71             }
72         }
73         return RequestInfo.TYPE_GET;
74     }
75 
76     /**
77      * Creates a new provider session, and adds it list of providers that are contributing to
78      * this session.
79      *
80      * @return the provider session created within this request session, for the given provider
81      * info.
82      */
83     @Override
84     @Nullable
initiateProviderSession(CredentialProviderInfo providerInfo, RemoteCredentialService remoteCredentialService)85     public ProviderSession initiateProviderSession(CredentialProviderInfo providerInfo,
86             RemoteCredentialService remoteCredentialService) {
87         ProviderGetSession providerGetSession = ProviderGetSession
88                 .createNewSession(mContext, mUserId, providerInfo,
89                         this, remoteCredentialService);
90         if (providerGetSession != null) {
91             Slog.i(TAG, "Provider session created and "
92                     + "being added for: " + providerInfo.getComponentName());
93             mProviders.put(providerGetSession.getComponentName().flattenToString(),
94                     providerGetSession);
95         }
96         return providerGetSession;
97     }
98 
99     @Override
launchUiWithProviderData(ArrayList<ProviderData> providerDataList)100     protected void launchUiWithProviderData(ArrayList<ProviderData> providerDataList) {
101         mRequestSessionMetric.collectUiCallStartTime(System.nanoTime());
102         mCredentialManagerUi.setStatus(CredentialManagerUi.UiStatus.USER_INTERACTION);
103         Binder.withCleanCallingIdentity(() -> {
104             try {
105                 cancelExistingPendingIntent();
106                 mPendingIntent = mCredentialManagerUi.createPendingIntent(
107                         RequestInfo.newGetRequestInfo(
108                                 mRequestId, mClientRequest, mClientAppInfo.getPackageName(),
109                                 PermissionUtils.hasPermission(mContext,
110                                         mClientAppInfo.getPackageName(),
111                                         Manifest.permission
112                                                 .CREDENTIAL_MANAGER_SET_ALLOWED_PROVIDERS),
113                                 /*isShowAllOptionsRequested=*/ false),
114                         providerDataList,
115                         mRequestSessionMetric);
116                 mClientCallback.onPendingIntent(mPendingIntent);
117             } catch (RemoteException e) {
118                 mRequestSessionMetric.collectUiReturnedFinalPhase(/*uiReturned=*/ false);
119                 mCredentialManagerUi.setStatus(CredentialManagerUi.UiStatus.TERMINATED);
120                 String exception = GetCredentialException.TYPE_UNKNOWN;
121                 mRequestSessionMetric.collectFrameworkException(exception);
122                 respondToClientWithErrorAndFinish(exception, "Unable to instantiate selector");
123             }
124         });
125     }
126 
127     @Override
invokeClientCallbackSuccess(GetCredentialResponse response)128     protected void invokeClientCallbackSuccess(GetCredentialResponse response)
129             throws RemoteException {
130         mClientCallback.onResponse(response);
131     }
132 
133     @Override
invokeClientCallbackError(String errorType, String errorMsg)134     protected void invokeClientCallbackError(String errorType, String errorMsg)
135             throws RemoteException {
136         mClientCallback.onError(errorType, errorMsg);
137     }
138 
139     @Override
onFinalResponseReceived(ComponentName componentName, @Nullable GetCredentialResponse response)140     public void onFinalResponseReceived(ComponentName componentName,
141             @Nullable GetCredentialResponse response) {
142         Slog.i(TAG, "onFinalResponseReceived from: " + componentName.flattenToString());
143         mRequestSessionMetric.collectUiResponseData(/*uiReturned=*/ true, System.nanoTime());
144         mRequestSessionMetric.updateMetricsOnResponseReceived(mProviders, componentName,
145                 isPrimaryProviderViaProviderInfo(componentName));
146         if (response != null) {
147             mRequestSessionMetric.collectChosenProviderStatus(
148                     ProviderStatusForMetrics.FINAL_SUCCESS.getMetricCode());
149             respondToClientWithResponseAndFinish(response);
150         } else {
151             mRequestSessionMetric.collectChosenProviderStatus(
152                     ProviderStatusForMetrics.FINAL_FAILURE.getMetricCode());
153             String exception = GetCredentialException.TYPE_NO_CREDENTIAL;
154             mRequestSessionMetric.collectFrameworkException(exception);
155             respondToClientWithErrorAndFinish(exception,
156                     "Invalid response from provider");
157         }
158     }
159 
160     //TODO(b/274954697): Further shorten the three below to completely migrate to superclass
161     @Override
onFinalErrorReceived(ComponentName componentName, String errorType, String message)162     public void onFinalErrorReceived(ComponentName componentName, String errorType,
163             String message) {
164         respondToClientWithErrorAndFinish(errorType, message);
165     }
166 
167     @Override
onUiCancellation(boolean isUserCancellation)168     public void onUiCancellation(boolean isUserCancellation) {
169         String exception = GetCredentialException.TYPE_USER_CANCELED;
170         String message = "User cancelled the selector";
171         if (!isUserCancellation) {
172             exception = GetCredentialException.TYPE_INTERRUPTED;
173             message = "The UI was interrupted - please try again.";
174         }
175         mRequestSessionMetric.collectFrameworkException(exception);
176         respondToClientWithErrorAndFinish(exception, message);
177     }
178 
179     @Override
onUiSelectorInvocationFailure()180     public void onUiSelectorInvocationFailure() {
181         String exception = GetCredentialException.TYPE_NO_CREDENTIAL;
182         mRequestSessionMetric.collectFrameworkException(exception);
183         respondToClientWithErrorAndFinish(exception,
184                 "No credentials available.");
185     }
186 
187     @Override
onProviderStatusChanged(ProviderSession.Status status, ComponentName componentName, ProviderSession.CredentialsSource source)188     public void onProviderStatusChanged(ProviderSession.Status status,
189             ComponentName componentName, ProviderSession.CredentialsSource source) {
190         Slog.i(TAG, "Status changed for: " + componentName + ", with status: "
191                 + status + ", and source: " + source);
192 
193         // Auth entry was selected, and it did not have any underlying credentials
194         if (status == ProviderSession.Status.NO_CREDENTIALS_FROM_AUTH_ENTRY) {
195             handleEmptyAuthenticationSelection(componentName);
196             return;
197         }
198         // For any other status, we check if all providers are done and then invoke UI if needed
199         if (!isAnyProviderPending()) {
200             // If all provider responses have been received, we can either need the UI,
201             // or we need to respond with error. The only other case is the entry being
202             // selected after the UI has been invoked which has a separate code path.
203             if (isUiInvocationNeeded()) {
204                 Slog.i(TAG, "Provider status changed - ui invocation is needed");
205                 getProviderDataAndInitiateUi();
206             } else {
207                 String exception = GetCredentialException.TYPE_NO_CREDENTIAL;
208                 mRequestSessionMetric.collectFrameworkException(exception);
209                 respondToClientWithErrorAndFinish(exception,
210                         "No credentials available");
211             }
212         }
213     }
214 
handleEmptyAuthenticationSelection(ComponentName componentName)215     protected void handleEmptyAuthenticationSelection(ComponentName componentName) {
216         // Update auth entry statuses across different provider sessions
217         mProviders.keySet().forEach(key -> {
218             ProviderGetSession session = (ProviderGetSession) mProviders.get(key);
219             if (!session.mComponentName.equals(componentName)) {
220                 session.updateAuthEntriesStatusFromAnotherSession();
221             }
222         });
223 
224         // Invoke UI since it needs to show a snackbar if last auth entry, or a status on each
225         // auth entries along with other valid entries
226         getProviderDataAndInitiateUi();
227 
228         // Respond to client if all auth entries are empty and nothing else to show on the UI
229         if (providerDataContainsEmptyAuthEntriesOnly()) {
230             String exception = GetCredentialException.TYPE_NO_CREDENTIAL;
231             mRequestSessionMetric.collectFrameworkException(exception);
232             respondToClientWithErrorAndFinish(exception,
233                     "No credentials available");
234         }
235     }
236 
providerDataContainsEmptyAuthEntriesOnly()237     private boolean providerDataContainsEmptyAuthEntriesOnly() {
238         for (String key : mProviders.keySet()) {
239             ProviderGetSession session = (ProviderGetSession) mProviders.get(key);
240             if (!session.containsEmptyAuthEntriesOnly()) {
241                 return false;
242             }
243         }
244         return true;
245     }
246 }
247