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 android.content.pm.PackageManager;
20 import android.hardware.biometrics.BiometricManager;
21 import android.hardware.biometrics.BiometricManager.Authenticators;
22 import android.hardware.biometrics.BiometricPrompt;
23 import android.hardware.biometrics.BiometricPrompt.AuthenticationCallback;
24 import android.hardware.biometrics.BiometricPrompt.AuthenticationResult;
25 import android.hardware.biometrics.BiometricPrompt.CryptoObject;
26 import android.os.Bundle;
27 import android.os.CancellationSignal;
28 import android.os.Handler;
29 import android.os.Looper;
30 import android.security.keystore.KeyProperties;
31 import android.util.Log;
32 import android.view.View;
33 import android.widget.Button;
34 import android.widget.TextView;
35 import android.widget.Toast;
36 
37 import com.android.cts.verifier.PassFailButtons;
38 import com.android.cts.verifier.R;
39 
40 import java.util.concurrent.Executor;
41 
42 /**
43  * This is the abstract base class for testing/checking that keys generated via
44  * setUserAuthenticationParameters(timeout, CREDENTIAL) can be unlocked (or not) depending on the
45  * type of authenticator used. This tests various combinations of
46  * {timeout, authenticator, strongbox}. Extending classes currently consist of:
47  * {@link UserAuthenticationCredentialCipherTest} for testing {@link javax.crypto.Cipher}.
48  */
49 public abstract class AbstractUserAuthenticationTest extends PassFailButtons.Activity {
50 
51     private static final String TAG = "AbstractUserAuthenticationCredentialTest";
52 
53     private static final int TIMED_KEY_DURATION = 3;
54     private static final byte[] PAYLOAD = new byte[] {1, 2, 3, 4, 5, 6};
55 
56     abstract class ExpectedResults {
shouldCredentialUnlockPerUseKey()57         abstract boolean shouldCredentialUnlockPerUseKey();
shouldCredentialUnlockTimedKey()58         abstract boolean shouldCredentialUnlockTimedKey();
shouldBiometricUnlockPerUseKey()59         abstract boolean shouldBiometricUnlockPerUseKey();
shouldBiometricUnlockTimedKey()60         abstract boolean shouldBiometricUnlockTimedKey();
61     }
62 
63     /**
64      * @return Log tag.
65      */
getTag()66     abstract String getTag();
67 
getInstructionsResourceId()68     abstract int getInstructionsResourceId();
69 
createUserAuthenticationKey(String keyName, int timeout, int authType, boolean useStrongBox)70     abstract void createUserAuthenticationKey(String keyName, int timeout, int authType,
71             boolean useStrongBox) throws Exception;
72 
getExpectedResults()73     abstract ExpectedResults getExpectedResults();
74 
75     /**
76      * @return The authenticators allowed to unlock the cryptographic operation. See
77      * {@link KeyProperties#AUTH_DEVICE_CREDENTIAL} and {@link KeyProperties#AUTH_BIOMETRIC_STRONG}
78      */
getKeyAuthenticators()79     abstract int getKeyAuthenticators();
80 
81     /**
82      * Due to the differences between auth-per-use operations and time-based operations, the
83      * initialization of the keystore operation may be before or after authentication. Initializing
84      * the operation will require the extending class to store it somewhere for later use. This
85      * cached operation should be cleared after {@link #doKeystoreOperation(byte[])} is invoked.
86      */
initializeKeystoreOperation(String keyName)87     abstract void initializeKeystoreOperation(String keyName) throws Exception;
88 
89     /**
90      * This method is used only for auth-per-use keys. This requires the keystore operation to
91      * already be initialized and cached within the extending class.
92      */
getCryptoObject()93     abstract CryptoObject getCryptoObject();
94 
95     /**
96      * Attempts to perform the initialized/cached keystore operation. This method must guarantee
97      * that the cached operation is null after it's run (both passing and failing cases).
98      */
doKeystoreOperation(byte[] payload)99     abstract void doKeystoreOperation(byte[] payload) throws Exception;
100 
101     protected final Handler mHandler = new Handler(Looper.getMainLooper());
102     protected final Executor mExecutor = mHandler::post;
103 
104     private BiometricManager mBiometricManager;
105 
106     private Button mCredentialPerUseButton;
107     private Button mCredentialTimedButton;
108     private Button mBiometricPerUseButton;
109     private Button mBiometricTimedButton;
110     private Button mCredentialPerUseButton_strongbox;
111     private Button mCredentialTimedButton_strongbox;
112     private Button mBiometricPerUseButton_strongbox;
113     private Button mBiometricTimedButton_strongbox;
114 
115     private Button[] mButtons;
116 
117     @Override
onCreate(Bundle savedInstanceState)118     protected void onCreate(Bundle savedInstanceState) {
119         super.onCreate(savedInstanceState);
120         setContentView(R.layout.biometric_test_user_authentication_credential_tests);
121         setPassFailButtonClickListeners();
122         getPassButton().setEnabled(false);
123 
124         mBiometricManager = getSystemService(BiometricManager.class);
125 
126         TextView instructionsText = findViewById(R.id.instructions);
127         instructionsText.setText(getInstructionsResourceId());
128 
129         mCredentialPerUseButton = findViewById(R.id.per_use_auth_with_credential);
130         mCredentialTimedButton = findViewById(R.id.duration_auth_with_credential);
131         mBiometricPerUseButton = findViewById(R.id.per_use_auth_with_biometric);
132         mBiometricTimedButton = findViewById(R.id.duration_auth_with_biometric);
133         mCredentialPerUseButton_strongbox
134                 = findViewById(R.id.per_use_auth_with_credential_strongbox);
135         mCredentialTimedButton_strongbox
136                 = findViewById(R.id.duration_auth_with_credential_strongbox);
137         mBiometricPerUseButton_strongbox
138                 = findViewById(R.id.per_use_auth_with_biometric_strongbox);
139         mBiometricTimedButton_strongbox
140                 = findViewById(R.id.duration_auth_with_biometric_strongbox);
141 
142         mButtons = new Button[] {
143                 mCredentialPerUseButton,
144                 mCredentialTimedButton,
145                 mBiometricPerUseButton,
146                 mBiometricTimedButton,
147                 mCredentialPerUseButton_strongbox,
148                 mCredentialTimedButton_strongbox,
149                 mBiometricPerUseButton_strongbox,
150                 mBiometricTimedButton_strongbox
151         };
152 
153         final boolean hasStrongBox = getPackageManager().hasSystemFeature(
154                 PackageManager.FEATURE_STRONGBOX_KEYSTORE);
155         final boolean noStrongBiometricHardware = !hasStrongBiometrics();
156 
157         if (!hasStrongBox) {
158             mCredentialPerUseButton_strongbox.setVisibility(View.GONE);
159             mCredentialTimedButton_strongbox.setVisibility(View.GONE);
160             mBiometricPerUseButton_strongbox.setVisibility(View.GONE);
161             mBiometricTimedButton_strongbox.setVisibility(View.GONE);
162         }
163 
164         if (noStrongBiometricHardware) {
165             mBiometricPerUseButton.setVisibility(View.GONE);
166             mBiometricTimedButton.setVisibility(View.GONE);
167             mBiometricPerUseButton_strongbox.setVisibility(View.GONE);
168             mBiometricTimedButton_strongbox.setVisibility(View.GONE);
169         }
170 
171         // No strongbox
172 
173         mCredentialPerUseButton.setOnClickListener((view) -> {
174             testCredentialBoundEncryption("key1",
175                     0 /* timeout */,
176                     false /* useStrongBox */,
177                     Authenticators.DEVICE_CREDENTIAL,
178                     getExpectedResults().shouldCredentialUnlockPerUseKey(),
179                     PAYLOAD,
180                     mCredentialPerUseButton);
181         });
182 
183         mCredentialTimedButton.setOnClickListener((view) -> {
184             testCredentialBoundEncryption("key2",
185                     TIMED_KEY_DURATION /* timeout */,
186                     false /* useStrongBox */,
187                     Authenticators.DEVICE_CREDENTIAL,
188                     getExpectedResults().shouldCredentialUnlockTimedKey(),
189                     PAYLOAD,
190                     mCredentialTimedButton);
191         });
192 
193         mBiometricPerUseButton.setOnClickListener((view) -> {
194             testCredentialBoundEncryption("key3",
195                     0 /* timeout */,
196                     false /* useStrongBox */,
197                     Authenticators.BIOMETRIC_STRONG,
198                     getExpectedResults().shouldBiometricUnlockPerUseKey(),
199                     PAYLOAD,
200                     mBiometricPerUseButton);
201         });
202 
203         mBiometricTimedButton.setOnClickListener((view) -> {
204             testCredentialBoundEncryption("key4",
205                     TIMED_KEY_DURATION /* timeout */,
206                     false /* useStrongBox */,
207                     Authenticators.BIOMETRIC_STRONG,
208                     getExpectedResults().shouldBiometricUnlockTimedKey(),
209                     PAYLOAD,
210                     mBiometricTimedButton);
211         });
212 
213         // Strongbox
214 
215         mCredentialPerUseButton_strongbox.setOnClickListener((view) -> {
216             testCredentialBoundEncryption("key5",
217                     0 /* timeout */,
218                     true /* useStrongBox */,
219                     Authenticators.DEVICE_CREDENTIAL,
220                     getExpectedResults().shouldCredentialUnlockPerUseKey(),
221                     PAYLOAD,
222                     mCredentialPerUseButton_strongbox);
223         });
224 
225         mCredentialTimedButton_strongbox.setOnClickListener((view) -> {
226             testCredentialBoundEncryption("key6",
227                     TIMED_KEY_DURATION /* timeout */,
228                     true /* useStrongBox */,
229                     Authenticators.DEVICE_CREDENTIAL,
230                     getExpectedResults().shouldCredentialUnlockTimedKey(),
231                     PAYLOAD,
232                     mCredentialTimedButton_strongbox);
233         });
234 
235         mBiometricPerUseButton_strongbox.setOnClickListener((view) -> {
236             testCredentialBoundEncryption("key7",
237                     0 /* timeout */,
238                     true /* useStrongBox */,
239                     Authenticators.BIOMETRIC_STRONG,
240                     getExpectedResults().shouldBiometricUnlockPerUseKey(),
241                     PAYLOAD,
242                     mBiometricPerUseButton_strongbox);
243         });
244 
245         mBiometricTimedButton_strongbox.setOnClickListener((view) -> {
246             testCredentialBoundEncryption("key8",
247                     TIMED_KEY_DURATION /* timeout */,
248                     true /* useStrongBox */,
249                     Authenticators.BIOMETRIC_STRONG,
250                     getExpectedResults().shouldBiometricUnlockTimedKey(),
251                     PAYLOAD,
252                     mBiometricTimedButton_strongbox);
253         });
254     }
255 
256     @Override
onPause()257     protected void onPause() {
258         super.onPause();
259 
260         if (!getPassButton().isEnabled()) {
261             // This test is affected by PIN/Pattern/Password authentication. So, do not allow
262             // the test to complete if the user leaves the app (lockscreen, etc will affect this
263             // test).
264             showToastAndLog("This test must be completed without pausing the app");
265             finish();
266         }
267     }
268 
testCredentialBoundEncryption(String keyName, int timeout, boolean useStrongBox, int allowedAuthenticators, boolean shouldKeyBeUsable, byte[] payload, Button testButton)269     private void testCredentialBoundEncryption(String keyName, int timeout, boolean useStrongBox,
270             int allowedAuthenticators, boolean shouldKeyBeUsable, byte[] payload,
271             Button testButton) {
272 
273         final boolean requiresCryptoObject = timeout == 0;
274 
275         final int canAuthenticate = mBiometricManager.canAuthenticate(allowedAuthenticators);
276         if (canAuthenticate != BiometricManager.BIOMETRIC_SUCCESS) {
277             showToastAndLog("Please ensure you can authenticate with the following authenticators: "
278                     + allowedAuthenticators + " Result: " + canAuthenticate);
279             return;
280         }
281 
282         try {
283             if (mBiometricManager.canAuthenticate(allowedAuthenticators)
284                     != BiometricManager.BIOMETRIC_SUCCESS) {
285                 showToastAndLog("Please ensure you have the authenticator combination set up: "
286                         + allowedAuthenticators);
287                 return;
288             }
289 
290             try {
291                 createUserAuthenticationKey(keyName, timeout, getKeyAuthenticators(), useStrongBox);
292             } catch (Exception e) {
293                 final boolean shouldKeyBeGeneratable;
294                 if (getKeyAuthenticators() == KeyProperties.AUTH_BIOMETRIC_STRONG &&
295                         !hasStrongBiometrics()) {
296                     shouldKeyBeGeneratable = false;
297                 } else {
298                     shouldKeyBeGeneratable = true;
299                 }
300 
301                 if (!shouldKeyBeGeneratable) {
302                     Log.d(TAG, "Key not generatable (expected). Exception: " + e);
303                     testButton.setVisibility(View.INVISIBLE);
304                     updatePassButton();
305                     return;
306                 } else {
307                     throw e;
308                 }
309             }
310 
311             CryptoObject crypto;
312 
313             // For auth-per-use keys, the keystore operation needs to be initialized before
314             // authenticating, so we can wrap it into a CryptoObject. For time-based keys, the
315             // keystore operation can only be initialized after authentication has occurred.
316             if (requiresCryptoObject) {
317                 initializeKeystoreOperation(keyName);
318                 crypto = getCryptoObject();
319             } else {
320                 crypto = null;
321             }
322 
323             final BiometricPrompt.Builder builder = new BiometricPrompt.Builder(this);
324             builder.setTitle("Please authenticate");
325             builder.setAllowedAuthenticators(allowedAuthenticators);
326 
327             // The BiometricPrompt API requires a negative button if credential is not allowed.
328             if ((allowedAuthenticators & Authenticators.DEVICE_CREDENTIAL) == 0) {
329                 builder.setNegativeButton("Cancel", mExecutor, (dialog, which) -> {
330                     // Do nothing
331                 });
332             }
333 
334             final AuthenticationCallback callback = new AuthenticationCallback() {
335                 @Override
336                 public void onAuthenticationSucceeded(AuthenticationResult result) {
337                     // Key generation / initialization can depend on past authentication. Ensure
338                     // that the user has not authenticated within n+1 seconds before allowing the
339                     // next test to start.
340                     disableTestsForFewSeconds();
341 
342                     Exception exception = null;
343                     boolean keyUsed;
344                     try {
345                         if (!requiresCryptoObject) {
346                             initializeKeystoreOperation(keyName);
347                         }
348 
349                         doKeystoreOperation(payload);
350 
351                         keyUsed = true;
352                     } catch (Exception e) {
353                         keyUsed = false;
354                         exception = e;
355                     }
356 
357                     if (keyUsed != shouldKeyBeUsable) {
358                         showToastAndLog("Test failed. shouldKeyBeUsable: " + shouldKeyBeUsable
359                                 + " keyUsed: " + keyUsed + " Exception: " + exception, exception);
360                         if (exception != null) {
361                             exception.printStackTrace();
362                         }
363                     } else {
364                         // Set them to invisible, because for this test, disabled actually means
365                         // something else. For the initialization of some keys, its success/failure
366                         // can depend on if the user has entered their credential within the last
367                         // "n" seconds. Those tests need to be disabled until "n" has passed.
368                         testButton.setVisibility(View.INVISIBLE);
369                     }
370                     updatePassButton();
371                 }
372             };
373 
374 
375             final BiometricPrompt prompt = builder.build();
376 
377             if (requiresCryptoObject) {
378                 prompt.authenticate(crypto, new CancellationSignal(), mExecutor, callback);
379             } else {
380                 prompt.authenticate(new CancellationSignal(), mExecutor, callback);
381             }
382 
383         } catch (Exception e) {
384             showToastAndLog("Failed during Crypto test: " + e);
385             e.printStackTrace();
386         }
387     }
388 
hasStrongBiometrics()389     private boolean hasStrongBiometrics() {
390         return mBiometricManager.canAuthenticate(Authenticators.BIOMETRIC_STRONG)
391                 != BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE;
392     }
393 
disableTestsForFewSeconds()394     private void disableTestsForFewSeconds() {
395         for (Button b : mButtons) {
396             b.setEnabled(false);
397         }
398 
399         mHandler.postDelayed(() -> {
400             for (Button b : mButtons) {
401                 b.setEnabled(true);
402             }
403         }, TIMED_KEY_DURATION * 1000 + 1000);
404     }
405 
updatePassButton()406     private void updatePassButton() {
407         for (Button b : mButtons) {
408             if (b.getVisibility() == View.VISIBLE) {
409                 return;
410             }
411         }
412 
413         showToastAndLog("All tests passed");
414         getPassButton().setEnabled(true);
415     }
416 
showToastAndLog(String s)417     private void showToastAndLog(String s) {
418         Log.d(getTag(), s);
419         Toast.makeText(this, s, Toast.LENGTH_SHORT).show();
420     }
421 
showToastAndLog(String s, Exception e)422     private void showToastAndLog(String s, Exception e) {
423         Log.d(getTag(), s, e);
424         Toast.makeText(this, s, Toast.LENGTH_SHORT).show();
425     }
426 }
427