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.pm.PackageManager;
22 import android.hardware.biometrics.BiometricManager;
23 import android.hardware.biometrics.BiometricManager.Authenticators;
24 import android.hardware.biometrics.BiometricPrompt;
25 import android.hardware.biometrics.BiometricPrompt.AuthenticationCallback;
26 import android.hardware.biometrics.BiometricPrompt.AuthenticationResult;
27 import android.hardware.biometrics.BiometricPrompt.CryptoObject;
28 import android.os.Bundle;
29 import android.os.CancellationSignal;
30 import android.security.identity.AccessControlProfile;
31 import android.security.identity.AccessControlProfileId;
32 import android.security.identity.IdentityCredential;
33 import android.security.identity.IdentityCredentialStore;
34 import android.security.identity.PersonalizationData;
35 import android.security.identity.ResultData;
36 import android.security.identity.WritableIdentityCredential;
37 import android.widget.Button;
38 import android.widget.Toast;
39 
40 import androidx.annotation.NonNull;
41 import androidx.annotation.Nullable;
42 
43 import com.android.cts.verifier.PassFailButtons;
44 import com.android.cts.verifier.R;
45 
46 import java.io.ByteArrayOutputStream;
47 import java.security.cert.X509Certificate;
48 import java.util.Arrays;
49 import java.util.Collection;
50 import java.util.LinkedHashMap;
51 import java.util.LinkedList;
52 import java.util.Map;
53 
54 import co.nstant.in.cbor.CborBuilder;
55 import co.nstant.in.cbor.CborEncoder;
56 import co.nstant.in.cbor.CborException;
57 import co.nstant.in.cbor.builder.MapBuilder;
58 
59 public class IdentityCredentialAuthentication extends PassFailButtons.Activity {
60     private static final boolean DEBUG = false;
61     private static final String TAG = "IdentityCredentialAuthentication";
62 
63     private static final int BIOMETRIC_REQUEST_PERMISSION_CODE = 0;
64 
65     private BiometricManager mBiometricManager;
66     private KeyguardManager mKeyguardManager;
67 
getTitleRes()68     protected int getTitleRes() {
69         return R.string.sec_identity_credential_authentication_test;
70     }
71 
getDescriptionRes()72     private int getDescriptionRes() {
73         return R.string.sec_identity_credential_authentication_test_info;
74     }
75 
76     @Override
onCreate(Bundle savedInstanceState)77     protected void onCreate(Bundle savedInstanceState) {
78         super.onCreate(savedInstanceState);
79         setContentView(R.layout.sec_screen_lock_keys_main);
80         setPassFailButtonClickListeners();
81         setInfoResources(getTitleRes(), getDescriptionRes(), -1);
82         getPassButton().setEnabled(false);
83         requestPermissions(new String[]{Manifest.permission.USE_BIOMETRIC},
84                 BIOMETRIC_REQUEST_PERMISSION_CODE);
85     }
86 
87     @Override
onRequestPermissionsResult(int requestCode, String[] permissions, int[] state)88     public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] state) {
89         if (requestCode == BIOMETRIC_REQUEST_PERMISSION_CODE
90                 && state[0] == PackageManager.PERMISSION_GRANTED) {
91             mBiometricManager = getSystemService(BiometricManager.class);
92             mKeyguardManager = getSystemService(KeyguardManager.class);
93             Button startTestButton = findViewById(R.id.sec_start_test_button);
94 
95             if (!mKeyguardManager.isKeyguardSecure()) {
96                 // Show a message that the user hasn't set up a lock screen.
97                 showToast( "Secure lock screen hasn't been set up.\n Go to "
98                                 + "'Settings -> Security -> Screen lock' to set up a lock screen");
99                 startTestButton.setEnabled(false);
100                 return;
101             }
102 
103             startTestButton.setOnClickListener(v -> startTest());
104         }
105     }
106 
showToast(String message)107     protected void showToast(String message) {
108         Toast.makeText(this, message, Toast.LENGTH_LONG).show();
109     }
110 
provisionFoo(IdentityCredentialStore store)111     private void provisionFoo(IdentityCredentialStore store) throws Exception {
112         store.deleteCredentialByName("test");
113         WritableIdentityCredential wc = store.createCredential("test",
114                 "org.iso.18013-5.2019.mdl");
115 
116         // 'Bar' encoded as CBOR tstr
117         byte[] barCbor = {0x63, 0x42, 0x61, 0x72};
118 
119         AccessControlProfile acp = new AccessControlProfile.Builder(new AccessControlProfileId(0))
120                 .setUserAuthenticationRequired(true)
121                 .setUserAuthenticationTimeout(0)
122                 .build();
123         LinkedList<AccessControlProfileId> idsProfile0 = new LinkedList<AccessControlProfileId>();
124         idsProfile0.add(new AccessControlProfileId(0));
125         PersonalizationData pd = new PersonalizationData.Builder()
126                                  .addAccessControlProfile(acp)
127                                  .putEntry("org.iso.18013-5.2019", "Foo", idsProfile0, barCbor)
128                                  .build();
129         byte[] proofOfProvisioningSignature = wc.personalize(pd);
130 
131         // Create authentication keys.
132         IdentityCredential credential = store.getCredentialByName("test",
133                 IdentityCredentialStore.CIPHERSUITE_ECDHE_HKDF_ECDSA_WITH_AES_256_GCM_SHA256);
134         credential.setAvailableAuthenticationKeys(1, 10);
135         Collection<X509Certificate> dynAuthKeyCerts = credential.getAuthKeysNeedingCertification();
136         credential.storeStaticAuthenticationData(dynAuthKeyCerts.iterator().next(), new byte[0]);
137     }
138 
getFooAndCheckNotRetrievable(IdentityCredential credential)139     private boolean getFooAndCheckNotRetrievable(IdentityCredential credential) throws Exception {
140         Map<String, Collection<String>> entriesToRequest = new LinkedHashMap<>();
141         entriesToRequest.put("org.iso.18013-5.2019", Arrays.asList("Foo"));
142 
143         ResultData rd = credential.getEntries(
144             createItemsRequest(entriesToRequest, null),
145             entriesToRequest,
146             null,  // sessionTranscript
147             null); // readerSignature
148         if (rd.getStatus("org.iso.18013-5.2019", "Foo")
149                 != ResultData.STATUS_USER_AUTHENTICATION_FAILED) {
150             return false;
151         }
152         return true;
153     }
154 
getFooAndCheckRetrievable(IdentityCredential credential)155     private boolean getFooAndCheckRetrievable(IdentityCredential credential) throws Exception {
156         Map<String, Collection<String>> entriesToRequest = new LinkedHashMap<>();
157         entriesToRequest.put("org.iso.18013-5.2019", Arrays.asList("Foo"));
158 
159         ResultData rd = credential.getEntries(
160             createItemsRequest(entriesToRequest, null),
161             entriesToRequest,
162             null,  // sessionTranscript
163             null); // readerSignature
164         if (rd.getStatus("org.iso.18013-5.2019", "Foo") != ResultData.STATUS_OK) {
165             return false;
166         }
167         return true;
168     }
169 
startTest()170     protected void startTest() {
171         IdentityCredentialStore store = IdentityCredentialStore.getInstance(this);
172         if (store == null) {
173             showToast("No Identity Credential support, test passed.");
174             getPassButton().setEnabled(true);
175             return;
176         }
177 
178         final int result = mBiometricManager.canAuthenticate(Authenticators.BIOMETRIC_STRONG);
179         switch (result) {
180             case BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED:
181                 showToast("No strong biometrics (Class 3) enrolled.\n"
182                         + "Go to 'Settings -> Security' to enroll");
183                 Button startTestButton = findViewById(R.id.sec_start_test_button);
184                 startTestButton.setEnabled(false);
185                 return;
186             case BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE:
187                 showToast("No strong biometrics (Class 3), test passed.");
188                 showToast("No Identity Credential support, test passed.");
189                 getPassButton().setEnabled(true);
190                 return;
191         }
192 
193         try {
194             provisionFoo(store);
195 
196             // First, check that Foo cannot be retrieved without authentication.
197             //
198             IdentityCredential credentialWithoutAuth = store.getCredentialByName("test",
199                     IdentityCredentialStore.CIPHERSUITE_ECDHE_HKDF_ECDSA_WITH_AES_256_GCM_SHA256);
200             if (!getFooAndCheckNotRetrievable(credentialWithoutAuth)) {
201                 showToast("Failed while checking that data element cannot be retrieved without"
202                         + " authentication");
203                 return;
204             }
205 
206             // Try one more time, this time with a CryptoObject that we'll use with
207             // BiometricPrompt. This should work.
208             //
209             final IdentityCredential credentialWithAuth = store.getCredentialByName("test",
210                     IdentityCredentialStore.CIPHERSUITE_ECDHE_HKDF_ECDSA_WITH_AES_256_GCM_SHA256);
211             CryptoObject cryptoObject = new BiometricPrompt.CryptoObject(credentialWithAuth);
212             BiometricPrompt.Builder builder = new BiometricPrompt.Builder(this);
213             builder.setAllowedAuthenticators(Authenticators.BIOMETRIC_STRONG);
214             builder.setTitle("Identity Credential");
215             builder.setDescription("Authenticate to unlock credential.");
216             builder.setNegativeButton("Cancel",
217                     getMainExecutor(),
218                     (dialogInterface, i) -> showToast("Canceled biometric prompt."));
219             final BiometricPrompt prompt = builder.build();
220             final AuthenticationCallback callback = new AuthenticationCallback() {
221                 @Override
222                 public void onAuthenticationSucceeded(AuthenticationResult authResult) {
223                     try {
224                         // Check that Foo can be retrieved because we used
225                         // the CryptoObject to auth with.
226                         if (!getFooAndCheckRetrievable(credentialWithAuth)) {
227                             showToast("Failed while checking that data element can be"
228                                     + " retrieved with authentication");
229                             return;
230                         }
231 
232                         // Finally, check that Foo cannot be retrieved again.
233                         IdentityCredential credentialWithoutAuth2 = store.getCredentialByName(
234                                 "test",
235                                 IdentityCredentialStore
236                                         .CIPHERSUITE_ECDHE_HKDF_ECDSA_WITH_AES_256_GCM_SHA256);
237                         if (!getFooAndCheckNotRetrievable(credentialWithoutAuth2)) {
238                             showToast("Failed while checking that data element cannot be"
239                                     + " retrieved without authentication");
240                             return;
241                         }
242 
243                         showToast("Test passed.");
244                         getPassButton().setEnabled(true);
245 
246                     } catch (Exception e) {
247                         showToast("Unexpection exception " + e);
248                     }
249                 }
250             };
251 
252             prompt.authenticate(cryptoObject, new CancellationSignal(), getMainExecutor(),
253                     callback);
254         } catch (Exception e) {
255             showToast("Unexpection exception " + e);
256         }
257     }
258 
259 
260     /*
261      * Helper function to create a CBOR data for requesting data items. The IntentToRetain
262      * value will be set to false for all elements.
263      *
264      * <p>The returned CBOR data conforms to the following CDDL schema:</p>
265      *
266      * <pre>
267      *   ItemsRequest = {
268      *     ? "docType" : DocType,
269      *     "nameSpaces" : NameSpaces,
270      *     ? "RequestInfo" : {* tstr => any} ; Additional info the reader wants to provide
271      *   }
272      *
273      *   NameSpaces = {
274      *     + NameSpace => DataElements     ; Requested data elements for each NameSpace
275      *   }
276      *
277      *   DataElements = {
278      *     + DataElement => IntentToRetain
279      *   }
280      *
281      *   DocType = tstr
282      *
283      *   DataElement = tstr
284      *   IntentToRetain = bool
285      *   NameSpace = tstr
286      * </pre>
287      *
288      * @param entriesToRequest       The entries to request, organized as a map of namespace
289      *                               names with each value being a collection of data elements
290      *                               in the given namespace.
291      * @param docType                  The document type or {@code null} if there is no document
292      *                                 type.
293      * @return CBOR data conforming to the CDDL mentioned above.
294      */
createItemsRequest( @onNull Map<String, Collection<String>> entriesToRequest, @Nullable String docType)295     private static @NonNull byte[] createItemsRequest(
296             @NonNull Map<String, Collection<String>> entriesToRequest,
297             @Nullable String docType) {
298         CborBuilder builder = new CborBuilder();
299         MapBuilder<CborBuilder> mapBuilder = builder.addMap();
300         if (docType != null) {
301             mapBuilder.put("docType", docType);
302         }
303 
304         MapBuilder<MapBuilder<CborBuilder>> nsMapBuilder = mapBuilder.putMap("nameSpaces");
305         for (String namespaceName : entriesToRequest.keySet()) {
306             Collection<String> entryNames = entriesToRequest.get(namespaceName);
307             MapBuilder<MapBuilder<MapBuilder<CborBuilder>>> entryNameMapBuilder =
308                     nsMapBuilder.putMap(namespaceName);
309             for (String entryName : entryNames) {
310                 entryNameMapBuilder.put(entryName, false);
311             }
312         }
313 
314         ByteArrayOutputStream baos = new ByteArrayOutputStream();
315         CborEncoder encoder = new CborEncoder(baos);
316         try {
317             encoder.encode(builder.build());
318         } catch (CborException e) {
319             throw new RuntimeException("Error encoding CBOR", e);
320         }
321         return baos.toByteArray();
322     }
323 
324 
325 
326 }
327