1 /*
2  * Copyright (C) 2019 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.cts.verifier.security;
18 
19 import android.Manifest;
20 import android.app.KeyguardManager;
21 import android.content.Context;
22 import android.content.pm.FeatureInfo;
23 import android.content.pm.PackageManager;
24 import android.hardware.biometrics.BiometricManager;
25 import android.hardware.biometrics.BiometricManager.Authenticators;
26 import android.hardware.biometrics.BiometricPrompt;
27 import android.hardware.biometrics.BiometricPrompt.AuthenticationCallback;
28 import android.hardware.biometrics.BiometricPrompt.AuthenticationResult;
29 import android.hardware.biometrics.BiometricPrompt.CryptoObject;
30 import android.os.Bundle;
31 import android.os.CancellationSignal;
32 import android.security.identity.AccessControlProfile;
33 import android.security.identity.AccessControlProfileId;
34 import android.security.identity.CredentialDataRequest;
35 import android.security.identity.CredentialDataResult;
36 import android.security.identity.IdentityCredential;
37 import android.security.identity.IdentityCredentialStore;
38 import android.security.identity.PersonalizationData;
39 import android.security.identity.PresentationSession;
40 import android.security.identity.WritableIdentityCredential;
41 import android.util.Log;
42 import android.widget.Button;
43 import android.widget.Toast;
44 
45 import com.android.cts.verifier.PassFailButtons;
46 import com.android.cts.verifier.R;
47 
48 import java.security.cert.X509Certificate;
49 import java.util.Arrays;
50 import java.util.Collection;
51 import java.util.LinkedHashMap;
52 import java.util.LinkedList;
53 import java.util.Locale;
54 import java.util.Map;
55 
56 /**
57  * @hide
58  */
59 public class IdentityCredentialAuthenticationMultiDocument extends PassFailButtons.Activity {
60     private static final boolean DEBUG = false;
61     private static final String TAG = "IdentityCredentialAuthenticationMultiDocument";
62 
63     private static final int BIOMETRIC_REQUEST_PERMISSION_CODE = 0;
64 
65     private static final int PRESENTATION_SESSION_FEATURE_VERSION_NEEDED = 202201;
66 
67     private BiometricManager mBiometricManager;
68     private KeyguardManager mKeyguardManager;
69 
getTitleRes()70     protected int getTitleRes() {
71         return R.string.sec_identity_credential_authentication_multi_document_test;
72     }
73 
getDescriptionRes()74     private int getDescriptionRes() {
75         return R.string.sec_identity_credential_authentication_multi_document_test_info;
76     }
77 
78     // Returns 0 if Identity Credential is not implemented. Otherwise returns the feature version.
79     //
getFeatureVersion(Context appContext)80     private static int getFeatureVersion(Context appContext) {
81         PackageManager pm = appContext.getPackageManager();
82 
83         if (pm.hasSystemFeature(PackageManager.FEATURE_IDENTITY_CREDENTIAL_HARDWARE)) {
84             FeatureInfo[] infos = pm.getSystemAvailableFeatures();
85             for (int n = 0; n < infos.length; n++) {
86                 FeatureInfo info = infos[n];
87                 if (info.name.equals(PackageManager.FEATURE_IDENTITY_CREDENTIAL_HARDWARE)) {
88                     return info.version;
89                 }
90             }
91         }
92 
93         // Use of the system feature is not required since Android 12. So for Android 11
94         // return 202009 which is the feature version shipped with Android 11.
95         IdentityCredentialStore store = IdentityCredentialStore.getInstance(appContext);
96         if (store != null) {
97             return 202009;
98         }
99 
100         return 0;
101     }
102 
103     @Override
onCreate(Bundle savedInstanceState)104     protected void onCreate(Bundle savedInstanceState) {
105         super.onCreate(savedInstanceState);
106         setContentView(R.layout.sec_screen_lock_keys_main);
107         setPassFailButtonClickListeners();
108         setInfoResources(getTitleRes(), getDescriptionRes(), -1);
109         getPassButton().setEnabled(false);
110         requestPermissions(new String[]{Manifest.permission.USE_BIOMETRIC},
111                 BIOMETRIC_REQUEST_PERMISSION_CODE);
112     }
113 
114     @Override
onRequestPermissionsResult(int requestCode, String[] permissions, int[] state)115     public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] state) {
116         if (requestCode == BIOMETRIC_REQUEST_PERMISSION_CODE
117                 && state[0] == PackageManager.PERMISSION_GRANTED) {
118             mBiometricManager = getSystemService(BiometricManager.class);
119             mKeyguardManager = getSystemService(KeyguardManager.class);
120             Button startTestButton = findViewById(R.id.sec_start_test_button);
121 
122             if (!mKeyguardManager.isKeyguardSecure()) {
123                 // Show a message that the user hasn't set up a lock screen.
124                 showToast("Secure lock screen hasn't been set up.\n Go to "
125                           + "'Settings -> Security -> Screen lock' to set up a lock screen");
126                 startTestButton.setEnabled(false);
127                 return;
128             }
129 
130             startTestButton.setOnClickListener(v -> startTest());
131         }
132     }
133 
showToast(String message)134     protected void showToast(String message) {
135         Toast.makeText(this, message, Toast.LENGTH_LONG).show();
136         Log.i(TAG, "Showing Toast: " + message);
137     }
138 
provisionCredential(IdentityCredentialStore store, String credentialName)139     private void provisionCredential(IdentityCredentialStore store,
140                                      String credentialName) throws Exception {
141         store.deleteCredentialByName(credentialName);
142         WritableIdentityCredential wc = store.createCredential(
143                 credentialName, "org.iso.18013-5.2019.mdl");
144 
145         // 'Bar' encoded as CBOR tstr
146         byte[] barCbor = {0x63, 0x42, 0x61, 0x72};
147 
148         AccessControlProfile acp = new AccessControlProfile.Builder(new AccessControlProfileId(0))
149                 .setUserAuthenticationRequired(true)
150                 .setUserAuthenticationTimeout(0)
151                 .build();
152         LinkedList<AccessControlProfileId> idsProfile0 = new LinkedList<AccessControlProfileId>();
153         idsProfile0.add(new AccessControlProfileId(0));
154         PersonalizationData pd = new PersonalizationData.Builder()
155                                  .addAccessControlProfile(acp)
156                                  .putEntry("org.iso.18013-5.2019", "Foo", idsProfile0, barCbor)
157                                  .build();
158         byte[] proofOfProvisioningSignature = wc.personalize(pd);
159 
160         // Create authentication keys.
161         IdentityCredential credential = store.getCredentialByName(credentialName,
162                 IdentityCredentialStore.CIPHERSUITE_ECDHE_HKDF_ECDSA_WITH_AES_256_GCM_SHA256);
163         credential.setAvailableAuthenticationKeys(1, 10);
164         Collection<X509Certificate> dynAuthKeyCerts = credential.getAuthKeysNeedingCertification();
165         credential.storeStaticAuthenticationData(dynAuthKeyCerts.iterator().next(), new byte[0]);
166     }
167 
getFooStatus(PresentationSession session, String credentialName)168     private int getFooStatus(PresentationSession session, String credentialName) throws Exception {
169         Map<String, Collection<String>> isEntriesToRequest = new LinkedHashMap<>();
170         isEntriesToRequest.put("org.iso.18013-5.2019", Arrays.asList("Foo"));
171 
172         CredentialDataResult rd = session.getCredentialData(
173                 credentialName,
174                 new CredentialDataRequest.Builder()
175                 .setIssuerSignedEntriesToRequest(isEntriesToRequest)
176                 .build());
177         return rd.getIssuerSignedEntries().getStatus("org.iso.18013-5.2019", "Foo");
178     }
179 
startTest()180     protected void startTest() {
181         IdentityCredentialStore store = IdentityCredentialStore.getInstance(this);
182         if (store == null) {
183             showToast("No Identity Credential support, test passed.");
184             getPassButton().setEnabled(true);
185             return;
186         }
187         int featureVersion = getFeatureVersion(this);
188         Log.i(TAG, "Identity Credential featureVersion: " + featureVersion);
189         if (featureVersion < PRESENTATION_SESSION_FEATURE_VERSION_NEEDED) {
190             showToast(String.format(
191                           Locale.US,
192                           "Identity Credential version %d or later is required but "
193                           + "version %d was found. Test passed.",
194                           PRESENTATION_SESSION_FEATURE_VERSION_NEEDED,
195                           featureVersion));
196             getPassButton().setEnabled(true);
197             return;
198         }
199 
200         final int result = mBiometricManager.canAuthenticate(Authenticators.BIOMETRIC_STRONG);
201         switch (result) {
202             case BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED:
203                 showToast("No strong biometrics (Class 3) enrolled.\n"
204                         + "Go to 'Settings -> Security' to enroll");
205                 Button startTestButton = findViewById(R.id.sec_start_test_button);
206                 startTestButton.setEnabled(false);
207                 return;
208             case BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE:
209                 showToast("No strong biometrics (Class 3), test passed.");
210                 showToast("No Identity Credential support, test passed.");
211                 getPassButton().setEnabled(true);
212                 return;
213         }
214 
215         try {
216             provisionCredential(store, "credential1");
217             provisionCredential(store, "credential2");
218 
219             PresentationSession session = store.createPresentationSession(
220                     IdentityCredentialStore.CIPHERSUITE_ECDHE_HKDF_ECDSA_WITH_AES_256_GCM_SHA256);
221 
222             // First, check that Foo cannot be retrieved without authentication.
223             //
224             int status = getFooStatus(session, "credential1");
225             if (status != CredentialDataResult.Entries.STATUS_USER_AUTHENTICATION_FAILED) {
226                 showToast("Unexpected credential1 pre-auth status "
227                           + status + " expected STATUS_USER_AUTHENTICATION_FAILED");
228                 return;
229             }
230             status = getFooStatus(session, "credential2");
231             if (status != CredentialDataResult.Entries.STATUS_USER_AUTHENTICATION_FAILED) {
232                 showToast("Unexpected credential2 pre-auth status "
233                           + status + " expected STATUS_USER_AUTHENTICATION_FAILED");
234                 return;
235             }
236 
237             // Try one more time, this time with a CryptoObject that we'll use with
238             // BiometricPrompt. This should work.
239             //
240             CryptoObject cryptoObject = new BiometricPrompt.CryptoObject(session);
241             BiometricPrompt.Builder builder = new BiometricPrompt.Builder(this);
242             builder.setAllowedAuthenticators(Authenticators.BIOMETRIC_STRONG);
243             builder.setTitle("Identity Credential");
244             builder.setDescription("Authenticate to unlock multiple credentials.");
245             builder.setNegativeButton("Cancel",
246                     getMainExecutor(),
247                     (dialogInterface, i) -> showToast("Canceled biometric prompt."));
248             final BiometricPrompt prompt = builder.build();
249             final AuthenticationCallback callback = new AuthenticationCallback() {
250                 @Override
251                 public void onAuthenticationSucceeded(AuthenticationResult authResult) {
252                     try {
253                         // Check that Foo can be retrieved because we used
254                         // the CryptoObject to auth with.
255                         int status = getFooStatus(session, "credential1");
256                         if (status != CredentialDataResult.Entries.STATUS_OK) {
257                             showToast("Unexpected credential1 post-auth status "
258                                       + status + " expected STATUS_OK");
259                             return;
260                         }
261                         status = getFooStatus(session, "credential2");
262                         if (status != CredentialDataResult.Entries.STATUS_OK) {
263                             showToast("Unexpected credential2 post-auth status "
264                                       + status + " expected STATUS_OK");
265                             return;
266                         }
267 
268                         // Finally, check that Foo cannot be retrieved again from another session
269                         PresentationSession anotherSession = store.createPresentationSession(
270                                 IdentityCredentialStore
271                                     .CIPHERSUITE_ECDHE_HKDF_ECDSA_WITH_AES_256_GCM_SHA256);
272                         status = getFooStatus(anotherSession, "credential1");
273                         if (status
274                                 != CredentialDataResult.Entries.STATUS_USER_AUTHENTICATION_FAILED) {
275                             showToast("Unexpected credential1 other session status "
276                                       + status + " expected STATUS_USER_AUTHENTICATION_FAILED");
277                             return;
278                         }
279                         status = getFooStatus(anotherSession, "credential2");
280                         if (status
281                                 != CredentialDataResult.Entries.STATUS_USER_AUTHENTICATION_FAILED) {
282                             showToast("Unexpected credential2 other session status "
283                                       + status + " expected STATUS_USER_AUTHENTICATION_FAILED");
284                             return;
285                         }
286 
287                         showToast("Test passed.");
288                         getPassButton().setEnabled(true);
289 
290                     } catch (Exception e) {
291                         showToast("Unexpected exception " + e);
292                     }
293                 }
294             };
295 
296             prompt.authenticate(cryptoObject, new CancellationSignal(), getMainExecutor(),
297                     callback);
298         } catch (Exception e) {
299             showToast("Unexpection exception " + e);
300         }
301     }
302 }
303