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