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