1 /*
2  * Copyright (C) 2023 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 static com.google.common.truth.Truth.assertThat;
20 
21 import static org.mockito.ArgumentMatchers.anySet;
22 import static org.mockito.Mockito.anyString;
23 import static org.mockito.Mockito.verify;
24 import static org.mockito.Mockito.verifyZeroInteractions;
25 import static org.mockito.Mockito.when;
26 import static org.testng.Assert.assertThrows;
27 
28 import android.app.Activity;
29 import android.app.slice.Slice;
30 import android.app.slice.SliceSpec;
31 import android.content.ComponentName;
32 import android.content.Context;
33 import android.content.Intent;
34 import android.content.pm.ServiceInfo;
35 import android.content.pm.Signature;
36 import android.content.pm.SigningDetails;
37 import android.content.pm.SigningInfo;
38 import android.credentials.Credential;
39 import android.credentials.CredentialOption;
40 import android.credentials.GetCredentialException;
41 import android.credentials.GetCredentialResponse;
42 import android.credentials.ui.GetCredentialProviderData;
43 import android.credentials.ui.ProviderPendingIntentResponse;
44 import android.net.Uri;
45 import android.os.Bundle;
46 import android.service.credentials.CallingAppInfo;
47 import android.service.credentials.CredentialEntry;
48 import android.service.credentials.CredentialProviderService;
49 import android.service.credentials.GetCredentialRequest;
50 
51 import androidx.test.core.app.ApplicationProvider;
52 import androidx.test.filters.SmallTest;
53 import androidx.test.runner.AndroidJUnit4;
54 
55 import org.junit.Before;
56 import org.junit.Test;
57 import org.junit.runner.RunWith;
58 import org.mockito.ArgumentCaptor;
59 import org.mockito.Mock;
60 import org.mockito.MockitoAnnotations;
61 
62 import java.security.cert.CertificateException;
63 import java.util.ArrayList;
64 import java.util.HashSet;
65 import java.util.List;
66 import java.util.Set;
67 
68 /**
69  * Tests for CredentialDescriptionRegistry.
70  *
71  * atest FrameworksServicesTests:com.android.server.credentials.ProviderRegistryGetSessionTest
72  */
73 @RunWith(AndroidJUnit4.class)
74 @SmallTest
75 public class ProviderRegistryGetSessionTest {
76 
77     private static final String CALLING_PACKAGE_NAME = "com.credman.app";
78     private static final int USER_ID_1 = 1;
79     private static final ArrayList<String> FLATTENED_REQUEST =
80             new ArrayList<>(List.of("FLATTENED_REQ"));
81     private static final String CP_SERVICE_NAME = "CredentialProvider";
82     private static final ComponentName CREDENTIAL_PROVIDER_COMPONENT =
83             new ComponentName(CALLING_PACKAGE_NAME, CP_SERVICE_NAME);
84     private static final String GET_CREDENTIAL_EXCEPTION_TYPE = "TYPE";
85     private static final String CREDENTIAL_TYPE = "MDOC";
86     private static final String GET_CREDENTIAL_EXCEPTION_MESSAGE = "MESSAGE";
87 
88     private ProviderRegistryGetSession mProviderRegistryGetSession;
89     @Mock private GetRequestSession mGetRequestSession;
90     private CredentialOption mGetCredentialOption;
91     @Mock private ServiceInfo mServiceInfo;
92     private CallingAppInfo mCallingAppInfo;
93     @Mock private CredentialDescriptionRegistry mCredentialDescriptionRegistry;
94     private Bundle mRetrievalData;
95     @Mock private CredentialEntry mEntry;
96     @Mock private CredentialEntry mEntry2;
97     private Slice mSlice;
98     private Slice mSlice2;
99     private CredentialDescriptionRegistry.FilterResult mResult;
100     private Set<CredentialDescriptionRegistry.FilterResult> mResponse;
101 
102     @SuppressWarnings("GuardedBy")
103     @Before
setUp()104     public void setUp() throws CertificateException {
105         MockitoAnnotations.initMocks(this);
106         final Context context = ApplicationProvider.getApplicationContext();
107         mRetrievalData = new Bundle();
108         mRetrievalData.putStringArrayList(CredentialOption.SUPPORTED_ELEMENT_KEYS,
109                 FLATTENED_REQUEST);
110         mCallingAppInfo = createCallingAppInfo();
111         mGetCredentialOption = new CredentialOption(CREDENTIAL_TYPE, mRetrievalData,
112                 new Bundle(), false);
113         when(mServiceInfo.getComponentName()).thenReturn(CREDENTIAL_PROVIDER_COMPONENT);
114         CredentialDescriptionRegistry.setSession(USER_ID_1, mCredentialDescriptionRegistry);
115         mResponse = new HashSet<>();
116         mSlice = createSlice();
117         mSlice2 = createSlice();
118         when(mEntry.getSlice()).thenReturn(mSlice);
119         when(mEntry2.getSlice()).thenReturn(mSlice2);
120         mResult = new CredentialDescriptionRegistry.FilterResult(CALLING_PACKAGE_NAME,
121                 new HashSet<>(FLATTENED_REQUEST),
122                 List.of(mEntry, mEntry2));
123         mResponse.add(mResult);
124         when(mCredentialDescriptionRegistry.getFilteredResultForProvider(anyString(), anySet()))
125                 .thenReturn(mResponse);
126         mProviderRegistryGetSession = ProviderRegistryGetSession
127                 .createNewSession(context, USER_ID_1, mGetRequestSession,
128                         mCallingAppInfo,
129                         CALLING_PACKAGE_NAME,
130                         mGetCredentialOption);
131     }
132 
133     @Test
testInvokeSession_existingProvider_setsResults()134     public void testInvokeSession_existingProvider_setsResults() {
135         final ArgumentCaptor<String> packageNameCaptor = ArgumentCaptor.forClass(String.class);
136         final ArgumentCaptor<Set<String>> flattenedRequestCaptor =
137                 ArgumentCaptor.forClass(Set.class);
138         final ArgumentCaptor<ProviderSession.Status> statusCaptor =
139                 ArgumentCaptor.forClass(ProviderSession.Status.class);
140         final ArgumentCaptor<ComponentName> cpComponentNameCaptor =
141                 ArgumentCaptor.forClass(ComponentName.class);
142 
143         mProviderRegistryGetSession.invokeSession();
144 
145         verify(mCredentialDescriptionRegistry).getFilteredResultForProvider(
146                 packageNameCaptor.capture(),
147                 flattenedRequestCaptor.capture());
148         assertThat(packageNameCaptor.getValue()).isEqualTo(CALLING_PACKAGE_NAME);
149         assertThat(flattenedRequestCaptor.getValue()).containsExactly(FLATTENED_REQUEST);
150         verify(mGetRequestSession).onProviderStatusChanged(statusCaptor.capture(),
151                 cpComponentNameCaptor.capture(), ProviderSession.CredentialsSource.REGISTRY);
152         assertThat(statusCaptor.getValue()).isEqualTo(ProviderSession.Status.CREDENTIALS_RECEIVED);
153         assertThat(cpComponentNameCaptor.getValue()).isEqualTo(CREDENTIAL_PROVIDER_COMPONENT);
154         assertThat(mProviderRegistryGetSession.mCredentialEntries).hasSize(2);
155         assertThat(mProviderRegistryGetSession.mCredentialEntries.get(0)).isSameInstanceAs(mEntry);
156         assertThat(mProviderRegistryGetSession.mCredentialEntries.get(1)).isSameInstanceAs(mEntry2);
157     }
158 
159     @Test
testPrepareUiData_statusNonUIInvoking_throwsIllegalStateException()160     public void testPrepareUiData_statusNonUIInvoking_throwsIllegalStateException() {
161         mProviderRegistryGetSession.setStatus(ProviderSession.Status.CREDENTIALS_RECEIVED);
162 
163         assertThrows(IllegalStateException.class,
164                 () -> mProviderRegistryGetSession.prepareUiData());
165     }
166 
167     @Test
testPrepareUiData_statusUIInvokingNoResults_returnsNull()168     public void testPrepareUiData_statusUIInvokingNoResults_returnsNull() {
169         mProviderRegistryGetSession.setStatus(ProviderSession.Status.CANCELED);
170 
171         assertThat(mProviderRegistryGetSession.prepareUiData()).isNull();
172     }
173 
174     @Test
testPrepareUiData_invokeCalledSuccessfully_returnsCorrectData()175     public void testPrepareUiData_invokeCalledSuccessfully_returnsCorrectData() {
176         mProviderRegistryGetSession.invokeSession();
177         GetCredentialProviderData providerData = (GetCredentialProviderData)
178                 mProviderRegistryGetSession.prepareUiData();
179 
180         assertThat(providerData).isNotNull();
181         assertThat(providerData.getCredentialEntries()).hasSize(2);
182         assertThat(providerData.getCredentialEntries().get(0).getSlice()).isSameInstanceAs(mSlice);
183         assertThat(providerData.getCredentialEntries().get(1).getSlice()).isSameInstanceAs(mSlice2);
184         Intent intent = providerData.getCredentialEntries().get(0).getFrameworkExtrasIntent();
185         GetCredentialRequest getRequest = intent.getParcelableExtra(CredentialProviderService
186                 .EXTRA_GET_CREDENTIAL_REQUEST, GetCredentialRequest.class);
187         assertThat(getRequest.getCallingAppInfo()).isSameInstanceAs(mCallingAppInfo);
188         assertThat(getRequest.getCredentialOptions().get(0)).isSameInstanceAs(mGetCredentialOption);
189         Intent intent2 = providerData.getCredentialEntries().get(0).getFrameworkExtrasIntent();
190         GetCredentialRequest getRequest2 = intent2.getParcelableExtra(CredentialProviderService
191                 .EXTRA_GET_CREDENTIAL_REQUEST, GetCredentialRequest.class);
192         assertThat(getRequest2.getCallingAppInfo()).isSameInstanceAs(mCallingAppInfo);
193         assertThat(getRequest2.getCredentialOptions().get(0))
194                 .isSameInstanceAs(mGetCredentialOption);
195     }
196 
197     @Test
testOnUiEntrySelected_wrongEntryKey_doesNothing()198     public void testOnUiEntrySelected_wrongEntryKey_doesNothing() {
199         final Intent intent = new Intent();
200         final GetCredentialResponse response =
201                 new GetCredentialResponse(new Credential(CREDENTIAL_TYPE, new Bundle()));
202         intent.putExtra(CredentialProviderService.EXTRA_GET_CREDENTIAL_RESPONSE, response);
203         final ProviderPendingIntentResponse providerPendingIntentResponse = new
204                 ProviderPendingIntentResponse(Activity.RESULT_OK, intent);
205 
206         mProviderRegistryGetSession.onUiEntrySelected(
207                 ProviderRegistryGetSession.CREDENTIAL_ENTRY_KEY,
208                 "unsupportedKey", providerPendingIntentResponse);
209 
210         verifyZeroInteractions(mGetRequestSession);
211     }
212 
213     @Test
testOnUiEntrySelected_nullPendingIntentResponse_doesNothing()214     public void testOnUiEntrySelected_nullPendingIntentResponse_doesNothing() {
215         mProviderRegistryGetSession.onUiEntrySelected(
216                 ProviderRegistryGetSession.CREDENTIAL_ENTRY_KEY,
217                 ProviderRegistryGetSession.CREDENTIAL_ENTRY_KEY, null);
218 
219         verifyZeroInteractions(mGetRequestSession);
220     }
221 
222     @Test
testOnUiEntrySelected_pendingIntentWithException_callbackWithGivenException()223     public void testOnUiEntrySelected_pendingIntentWithException_callbackWithGivenException() {
224         final ArgumentCaptor<String> exceptionTypeCaptor =
225                 ArgumentCaptor.forClass(String.class);
226         final ArgumentCaptor<String> exceptionMessageCaptor =
227                 ArgumentCaptor.forClass(String.class);
228         final ArgumentCaptor<ComponentName> cpComponentNameCaptor =
229                 ArgumentCaptor.forClass(ComponentName.class);
230         final ArgumentCaptor<ProviderSession.Status> statusCaptor =
231                 ArgumentCaptor.forClass(ProviderSession.Status.class);
232         final GetCredentialException exception =
233                 new GetCredentialException(GET_CREDENTIAL_EXCEPTION_TYPE,
234                         GET_CREDENTIAL_EXCEPTION_MESSAGE);
235         mProviderRegistryGetSession.invokeSession();
236         GetCredentialProviderData providerData = (GetCredentialProviderData)
237                 mProviderRegistryGetSession.prepareUiData();
238         String entryKey = providerData.getCredentialEntries().get(0).getSubkey();
239         final Intent intent = new Intent();
240         intent.putExtra(CredentialProviderService.EXTRA_GET_CREDENTIAL_EXCEPTION, exception);
241         final ProviderPendingIntentResponse providerPendingIntentResponse = new
242                 ProviderPendingIntentResponse(Activity.RESULT_OK, intent);
243 
244         mProviderRegistryGetSession.onUiEntrySelected(
245                 ProviderRegistryGetSession.CREDENTIAL_ENTRY_KEY,
246                 entryKey, providerPendingIntentResponse);
247 
248         assertThat(statusCaptor.getValue()).isEqualTo(ProviderSession.Status.CREDENTIALS_RECEIVED);
249         verify(mGetRequestSession).onFinalErrorReceived(cpComponentNameCaptor.capture(),
250                 exceptionTypeCaptor.capture(), exceptionMessageCaptor.capture());
251         assertThat(cpComponentNameCaptor.getValue())
252                 .isSameInstanceAs(CREDENTIAL_PROVIDER_COMPONENT);
253         assertThat(exceptionTypeCaptor.getValue()).isEqualTo(GET_CREDENTIAL_EXCEPTION_TYPE);
254         assertThat(exceptionMessageCaptor.getValue()).isEqualTo(GET_CREDENTIAL_EXCEPTION_MESSAGE);
255     }
256 
257     @Test
testOnUiEntrySelected_pendingIntentWithException_callbackWithCancelledException()258     public void testOnUiEntrySelected_pendingIntentWithException_callbackWithCancelledException() {
259         final ArgumentCaptor<String> exceptionTypeCaptor =
260                 ArgumentCaptor.forClass(String.class);
261         final ArgumentCaptor<String> exceptionMessageCaptor =
262                 ArgumentCaptor.forClass(String.class);
263         final ArgumentCaptor<ComponentName> cpComponentNameCaptor =
264                 ArgumentCaptor.forClass(ComponentName.class);
265         final ArgumentCaptor<ProviderSession.Status> statusCaptor =
266                 ArgumentCaptor.forClass(ProviderSession.Status.class);
267 
268         mProviderRegistryGetSession.invokeSession();
269         GetCredentialProviderData providerData = (GetCredentialProviderData)
270                 mProviderRegistryGetSession.prepareUiData();
271         String entryKey = providerData.getCredentialEntries().get(0).getSubkey();
272         final Intent intent = new Intent();
273         final ProviderPendingIntentResponse providerPendingIntentResponse = new
274                 ProviderPendingIntentResponse(Activity.RESULT_CANCELED, intent);
275 
276         mProviderRegistryGetSession.onUiEntrySelected(
277                 ProviderRegistryGetSession.CREDENTIAL_ENTRY_KEY,
278                 entryKey, providerPendingIntentResponse);
279 
280         assertThat(statusCaptor.getValue()).isEqualTo(ProviderSession.Status.CREDENTIALS_RECEIVED);
281         verify(mGetRequestSession).onFinalErrorReceived(cpComponentNameCaptor.capture(),
282                 exceptionTypeCaptor.capture(), exceptionMessageCaptor.capture());
283         assertThat(cpComponentNameCaptor.getValue())
284                 .isSameInstanceAs(CREDENTIAL_PROVIDER_COMPONENT);
285         assertThat(exceptionTypeCaptor.getValue())
286                 .isEqualTo(GetCredentialException.TYPE_USER_CANCELED);
287     }
288 
289     @Test
testOnUiEntrySelected_correctEntryKeyPendingIntentResponseExists_succeeds()290     public void testOnUiEntrySelected_correctEntryKeyPendingIntentResponseExists_succeeds() {
291         final ArgumentCaptor<GetCredentialResponse> getCredentialResponseCaptor =
292                 ArgumentCaptor.forClass(GetCredentialResponse.class);
293         final ArgumentCaptor<ComponentName> cpComponentNameCaptor =
294                 ArgumentCaptor.forClass(ComponentName.class);
295         final ArgumentCaptor<ProviderSession.Status> statusCaptor =
296                 ArgumentCaptor.forClass(ProviderSession.Status.class);
297         mProviderRegistryGetSession.invokeSession();
298         GetCredentialProviderData providerData = (GetCredentialProviderData)
299                 mProviderRegistryGetSession.prepareUiData();
300         final Intent intent = new Intent();
301         final GetCredentialResponse response =
302                 new GetCredentialResponse(new Credential(CREDENTIAL_TYPE, new Bundle()));
303         intent.putExtra(CredentialProviderService.EXTRA_GET_CREDENTIAL_RESPONSE, response);
304         String entryKey = providerData.getCredentialEntries().get(0).getSubkey();
305         final ProviderPendingIntentResponse providerPendingIntentResponse = new
306                 ProviderPendingIntentResponse(Activity.RESULT_OK, intent);
307 
308         mProviderRegistryGetSession.onUiEntrySelected(
309                 ProviderRegistryGetSession.CREDENTIAL_ENTRY_KEY,
310                 entryKey, providerPendingIntentResponse);
311 
312         assertThat(statusCaptor.getValue()).isEqualTo(ProviderSession.Status.CREDENTIALS_RECEIVED);
313         verify(mGetRequestSession).onFinalResponseReceived(cpComponentNameCaptor.capture(),
314                 getCredentialResponseCaptor.capture());
315         assertThat(cpComponentNameCaptor.getValue())
316                 .isSameInstanceAs(CREDENTIAL_PROVIDER_COMPONENT);
317         assertThat(getCredentialResponseCaptor.getValue()).isSameInstanceAs(response);
318     }
319 
createSlice()320     private static Slice createSlice() {
321         return new Slice.Builder(Uri.EMPTY, new SliceSpec("", 0)).build();
322     }
323 
createCallingAppInfo()324     private static CallingAppInfo createCallingAppInfo() throws CertificateException {
325         return new CallingAppInfo(CALLING_PACKAGE_NAME,
326                 new SigningInfo(
327                         new SigningDetails(new Signature[]{}, 0,
328                                 null)));
329     }
330 }
331