1 /* 2 * Copyright (C) 2015 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.security; 18 19 import android.Manifest; 20 import android.app.AlertDialog; 21 import android.app.Dialog; 22 import android.app.DialogFragment; 23 import android.app.KeyguardManager; 24 import android.content.Context; 25 import android.content.DialogInterface; 26 import android.content.pm.PackageManager; 27 import android.hardware.biometrics.BiometricManager; 28 import android.hardware.biometrics.BiometricManager.Authenticators; 29 import android.hardware.fingerprint.FingerprintManager; 30 import android.os.Bundle; 31 import android.os.CancellationSignal; 32 import android.security.keystore.KeyGenParameterSpec; 33 import android.security.keystore.KeyPermanentlyInvalidatedException; 34 import android.security.keystore.KeyProperties; 35 import android.security.keystore.UserNotAuthenticatedException; 36 import android.util.Log; 37 import android.view.View; 38 import android.view.View.OnClickListener; 39 import android.widget.Button; 40 import android.widget.Toast; 41 42 import com.android.cts.verifier.PassFailButtons; 43 import com.android.cts.verifier.R; 44 45 import java.io.IOException; 46 import java.security.InvalidAlgorithmParameterException; 47 import java.security.InvalidKeyException; 48 import java.security.KeyStore; 49 import java.security.KeyStoreException; 50 import java.security.NoSuchAlgorithmException; 51 import java.security.NoSuchProviderException; 52 import java.security.UnrecoverableKeyException; 53 import java.security.cert.CertificateException; 54 55 import javax.crypto.BadPaddingException; 56 import javax.crypto.Cipher; 57 import javax.crypto.IllegalBlockSizeException; 58 import javax.crypto.KeyGenerator; 59 import javax.crypto.NoSuchPaddingException; 60 import javax.crypto.SecretKey; 61 62 public class FingerprintBoundKeysTest extends PassFailButtons.Activity { 63 private static final boolean DEBUG = false; 64 private static final String TAG = "FingerprintBoundKeysTest"; 65 66 /** Alias for our key in the Android Key Store. */ 67 private static final String KEY_NAME = "my_key"; 68 private static final byte[] SECRET_BYTE_ARRAY = new byte[] {1, 2, 3, 4, 5, 6}; 69 private static final int AUTHENTICATION_DURATION_SECONDS = 2; 70 private static final int CONFIRM_CREDENTIALS_REQUEST_CODE = 1; 71 private static final int BIOMETRIC_REQUEST_PERMISSION_CODE = 0; 72 73 protected boolean useStrongBox; 74 75 private BiometricManager mBiometricManager; 76 77 private FingerprintManager mFingerprintManager; 78 private KeyguardManager mKeyguardManager; 79 private FingerprintAuthDialogFragment mFingerprintDialog; 80 private Cipher mCipher; 81 getTitleRes()82 protected int getTitleRes() { 83 return R.string.sec_fingerprint_bound_key_test; 84 } 85 getDescriptionRes()86 protected int getDescriptionRes() { 87 return R.string.sec_fingerprint_bound_key_test_info; 88 } 89 90 @Override onCreate(Bundle savedInstanceState)91 protected void onCreate(Bundle savedInstanceState) { 92 super.onCreate(savedInstanceState); 93 setContentView(R.layout.sec_screen_lock_keys_main); 94 setPassFailButtonClickListeners(); 95 setInfoResources(getTitleRes(), getDescriptionRes(), -1); 96 getPassButton().setEnabled(false); 97 requestPermissions(new String[]{Manifest.permission.USE_BIOMETRIC}, 98 BIOMETRIC_REQUEST_PERMISSION_CODE); 99 100 mBiometricManager = getSystemService(BiometricManager.class); 101 102 checkBiometricStrength(); 103 } 104 105 @Override onRequestPermissionsResult(int requestCode, String[] permissions, int[] state)106 public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] state) { 107 if (requestCode == BIOMETRIC_REQUEST_PERMISSION_CODE && state[0] == PackageManager.PERMISSION_GRANTED) { 108 useStrongBox = false; 109 mFingerprintManager = (FingerprintManager) getSystemService(Context.FINGERPRINT_SERVICE); 110 mKeyguardManager = getSystemService(KeyguardManager.class); 111 Button startTestButton = findViewById(R.id.sec_start_test_button); 112 113 if (!mKeyguardManager.isKeyguardSecure()) { 114 // Show a message that the user hasn't set up a lock screen. 115 showToast( "Secure lock screen hasn't been set up.\n" 116 + "Go to 'Settings -> Security -> Screen lock' to set up a lock screen"); 117 startTestButton.setEnabled(false); 118 return; 119 } 120 121 onPermissionsGranted(); 122 123 startTestButton.setOnClickListener(new OnClickListener() { 124 @Override 125 public void onClick(View v) { 126 startTest(); 127 } 128 }); 129 } 130 } 131 132 /** 133 * Fingerprint-specific check before allowing test to be started 134 */ onPermissionsGranted()135 protected void onPermissionsGranted() { 136 mFingerprintManager = getSystemService(FingerprintManager.class); 137 if (!mFingerprintManager.hasEnrolledFingerprints()) { 138 showToast("No fingerprints enrolled.\n" 139 + "Go to 'Settings -> Security -> Fingerprint' to set up a fingerprint"); 140 Button startTestButton = findViewById(R.id.sec_start_test_button); 141 startTestButton.setEnabled(false); 142 } 143 } 144 checkBiometricStrength()145 private void checkBiometricStrength() 146 { 147 if (!hasStrongBiometrics()) 148 { 149 // Disable the start button 150 Button startTestButton = findViewById(R.id.sec_start_test_button); 151 startTestButton.setEnabled(false); 152 153 // Show a message that STRONG Biometrics is not supported and user can 154 // pass the test. 155 showToast("Device does not support STRONG Biometric level of authentication."); 156 157 // Allow to pass the test 158 getPassButton().setEnabled(true); 159 } 160 } 161 hasStrongBiometrics()162 private boolean hasStrongBiometrics() { 163 return mBiometricManager.canAuthenticate(Authenticators.BIOMETRIC_STRONG) 164 != BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE; 165 } 166 startTest()167 protected void startTest() { 168 createKey(false /* hasValidityDuration */); 169 prepareEncrypt(); 170 if (tryEncrypt()) { 171 showToast("Test failed. Key accessible without auth."); 172 } else { 173 prepareEncrypt(); 174 showAuthenticationScreen(); 175 } 176 } 177 178 /** 179 * Creates a symmetric key in the Android Key Store which requires auth 180 */ createKey(boolean hasValidityDuration)181 private void createKey(boolean hasValidityDuration) { 182 // Generate a key to decrypt payment credentials, tokens, etc. 183 // This will most likely be a registration step for the user when they are setting up your app. 184 try { 185 KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore"); 186 keyStore.load(null); 187 KeyGenerator keyGenerator = KeyGenerator.getInstance( 188 KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore"); 189 190 // Set the alias of the entry in Android KeyStore where the key will appear 191 // and the constrains (purposes) in the constructor of the Builder 192 keyGenerator.init(new KeyGenParameterSpec.Builder(KEY_NAME, 193 KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) 194 .setBlockModes(KeyProperties.BLOCK_MODE_CBC) 195 .setUserAuthenticationRequired(true) 196 .setUserAuthenticationValidityDurationSeconds( 197 hasValidityDuration ? AUTHENTICATION_DURATION_SECONDS : -1) 198 .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7) 199 .setIsStrongBoxBacked(useStrongBox) 200 .build()); 201 keyGenerator.generateKey(); 202 if (DEBUG) { 203 Log.i(TAG, "createKey: [1]: done"); 204 } 205 } catch (NoSuchAlgorithmException | NoSuchProviderException 206 | InvalidAlgorithmParameterException | KeyStoreException 207 | CertificateException | IOException e) { 208 if (DEBUG) { 209 Log.i(TAG, "createKey: [2]: failed"); 210 } 211 throw new RuntimeException("Failed to create a symmetric key", e); 212 } 213 } 214 215 /** 216 * create and init cipher; has to be done before we do auth 217 */ prepareEncrypt()218 private boolean prepareEncrypt() { 219 return encryptInternal(false); 220 } 221 222 /** 223 * Tries to encrypt some data with the generated key in {@link #createKey} which is 224 * only works if the user has just authenticated via device credentials. 225 * has to be run after successful auth, in order to succeed 226 */ tryEncrypt()227 protected boolean tryEncrypt() { 228 return encryptInternal(true); 229 } 230 getCipher()231 protected Cipher getCipher() { 232 return mCipher; 233 } 234 doValidityDurationTest(boolean useStrongBox)235 protected boolean doValidityDurationTest(boolean useStrongBox) { 236 mCipher = null; 237 createKey(true /* hasValidityDuration */); 238 if (prepareEncrypt()) { 239 return tryEncrypt(); 240 } 241 return false; 242 } 243 encryptInternal(boolean doEncrypt)244 private boolean encryptInternal(boolean doEncrypt) { 245 try { 246 if (!doEncrypt) { 247 KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore"); 248 keyStore.load(null); 249 SecretKey secretKey = (SecretKey) keyStore.getKey(KEY_NAME, null); 250 if (DEBUG) { 251 Log.i(TAG, "encryptInternal: [1]: key retrieved"); 252 } 253 if (mCipher == null) { 254 mCipher = Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/" 255 + KeyProperties.BLOCK_MODE_CBC + "/" 256 + KeyProperties.ENCRYPTION_PADDING_PKCS7); 257 } 258 mCipher.init(Cipher.ENCRYPT_MODE, secretKey); 259 if (DEBUG) { 260 Log.i(TAG, "encryptInternal: [2]: cipher initialized"); 261 } 262 } else { 263 mCipher.doFinal(SECRET_BYTE_ARRAY); 264 if (DEBUG) { 265 Log.i(TAG, "encryptInternal: [3]: encryption performed"); 266 } 267 } 268 return true; 269 } catch (BadPaddingException | IllegalBlockSizeException e) { 270 // this happens in "no-error" scenarios routinely; 271 // All we want it to see the event in the log; 272 // Extra exception info is not valuable 273 if (DEBUG) { 274 Log.w(TAG, "encryptInternal: [4]: Encryption failed", e); 275 } 276 return false; 277 } catch (KeyPermanentlyInvalidatedException e) { 278 // Extra exception info is not of big value, but let's have it, 279 // since this is an unlikely sutuation and potential error condition 280 Log.w(TAG, "encryptInternal: [5]: Key invalidated", e); 281 createKey(false /* hasValidityDuration */); 282 showToast("The key has been invalidated, please try again.\n"); 283 return false; 284 } catch (UserNotAuthenticatedException e) { 285 Log.w(TAG, "encryptInternal: [6]: User not authenticated", e); 286 return false; 287 } catch (NoSuchPaddingException | KeyStoreException | CertificateException 288 | UnrecoverableKeyException | IOException 289 | NoSuchAlgorithmException | InvalidKeyException e) { 290 throw new RuntimeException("Failed to init Cipher", e); 291 } 292 } 293 showAuthenticationScreen()294 protected void showAuthenticationScreen() { 295 mFingerprintDialog = new FingerprintAuthDialogFragment(); 296 mFingerprintDialog.setActivity(this); 297 mFingerprintDialog.show(getFragmentManager(), "fingerprint_dialog"); 298 } 299 showToast(String message)300 protected void showToast(String message) { 301 Toast.makeText(this, message, Toast.LENGTH_LONG).show(); 302 } 303 304 public static class FingerprintAuthDialogFragment extends DialogFragment { 305 306 private FingerprintBoundKeysTest mActivity; 307 private CancellationSignal mCancellationSignal; 308 private FingerprintManager mFingerprintManager; 309 private FingerprintManagerCallback mFingerprintManagerCallback; 310 private boolean mSelfCancelled; 311 private boolean hasStrongBox; 312 313 class FingerprintManagerCallback extends FingerprintManager.AuthenticationCallback { 314 @Override onAuthenticationError(int errMsgId, CharSequence errString)315 public void onAuthenticationError(int errMsgId, CharSequence errString) { 316 if (DEBUG) { 317 Log.i(TAG,"onAuthenticationError: id=" + errMsgId + "; str=" + errString); 318 } 319 if (!mSelfCancelled) { 320 showToast(errString.toString()); 321 } 322 } 323 324 @Override onAuthenticationHelp(int helpMsgId, CharSequence helpString)325 public void onAuthenticationHelp(int helpMsgId, CharSequence helpString) { 326 showToast(helpString.toString()); 327 } 328 329 @Override onAuthenticationFailed()330 public void onAuthenticationFailed() { 331 if (DEBUG) { 332 Log.i(TAG,"onAuthenticationFailed"); 333 } 334 showToast(getString(R.string.sec_fp_auth_failed)); 335 } 336 337 @Override onAuthenticationSucceeded(FingerprintManager.AuthenticationResult result)338 public void onAuthenticationSucceeded(FingerprintManager.AuthenticationResult result) { 339 if (DEBUG) { 340 Log.i(TAG,"onAuthenticationSucceeded"); 341 } 342 hasStrongBox = getContext().getPackageManager() 343 .hasSystemFeature(PackageManager.FEATURE_STRONGBOX_KEYSTORE); 344 if (mActivity.tryEncrypt() && 345 mActivity.doValidityDurationTest(false)) { 346 try { 347 Thread.sleep(3000); 348 } catch (Exception e) { 349 throw new RuntimeException("Failed to sleep", e); 350 } 351 if (!mActivity.doValidityDurationTest(false)) { 352 showToast(String.format("Test passed. useStrongBox: %b", 353 mActivity.useStrongBox)); 354 if (mActivity.useStrongBox || !hasStrongBox) { 355 mActivity.getPassButton().setEnabled(true); 356 } else { 357 showToast("Rerunning with StrongBox"); 358 } 359 FingerprintAuthDialogFragment.this.dismiss(); 360 } else { 361 showToast("Test failed. Key accessible after validity time limit."); 362 } 363 } else { 364 showToast("Test failed. Key not accessible after auth"); 365 } 366 } 367 } 368 369 @Override onDismiss(DialogInterface dialog)370 public void onDismiss(DialogInterface dialog) { 371 mCancellationSignal.cancel(); 372 mSelfCancelled = true; 373 // Start the test again, but with StrongBox if supported 374 if (!mActivity.useStrongBox && hasStrongBox) { 375 mActivity.useStrongBox = true; 376 mActivity.startTest(); 377 } 378 } 379 setActivity(FingerprintBoundKeysTest activity)380 private void setActivity(FingerprintBoundKeysTest activity) { 381 mActivity = activity; 382 } 383 showToast(String message)384 private void showToast(String message) { 385 Toast.makeText(getContext(), message, Toast.LENGTH_LONG) 386 .show(); 387 } 388 389 @Override onCreateDialog(Bundle savedInstanceState)390 public Dialog onCreateDialog(Bundle savedInstanceState) { 391 mCancellationSignal = new CancellationSignal(); 392 mSelfCancelled = false; 393 mFingerprintManager = 394 (FingerprintManager) getContext().getSystemService(Context.FINGERPRINT_SERVICE); 395 mFingerprintManagerCallback = new FingerprintManagerCallback(); 396 mFingerprintManager.authenticate( 397 new FingerprintManager.CryptoObject(mActivity.mCipher), 398 mCancellationSignal, 0, mFingerprintManagerCallback, null); 399 AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); 400 builder.setMessage(R.string.sec_fp_dialog_message); 401 return builder.create(); 402 } 403 404 } 405 } 406 407 408