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