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
FingerprintAuthenticationDialogFragment()82     public FingerprintAuthenticationDialogFragment() {}
83 
84     @Override
onCreate(Bundle savedInstanceState)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
onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)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
onResume()143     public void onResume() {
144         super.onResume();
145         if (mStage == Stage.FINGERPRINT) {
146             mFingerprintUiHelper.startListening(mCryptoObject);
147         }
148     }
149 
setStage(Stage stage)150     public void setStage(Stage stage) {
151         mStage = stage;
152     }
153 
154     @Override
onPause()155     public void onPause() {
156         super.onPause();
157         mFingerprintUiHelper.stopListening();
158     }
159 
160     @Override
onAttach(Context context)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      */
setCryptoObject(FingerprintManager.CryptoObject cryptoObject)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      */
goToBackup()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      */
enroll()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      */
verifyPassword()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 
updateStage()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
onEditorAction(TextView v, int actionId, KeyEvent event)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
onAuthenticated()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
onError()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