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