Home | History | Annotate | Download | only in com.example.android.fingerprintdialog
      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
     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      */
    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      */
    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.
    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      */
    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      */
    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
    268     public boolean onCreateOptionsMenu(Menu menu) {
    269         getMenuInflater().inflate(R.menu.menu_main, menu);
    270         return true;
    271     }
    272 
    273     @Override
    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 
    290         PurchaseButtonClickListener(Cipher cipher, String keyName) {
    291             mCipher = cipher;
    292             mKeyName = keyName;
    293         }
    294 
    295         @Override
    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