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