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