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
onCreate(Bundle savedInstanceState)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      */
initCipher(Cipher cipher, String keyName)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      */
onPurchased(boolean withFingerprint, @Nullable FingerprintManager.CryptoObject cryptoObject)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.
showConfirmation(byte[] encrypted)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      */
tryEncrypt(Cipher cipher)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      */
createKey(String keyName, boolean invalidatedByBiometricEnrollment)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
onCreateOptionsMenu(Menu menu)268     public boolean onCreateOptionsMenu(Menu menu) {
269         getMenuInflater().inflate(R.menu.menu_main, menu);
270         return true;
271     }
272 
273     @Override
onOptionsItemSelected(MenuItem item)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 
PurchaseButtonClickListener(Cipher cipher, String keyName)290         PurchaseButtonClickListener(Cipher cipher, String keyName) {
291             mCipher = cipher;
292             mKeyName = keyName;
293         }
294 
295         @Override
onClick(View view)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