Home | History | Annotate | Download | only in asymmetricfingerprintdialog
      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.asymmetricfingerprintdialog;
     18 
     19 import com.example.android.asymmetricfingerprintdialog.server.StoreBackend;
     20 import com.example.android.asymmetricfingerprintdialog.server.Transaction;
     21 
     22 import android.app.DialogFragment;
     23 import android.content.Context;
     24 import android.content.SharedPreferences;
     25 import android.hardware.fingerprint.FingerprintManager;
     26 import android.os.Bundle;
     27 import android.view.KeyEvent;
     28 import android.view.LayoutInflater;
     29 import android.view.View;
     30 import android.view.ViewGroup;
     31 import android.view.inputmethod.EditorInfo;
     32 import android.view.inputmethod.InputMethodManager;
     33 import android.widget.Button;
     34 import android.widget.CheckBox;
     35 import android.widget.EditText;
     36 import android.widget.ImageView;
     37 import android.widget.TextView;
     38 
     39 import java.io.IOException;
     40 import java.security.KeyFactory;
     41 import java.security.KeyStore;
     42 import java.security.KeyStoreException;
     43 import java.security.NoSuchAlgorithmException;
     44 import java.security.PublicKey;
     45 import java.security.SecureRandom;
     46 import java.security.Signature;
     47 import java.security.SignatureException;
     48 import java.security.cert.CertificateException;
     49 import java.security.spec.InvalidKeySpecException;
     50 import java.security.spec.X509EncodedKeySpec;
     51 
     52 import javax.inject.Inject;
     53 
     54 /**
     55  * A dialog which uses fingerprint APIs to authenticate the user, and falls back to password
     56  * authentication if fingerprint is not available.
     57  */
     58 public class FingerprintAuthenticationDialogFragment extends DialogFragment
     59         implements TextView.OnEditorActionListener, FingerprintUiHelper.Callback {
     60 
     61     private Button mCancelButton;
     62     private Button mSecondDialogButton;
     63     private View mFingerprintContent;
     64     private View mBackupContent;
     65     private EditText mPassword;
     66     private CheckBox mUseFingerprintFutureCheckBox;
     67     private TextView mPasswordDescriptionTextView;
     68     private TextView mNewFingerprintEnrolledTextView;
     69 
     70     private Stage mStage = Stage.FINGERPRINT;
     71 
     72     private FingerprintManager.CryptoObject mCryptoObject;
     73     private FingerprintUiHelper mFingerprintUiHelper;
     74     private MainActivity mActivity;
     75 
     76     @Inject FingerprintUiHelper.FingerprintUiHelperBuilder mFingerprintUiHelperBuilder;
     77     @Inject InputMethodManager mInputMethodManager;
     78     @Inject SharedPreferences mSharedPreferences;
     79     @Inject StoreBackend mStoreBackend;
     80 
     81     @Inject
     82     public FingerprintAuthenticationDialogFragment() {}
     83 
     84     @Override
     85     public void onCreate(Bundle savedInstanceState) {
     86         super.onCreate(savedInstanceState);
     87 
     88         // Do not create a new Fragment when the Activity is re-created such as orientation changes.
     89         setRetainInstance(true);
     90         setStyle(DialogFragment.STYLE_NORMAL, android.R.style.Theme_Material_Light_Dialog);
     91 
     92         // We register a new user account here. Real apps should do this with proper UIs.
     93         enroll();
     94     }
     95 
     96     @Override
     97     public View onCreateView(LayoutInflater inflater, ViewGroup container,
     98             Bundle savedInstanceState) {
     99         getDialog().setTitle(getString(R.string.sign_in));
    100         View v = inflater.inflate(R.layout.fingerprint_dialog_container, container, false);
    101         mCancelButton = (Button) v.findViewById(R.id.cancel_button);
    102         mCancelButton.setOnClickListener(new View.OnClickListener() {
    103             @Override
    104             public void onClick(View view) {
    105                 dismiss();
    106             }
    107         });
    108 
    109         mSecondDialogButton = (Button) v.findViewById(R.id.second_dialog_button);
    110         mSecondDialogButton.setOnClickListener(new View.OnClickListener() {
    111             @Override
    112             public void onClick(View view) {
    113                 if (mStage == Stage.FINGERPRINT) {
    114                     goToBackup();
    115                 } else {
    116                     verifyPassword();
    117                 }
    118             }
    119         });
    120         mFingerprintContent = v.findViewById(R.id.fingerprint_container);
    121         mBackupContent = v.findViewById(R.id.backup_container);
    122         mPassword = (EditText) v.findViewById(R.id.password);
    123         mPassword.setOnEditorActionListener(this);
    124         mPasswordDescriptionTextView = (TextView) v.findViewById(R.id.password_description);
    125         mUseFingerprintFutureCheckBox = (CheckBox)
    126                 v.findViewById(R.id.use_fingerprint_in_future_check);
    127         mNewFingerprintEnrolledTextView = (TextView)
    128                 v.findViewById(R.id.new_fingerprint_enrolled_description);
    129         mFingerprintUiHelper = mFingerprintUiHelperBuilder.build(
    130                 (ImageView) v.findViewById(R.id.fingerprint_icon),
    131                 (TextView) v.findViewById(R.id.fingerprint_status), this);
    132         updateStage();
    133 
    134         // If fingerprint authentication is not available, switch immediately to the backup
    135         // (password) screen.
    136         if (!mFingerprintUiHelper.isFingerprintAuthAvailable()) {
    137             goToBackup();
    138         }
    139         return v;
    140     }
    141 
    142     @Override
    143     public void onResume() {
    144         super.onResume();
    145         if (mStage == Stage.FINGERPRINT) {
    146             mFingerprintUiHelper.startListening(mCryptoObject);
    147         }
    148     }
    149 
    150     public void setStage(Stage stage) {
    151         mStage = stage;
    152     }
    153 
    154     @Override
    155     public void onPause() {
    156         super.onPause();
    157         mFingerprintUiHelper.stopListening();
    158     }
    159 
    160     @Override
    161     public void onAttach(Context context) {
    162         super.onAttach(context);
    163         mActivity = (MainActivity) getActivity();
    164     }
    165 
    166     /**
    167      * Sets the crypto object to be passed in when authenticating with fingerprint.
    168      */
    169     public void setCryptoObject(FingerprintManager.CryptoObject cryptoObject) {
    170         mCryptoObject = cryptoObject;
    171     }
    172 
    173     /**
    174      * Switches to backup (password) screen. This either can happen when fingerprint is not
    175      * available or the user chooses to use the password authentication method by pressing the
    176      * button. This can also happen when the user had too many fingerprint attempts.
    177      */
    178     private void goToBackup() {
    179         mStage = Stage.PASSWORD;
    180         updateStage();
    181         mPassword.requestFocus();
    182 
    183         // Show the keyboard.
    184         mPassword.postDelayed(mShowKeyboardRunnable, 500);
    185 
    186         // Fingerprint is not used anymore. Stop listening for it.
    187         mFingerprintUiHelper.stopListening();
    188     }
    189 
    190     /**
    191      * Enrolls a user to the fake backend.
    192      */
    193     private void enroll() {
    194         try {
    195             KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
    196             keyStore.load(null);
    197             PublicKey publicKey = keyStore.getCertificate(MainActivity.KEY_NAME).getPublicKey();
    198             // Provide the public key to the backend. In most cases, the key needs to be transmitted
    199             // to the backend over the network, for which Key.getEncoded provides a suitable wire
    200             // format (X.509 DER-encoded). The backend can then create a PublicKey instance from the
    201             // X.509 encoded form using KeyFactory.generatePublic. This conversion is also currently
    202             // needed on API Level 23 (Android M) due to a platform bug which prevents the use of
    203             // Android Keystore public keys when their private keys require user authentication.
    204             // This conversion creates a new public key which is not backed by Android Keystore and
    205             // thus is not affected by the bug.
    206             KeyFactory factory = KeyFactory.getInstance(publicKey.getAlgorithm());
    207             X509EncodedKeySpec spec = new X509EncodedKeySpec(publicKey.getEncoded());
    208             PublicKey verificationKey = factory.generatePublic(spec);
    209             mStoreBackend.enroll("user", "password", verificationKey);
    210         } catch (KeyStoreException | CertificateException | NoSuchAlgorithmException |
    211                 IOException | InvalidKeySpecException e) {
    212             e.printStackTrace();
    213         }
    214     }
    215 
    216     /**
    217      * Checks whether the current entered password is correct, and dismisses the the dialog and lets
    218      * the activity know about the result.
    219      */
    220     private void verifyPassword() {
    221         Transaction transaction = new Transaction("user", 1, new SecureRandom().nextLong());
    222         if (!mStoreBackend.verify(transaction, mPassword.getText().toString())) {
    223             return;
    224         }
    225         if (mStage == Stage.NEW_FINGERPRINT_ENROLLED) {
    226             SharedPreferences.Editor editor = mSharedPreferences.edit();
    227             editor.putBoolean(getString(R.string.use_fingerprint_to_authenticate_key),
    228                     mUseFingerprintFutureCheckBox.isChecked());
    229             editor.apply();
    230 
    231             if (mUseFingerprintFutureCheckBox.isChecked()) {
    232                 // Re-create the key so that fingerprints including new ones are validated.
    233                 mActivity.createKeyPair();
    234                 mStage = Stage.FINGERPRINT;
    235             }
    236         }
    237         mPassword.setText("");
    238         mActivity.onPurchased(null);
    239         dismiss();
    240     }
    241 
    242     private final Runnable mShowKeyboardRunnable = new Runnable() {
    243         @Override
    244         public void run() {
    245             mInputMethodManager.showSoftInput(mPassword, 0);
    246         }
    247     };
    248 
    249     private void updateStage() {
    250         switch (mStage) {
    251             case FINGERPRINT:
    252                 mCancelButton.setText(R.string.cancel);
    253                 mSecondDialogButton.setText(R.string.use_password);
    254                 mFingerprintContent.setVisibility(View.VISIBLE);
    255                 mBackupContent.setVisibility(View.GONE);
    256                 break;
    257             case NEW_FINGERPRINT_ENROLLED:
    258                 // Intentional fall through
    259             case PASSWORD:
    260                 mCancelButton.setText(R.string.cancel);
    261                 mSecondDialogButton.setText(R.string.ok);
    262                 mFingerprintContent.setVisibility(View.GONE);
    263                 mBackupContent.setVisibility(View.VISIBLE);
    264                 if (mStage == Stage.NEW_FINGERPRINT_ENROLLED) {
    265                     mPasswordDescriptionTextView.setVisibility(View.GONE);
    266                     mNewFingerprintEnrolledTextView.setVisibility(View.VISIBLE);
    267                     mUseFingerprintFutureCheckBox.setVisibility(View.VISIBLE);
    268                 }
    269                 break;
    270         }
    271     }
    272 
    273     @Override
    274     public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
    275         if (actionId == EditorInfo.IME_ACTION_GO) {
    276             verifyPassword();
    277             return true;
    278         }
    279         return false;
    280     }
    281 
    282     @Override
    283     public void onAuthenticated() {
    284         // Callback from FingerprintUiHelper. Let the activity know that authentication was
    285         // successful.
    286         mPassword.setText("");
    287         Signature signature = mCryptoObject.getSignature();
    288         // Include a client nonce in the transaction so that the nonce is also signed by the private
    289         // key and the backend can verify that the same nonce can't be used to prevent replay
    290         // attacks.
    291         Transaction transaction = new Transaction("user", 1, new SecureRandom().nextLong());
    292         try {
    293             signature.update(transaction.toByteArray());
    294             byte[] sigBytes = signature.sign();
    295             if (mStoreBackend.verify(transaction, sigBytes)) {
    296                 mActivity.onPurchased(sigBytes);
    297                 dismiss();
    298             } else {
    299                 mActivity.onPurchaseFailed();
    300                 dismiss();
    301             }
    302         } catch (SignatureException e) {
    303             throw new RuntimeException(e);
    304         }
    305     }
    306 
    307     @Override
    308     public void onError() {
    309         goToBackup();
    310     }
    311 
    312     /**
    313      * Enumeration to indicate which authentication method the user is trying to authenticate with.
    314      */
    315     public enum Stage {
    316         FINGERPRINT,
    317         NEW_FINGERPRINT_ENROLLED,
    318         PASSWORD
    319     }
    320 }
    321