Skip to content

Most visited

Recently visited

navigation
AsymmetricFingerprintDialog / src / com.example.android.asymmetricfingerprintdialog /

FingerprintAuthenticationDialogFragment.java

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
</