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