1 /*
2  * Copyright (C) 2020 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.biometrics;
18 
19 import static android.hardware.biometrics.BiometricManager.Authenticators;
20 
21 import android.content.DialogInterface;
22 import android.content.pm.PackageManager;
23 import android.hardware.biometrics.BiometricManager;
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.provider.Settings;
31 import android.security.keystore.KeyPermanentlyInvalidatedException;
32 import android.util.Log;
33 import android.view.View;
34 import android.widget.Button;
35 
36 import com.android.cts.verifier.R;
37 
38 import java.util.Arrays;
39 
40 import javax.crypto.Cipher;
41 import javax.crypto.IllegalBlockSizeException;
42 
43 /**
44  * On devices without a strong biometric, ensure that the
45  * {@link BiometricManager#canAuthenticate(int)} returns
46  * {@link BiometricManager#BIOMETRIC_ERROR_NO_HARDWARE}
47  *
48  * Ensure that this result is consistent with the configuration in core/res/res/values/config.xml
49  *
50  * Ensure that invoking {@link Settings.ACTION_BIOMETRIC_ENROLL} with its corresponding
51  * {@link Settings.EXTRA_BIOMETRIC_AUTHENTICATORS_ALLOWED} enrolls a
52  * {@link BiometricManager.Authenticators.BIOMETRIC_STRONG} authenticator. This can be done by
53  * authenticating a {@link BiometricPrompt.CryptoObject}.
54  *
55  * Ensure that authentication with a strong biometric unlocks the appropriate keys.
56  *
57  * Ensure that the BiometricPrompt UI displays all fields in the public API surface.
58  */
59 public class BiometricStrongTests extends AbstractBaseTest {
60     private static final String TAG = "BiometricStrongTests";
61 
62     private static final String KEY_NAME_STRONGBOX = "key_using_strongbox";
63     private static final String KEY_NAME_NO_STRONGBOX = "key_without_strongbox";
64     private static final byte[] PAYLOAD = new byte[] {1, 2, 3, 4, 5, 6};
65 
66     // TODO: Build these lists in a smarter way. For now, when adding a test to this list, please
67     // double check the logic in isOnPauseAllowed()
68     private boolean mHasStrongBox;
69     private Button mCheckAndEnrollButton;
70     private Button mAuthenticateWithoutStrongBoxButton;
71     private Button mAuthenticateWithStrongBoxButton;
72     private Button mKeyInvalidatedButton;
73 
74     private boolean mAuthenticateWithoutStrongBoxPassed;
75     private boolean mAuthenticateWithStrongBoxPassed;
76     private boolean mKeyInvalidatedStrongboxPassed;
77     private boolean mKeyInvalidatedNoStrongboxPassed;
78 
79     @Override
getTag()80     protected String getTag() {
81         return TAG;
82     }
83 
84     @Override
onBiometricEnrollFinished()85     protected void onBiometricEnrollFinished() {
86         final int biometricStatus =
87                 mBiometricManager.canAuthenticate(Authenticators.BIOMETRIC_STRONG);
88         if (biometricStatus == BiometricManager.BIOMETRIC_SUCCESS) {
89             showToastAndLog("Successfully enrolled, please continue the test");
90             mCheckAndEnrollButton.setEnabled(false);
91             mAuthenticateWithoutStrongBoxButton.setEnabled(true);
92             mAuthenticateWithStrongBoxButton.setEnabled(true);
93         } else {
94             showToastAndLog("Unexpected result after enrollment: " + biometricStatus);
95         }
96     }
97 
98     @Override
onCreate(Bundle savedInstanceState)99     protected void onCreate(Bundle savedInstanceState) {
100         super.onCreate(savedInstanceState);
101         setContentView(R.layout.biometric_test_strong_tests);
102         setPassFailButtonClickListeners();
103         getPassButton().setEnabled(false);
104 
105         mCheckAndEnrollButton = findViewById(R.id.check_and_enroll_button);
106         mAuthenticateWithoutStrongBoxButton = findViewById(R.id.authenticate_no_strongbox_button);
107         mAuthenticateWithStrongBoxButton = findViewById(R.id.authenticate_strongbox_button);
108         mKeyInvalidatedButton = findViewById(R.id.authenticate_key_invalidated_button);
109 
110         mHasStrongBox = getPackageManager()
111                 .hasSystemFeature(PackageManager.FEATURE_STRONGBOX_KEYSTORE);
112         if (!mHasStrongBox) {
113             Log.d(TAG, "Device does not support StrongBox");
114             mAuthenticateWithStrongBoxButton.setVisibility(View.GONE);
115             mAuthenticateWithStrongBoxPassed = true;
116             mKeyInvalidatedStrongboxPassed = true;
117         }
118 
119         mCheckAndEnrollButton.setOnClickListener((view) -> {
120             checkAndEnroll(mCheckAndEnrollButton, Authenticators.BIOMETRIC_STRONG);
121         });
122 
123         mAuthenticateWithoutStrongBoxButton.setOnClickListener((view) -> {
124             testBiometricBoundEncryption(KEY_NAME_NO_STRONGBOX, PAYLOAD,
125                     false /* useStrongBox */);
126         });
127 
128         mAuthenticateWithStrongBoxButton.setOnClickListener((view) -> {
129             testBiometricBoundEncryption(KEY_NAME_STRONGBOX, PAYLOAD,
130                     true /* useStrongBox */);
131         });
132 
133         mKeyInvalidatedButton.setOnClickListener((view) -> {
134             Utils.showInstructionDialog(this,
135                     R.string.biometric_test_strong_authenticate_invalidated_instruction_title,
136                     R.string.biometric_test_strong_authenticate_invalidated_instruction_contents,
137                     R.string.biometric_test_strong_authenticate_invalidated_instruction_continue,
138                     (dialog, which) -> {
139                 if (which == DialogInterface.BUTTON_POSITIVE) {
140                     // If the device supports StrongBox, check that this key is invalidated.
141                     if (mHasStrongBox)
142                         if (isKeyInvalidated(KEY_NAME_STRONGBOX)) {
143                             mKeyInvalidatedStrongboxPassed = true;
144                         } else {
145                             showToastAndLog("StrongBox key not invalidated");
146                             return;
147                         }
148                     }
149 
150                 // Always check that non-StrongBox keys are invalidated.
151                 if (isKeyInvalidated(KEY_NAME_NO_STRONGBOX)) {
152                     mKeyInvalidatedNoStrongboxPassed = true;
153                 } else {
154                     showToastAndLog("Key not invalidated");
155                     return;
156                 }
157 
158                 mKeyInvalidatedButton.setEnabled(false);
159                 updatePassButton();
160             });
161         });
162     }
163 
164     @Override
isOnPauseAllowed()165     protected boolean isOnPauseAllowed() {
166         // Test hasn't started yet, user may need to go to Settings to remove enrollments
167         if (mCheckAndEnrollButton.isEnabled()) {
168             return true;
169         }
170 
171         // Key invalidation test is currently the last test. Thus, if every other test is currently
172         // completed, let's allow onPause (allow tester to go into settings multiple times if
173         // needed).
174         if (mAuthenticateWithoutStrongBoxPassed && mAuthenticateWithStrongBoxPassed) {
175             return true;
176         }
177 
178         if (mCurrentlyEnrolling) {
179             return true;
180         }
181 
182         return false;
183     }
184 
isKeyInvalidated(String keyName)185     private boolean isKeyInvalidated(String keyName) {
186         try {
187             Utils.initCipher(keyName);
188         } catch (KeyPermanentlyInvalidatedException e) {
189             return true;
190         } catch (Exception e) {
191             showToastAndLog("Unexpected exception: " + e);
192         }
193         return false;
194     }
195 
testBiometricBoundEncryption(String keyName, byte[] secret, boolean useStrongBox)196     private void testBiometricBoundEncryption(String keyName, byte[] secret, boolean useStrongBox) {
197         try {
198             // Create the biometric-bound key
199             Utils.createBiometricBoundKey(keyName, useStrongBox);
200 
201             // Initialize a cipher and try to use it before a biometric has been authenticated
202             Cipher tryUseBeforeAuthCipher = Utils.initCipher(keyName);
203 
204             try {
205                 byte[] encrypted = Utils.doEncrypt(tryUseBeforeAuthCipher, secret);
206                 showToastAndLog("Should not be able to encrypt prior to authenticating: "
207                         + Arrays.toString(encrypted));
208                 return;
209             } catch (IllegalBlockSizeException e) {
210                 // Normal, user has not authenticated yet
211                 Log.d(TAG, "Exception before authentication has occurred: " + e);
212             }
213 
214             // Initialize a cipher and try to use it after a biometric has been authenticated
215             final Cipher tryUseAfterAuthCipher = Utils.initCipher(keyName);
216             CryptoObject crypto = new CryptoObject(tryUseAfterAuthCipher);
217 
218             final BiometricPrompt.Builder builder = new BiometricPrompt.Builder(this);
219             builder.setTitle("Please authenticate");
220             builder.setAllowedAuthenticators(Authenticators.BIOMETRIC_STRONG);
221             builder.setNegativeButton("Cancel", mExecutor, (dialog, which) -> {
222                 // Do nothing
223             });
224             final BiometricPrompt prompt = builder.build();
225             prompt.authenticate(crypto, new CancellationSignal(), mExecutor,
226                     new AuthenticationCallback() {
227                         @Override
228                         public void onAuthenticationSucceeded(AuthenticationResult result) {
229                             try {
230                                 final int authenticationType = result.getAuthenticationType();
231                                 if (authenticationType
232                                         != BiometricPrompt.AUTHENTICATION_RESULT_TYPE_BIOMETRIC) {
233                                     showToastAndLog("Unexpected authenticationType: "
234                                             + authenticationType);
235                                     return;
236                                 }
237 
238                                 byte[] encrypted = Utils.doEncrypt(tryUseAfterAuthCipher,
239                                         secret);
240                                 showToastAndLog("Encrypted payload: " + Arrays.toString(encrypted)
241                                         + ", please run the next test");
242                                 if (useStrongBox) {
243                                     mAuthenticateWithStrongBoxPassed = true;
244                                     mAuthenticateWithStrongBoxButton.setEnabled(false);
245                                 } else {
246                                     mAuthenticateWithoutStrongBoxPassed = true;
247                                     mAuthenticateWithoutStrongBoxButton.setEnabled(false);
248                                 }
249                                 updatePassButton();
250                             } catch (Exception e) {
251                                 showToastAndLog("Failed to encrypt after biometric was"
252                                         + "authenticated: " + e, e);
253                             }
254                         }
255                     });
256         } catch (Exception e) {
257             showToastAndLog("Failed during Crypto test: " + e);
258         }
259     }
260 
updatePassButton()261     private void updatePassButton() {
262         if (mAuthenticateWithoutStrongBoxPassed && mAuthenticateWithStrongBoxPassed) {
263 
264             if (!mKeyInvalidatedStrongboxPassed || !mKeyInvalidatedNoStrongboxPassed) {
265                 mKeyInvalidatedButton.setEnabled(true);
266             }
267 
268             if (mKeyInvalidatedStrongboxPassed && mKeyInvalidatedNoStrongboxPassed) {
269                 showToastAndLog("All tests passed");
270                 getPassButton().setEnabled(true);
271             }
272         }
273     }
274 }
275