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