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