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.example.android.fingerprintdialog; 18 19 import android.app.Activity; 20 import android.app.KeyguardManager; 21 import android.content.Intent; 22 import android.content.SharedPreferences; 23 import android.hardware.fingerprint.FingerprintManager; 24 import android.os.Build; 25 import android.os.Bundle; 26 import android.preference.PreferenceManager; 27 import android.security.keystore.KeyGenParameterSpec; 28 import android.security.keystore.KeyPermanentlyInvalidatedException; 29 import android.security.keystore.KeyProperties; 30 import android.support.annotation.Nullable; 31 import android.util.Base64; 32 import android.util.Log; 33 import android.view.Menu; 34 import android.view.MenuItem; 35 import android.view.View; 36 import android.widget.Button; 37 import android.widget.TextView; 38 import android.widget.Toast; 39 40 import java.io.IOException; 41 import java.security.InvalidAlgorithmParameterException; 42 import java.security.InvalidKeyException; 43 import java.security.KeyStore; 44 import java.security.KeyStoreException; 45 import java.security.NoSuchAlgorithmException; 46 import java.security.NoSuchProviderException; 47 import java.security.UnrecoverableKeyException; 48 import java.security.cert.CertificateException; 49 50 import javax.crypto.BadPaddingException; 51 import javax.crypto.Cipher; 52 import javax.crypto.IllegalBlockSizeException; 53 import javax.crypto.KeyGenerator; 54 import javax.crypto.NoSuchPaddingException; 55 import javax.crypto.SecretKey; 56 57 /** 58 * Main entry point for the sample, showing a backpack and "Purchase" button. 59 */ 60 public class MainActivity extends Activity { 61 62 private static final String TAG = MainActivity.class.getSimpleName(); 63 64 private static final String DIALOG_FRAGMENT_TAG = "myFragment"; 65 private static final String SECRET_MESSAGE = "Very secret message"; 66 private static final String KEY_NAME_NOT_INVALIDATED = "key_not_invalidated"; 67 static final String DEFAULT_KEY_NAME = "default_key"; 68 69 private KeyStore mKeyStore; 70 private KeyGenerator mKeyGenerator; 71 private SharedPreferences mSharedPreferences; 72 73 @Override onCreate(Bundle savedInstanceState)74 protected void onCreate(Bundle savedInstanceState) { 75 super.onCreate(savedInstanceState); 76 setContentView(R.layout.activity_main); 77 78 try { 79 mKeyStore = KeyStore.getInstance("AndroidKeyStore"); 80 } catch (KeyStoreException e) { 81 throw new RuntimeException("Failed to get an instance of KeyStore", e); 82 } 83 try { 84 mKeyGenerator = KeyGenerator 85 .getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore"); 86 } catch (NoSuchAlgorithmException | NoSuchProviderException e) { 87 throw new RuntimeException("Failed to get an instance of KeyGenerator", e); 88 } 89 Cipher defaultCipher; 90 Cipher cipherNotInvalidated; 91 try { 92 defaultCipher = Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/" 93 + KeyProperties.BLOCK_MODE_CBC + "/" 94 + KeyProperties.ENCRYPTION_PADDING_PKCS7); 95 cipherNotInvalidated = Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/" 96 + KeyProperties.BLOCK_MODE_CBC + "/" 97 + KeyProperties.ENCRYPTION_PADDING_PKCS7); 98 } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { 99 throw new RuntimeException("Failed to get an instance of Cipher", e); 100 } 101 mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); 102 103 KeyguardManager keyguardManager = getSystemService(KeyguardManager.class); 104 FingerprintManager fingerprintManager = getSystemService(FingerprintManager.class); 105 Button purchaseButton = (Button) findViewById(R.id.purchase_button); 106 Button purchaseButtonNotInvalidated = (Button) findViewById( 107 R.id.purchase_button_not_invalidated); 108 109 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { 110 purchaseButtonNotInvalidated.setEnabled(true); 111 purchaseButtonNotInvalidated.setOnClickListener( 112 new PurchaseButtonClickListener(cipherNotInvalidated, 113 KEY_NAME_NOT_INVALIDATED)); 114 } else { 115 // Hide the purchase button which uses a non-invalidated key 116 // if the app doesn't work on Android N preview 117 purchaseButtonNotInvalidated.setVisibility(View.GONE); 118 findViewById(R.id.purchase_button_not_invalidated_description) 119 .setVisibility(View.GONE); 120 } 121 122 if (!keyguardManager.isKeyguardSecure()) { 123 // Show a message that the user hasn't set up a fingerprint or lock screen. 124 Toast.makeText(this, 125 "Secure lock screen hasn't set up.\n" 126 + "Go to 'Settings -> Security -> Fingerprint' to set up a fingerprint", 127 Toast.LENGTH_LONG).show(); 128 purchaseButton.setEnabled(false); 129 purchaseButtonNotInvalidated.setEnabled(false); 130 return; 131 } 132 133 // Now the protection level of USE_FINGERPRINT permission is normal instead of dangerous. 134 // See http://developer.android.com/reference/android/Manifest.permission.html#USE_FINGERPRINT 135 // The line below prevents the false positive inspection from Android Studio 136 // noinspection ResourceType 137 if (!fingerprintManager.hasEnrolledFingerprints()) { 138 purchaseButton.setEnabled(false); 139 // This happens when no fingerprints are registered. 140 Toast.makeText(this, 141 "Go to 'Settings -> Security -> Fingerprint' and register at least one fingerprint", 142 Toast.LENGTH_LONG).show(); 143 return; 144 } 145 createKey(DEFAULT_KEY_NAME, true); 146 createKey(KEY_NAME_NOT_INVALIDATED, false); 147 purchaseButton.setEnabled(true); 148 purchaseButton.setOnClickListener( 149 new PurchaseButtonClickListener(defaultCipher, DEFAULT_KEY_NAME)); 150 } 151 152 /** 153 * Initialize the {@link Cipher} instance with the created key in the 154 * {@link #createKey(String, boolean)} method. 155 * 156 * @param keyName the key name to init the cipher 157 * @return {@code true} if initialization is successful, {@code false} if the lock screen has 158 * been disabled or reset after the key was generated, or if a fingerprint got enrolled after 159 * the key was generated. 160 */ initCipher(Cipher cipher, String keyName)161 private boolean initCipher(Cipher cipher, String keyName) { 162 try { 163 mKeyStore.load(null); 164 SecretKey key = (SecretKey) mKeyStore.getKey(keyName, null); 165 cipher.init(Cipher.ENCRYPT_MODE, key); 166 return true; 167 } catch (KeyPermanentlyInvalidatedException e) { 168 return false; 169 } catch (KeyStoreException | CertificateException | UnrecoverableKeyException | IOException 170 | NoSuchAlgorithmException | InvalidKeyException e) { 171 throw new RuntimeException("Failed to init Cipher", e); 172 } 173 } 174 175 /** 176 * Proceed the purchase operation 177 * 178 * @param withFingerprint {@code true} if the purchase was made by using a fingerprint 179 * @param cryptoObject the Crypto object 180 */ onPurchased(boolean withFingerprint, @Nullable FingerprintManager.CryptoObject cryptoObject)181 public void onPurchased(boolean withFingerprint, 182 @Nullable FingerprintManager.CryptoObject cryptoObject) { 183 if (withFingerprint) { 184 // If the user has authenticated with fingerprint, verify that using cryptography and 185 // then show the confirmation message. 186 assert cryptoObject != null; 187 tryEncrypt(cryptoObject.getCipher()); 188 } else { 189 // Authentication happened with backup password. Just show the confirmation message. 190 showConfirmation(null); 191 } 192 } 193 194 // Show confirmation, if fingerprint was used show crypto information. showConfirmation(byte[] encrypted)195 private void showConfirmation(byte[] encrypted) { 196 findViewById(R.id.confirmation_message).setVisibility(View.VISIBLE); 197 if (encrypted != null) { 198 TextView v = (TextView) findViewById(R.id.encrypted_message); 199 v.setVisibility(View.VISIBLE); 200 v.setText(Base64.encodeToString(encrypted, 0 /* flags */)); 201 } 202 } 203 204 /** 205 * Tries to encrypt some data with the generated key in {@link #createKey} which is 206 * only works if the user has just authenticated via fingerprint. 207 */ tryEncrypt(Cipher cipher)208 private void tryEncrypt(Cipher cipher) { 209 try { 210 byte[] encrypted = cipher.doFinal(SECRET_MESSAGE.getBytes()); 211 showConfirmation(encrypted); 212 } catch (BadPaddingException | IllegalBlockSizeException e) { 213 Toast.makeText(this, "Failed to encrypt the data with the generated key. " 214 + "Retry the purchase", Toast.LENGTH_LONG).show(); 215 Log.e(TAG, "Failed to encrypt the data with the generated key." + e.getMessage()); 216 } 217 } 218 219 /** 220 * Creates a symmetric key in the Android Key Store which can only be used after the user has 221 * authenticated with fingerprint. 222 * 223 * @param keyName the name of the key to be created 224 * @param invalidatedByBiometricEnrollment if {@code false} is passed, the created key will not 225 * be invalidated even if a new fingerprint is enrolled. 226 * The default value is {@code true}, so passing 227 * {@code true} doesn't change the behavior 228 * (the key will be invalidated if a new fingerprint is 229 * enrolled.). Note that this parameter is only valid if 230 * the app works on Android N developer preview. 231 * 232 */ createKey(String keyName, boolean invalidatedByBiometricEnrollment)233 public void createKey(String keyName, boolean invalidatedByBiometricEnrollment) { 234 // The enrolling flow for fingerprint. This is where you ask the user to set up fingerprint 235 // for your flow. Use of keys is necessary if you need to know if the set of 236 // enrolled fingerprints has changed. 237 try { 238 mKeyStore.load(null); 239 // Set the alias of the entry in Android KeyStore where the key will appear 240 // and the constrains (purposes) in the constructor of the Builder 241 242 KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec.Builder(keyName, 243 KeyProperties.PURPOSE_ENCRYPT | 244 KeyProperties.PURPOSE_DECRYPT) 245 .setBlockModes(KeyProperties.BLOCK_MODE_CBC) 246 // Require the user to authenticate with a fingerprint to authorize every use 247 // of the key 248 .setUserAuthenticationRequired(true) 249 .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7); 250 251 // This is a workaround to avoid crashes on devices whose API level is < 24 252 // because KeyGenParameterSpec.Builder#setInvalidatedByBiometricEnrollment is only 253 // visible on API level +24. 254 // Ideally there should be a compat library for KeyGenParameterSpec.Builder but 255 // which isn't available yet. 256 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { 257 builder.setInvalidatedByBiometricEnrollment(invalidatedByBiometricEnrollment); 258 } 259 mKeyGenerator.init(builder.build()); 260 mKeyGenerator.generateKey(); 261 } catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException 262 | CertificateException | IOException e) { 263 throw new RuntimeException(e); 264 } 265 } 266 267 @Override onCreateOptionsMenu(Menu menu)268 public boolean onCreateOptionsMenu(Menu menu) { 269 getMenuInflater().inflate(R.menu.menu_main, menu); 270 return true; 271 } 272 273 @Override onOptionsItemSelected(MenuItem item)274 public boolean onOptionsItemSelected(MenuItem item) { 275 int id = item.getItemId(); 276 277 if (id == R.id.action_settings) { 278 Intent intent = new Intent(this, SettingsActivity.class); 279 startActivity(intent); 280 return true; 281 } 282 return super.onOptionsItemSelected(item); 283 } 284 285 private class PurchaseButtonClickListener implements View.OnClickListener { 286 287 Cipher mCipher; 288 String mKeyName; 289 PurchaseButtonClickListener(Cipher cipher, String keyName)290 PurchaseButtonClickListener(Cipher cipher, String keyName) { 291 mCipher = cipher; 292 mKeyName = keyName; 293 } 294 295 @Override onClick(View view)296 public void onClick(View view) { 297 findViewById(R.id.confirmation_message).setVisibility(View.GONE); 298 findViewById(R.id.encrypted_message).setVisibility(View.GONE); 299 300 // Set up the crypto object for later. The object will be authenticated by use 301 // of the fingerprint. 302 if (initCipher(mCipher, mKeyName)) { 303 304 // Show the fingerprint dialog. The user has the option to use the fingerprint with 305 // crypto, or you can fall back to using a server-side verified password. 306 FingerprintAuthenticationDialogFragment fragment 307 = new FingerprintAuthenticationDialogFragment(); 308 fragment.setCryptoObject(new FingerprintManager.CryptoObject(mCipher)); 309 boolean useFingerprintPreference = mSharedPreferences 310 .getBoolean(getString(R.string.use_fingerprint_to_authenticate_key), 311 true); 312 if (useFingerprintPreference) { 313 fragment.setStage( 314 FingerprintAuthenticationDialogFragment.Stage.FINGERPRINT); 315 } else { 316 fragment.setStage( 317 FingerprintAuthenticationDialogFragment.Stage.PASSWORD); 318 } 319 fragment.show(getFragmentManager(), DIALOG_FRAGMENT_TAG); 320 } else { 321 // This happens if the lock screen has been disabled or or a fingerprint got 322 // enrolled. Thus show the dialog to authenticate with their password first 323 // and ask the user if they want to authenticate with fingerprints in the 324 // future 325 FingerprintAuthenticationDialogFragment fragment 326 = new FingerprintAuthenticationDialogFragment(); 327 fragment.setCryptoObject(new FingerprintManager.CryptoObject(mCipher)); 328 fragment.setStage( 329 FingerprintAuthenticationDialogFragment.Stage.NEW_FINGERPRINT_ENROLLED); 330 fragment.show(getFragmentManager(), DIALOG_FRAGMENT_TAG); 331 } 332 } 333 } 334 } 335