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.android.cts.verifier.security;
18 
19 import android.Manifest;
20 import android.app.AlertDialog;
21 import android.app.Dialog;
22 import android.app.DialogFragment;
23 import android.app.KeyguardManager;
24 import android.content.Context;
25 import android.content.DialogInterface;
26 import android.content.pm.PackageManager;
27 import android.hardware.fingerprint.FingerprintManager;
28 import android.os.Bundle;
29 import android.os.CancellationSignal;
30 import android.security.keystore.KeyGenParameterSpec;
31 import android.security.keystore.KeyPermanentlyInvalidatedException;
32 import android.security.keystore.KeyProperties;
33 import android.security.keystore.UserNotAuthenticatedException;
34 import android.util.Log;
35 import android.view.View;
36 import android.view.View.OnClickListener;
37 import android.widget.Button;
38 import android.widget.Toast;
39 
40 import com.android.cts.verifier.PassFailButtons;
41 import com.android.cts.verifier.R;
42 
43 import java.io.IOException;
44 import java.security.InvalidAlgorithmParameterException;
45 import java.security.InvalidKeyException;
46 import java.security.KeyStore;
47 import java.security.KeyStoreException;
48 import java.security.NoSuchAlgorithmException;
49 import java.security.NoSuchProviderException;
50 import java.security.UnrecoverableKeyException;
51 import java.security.cert.CertificateException;
52 
53 import javax.crypto.BadPaddingException;
54 import javax.crypto.Cipher;
55 import javax.crypto.IllegalBlockSizeException;
56 import javax.crypto.KeyGenerator;
57 import javax.crypto.NoSuchPaddingException;
58 import javax.crypto.SecretKey;
59 
60 public class FingerprintBoundKeysTest extends PassFailButtons.Activity {
61     private static final boolean DEBUG = false;
62     private static final String TAG = "FingerprintBoundKeysTest";
63 
64     /** Alias for our key in the Android Key Store. */
65     private static final String KEY_NAME = "my_key";
66     private static final byte[] SECRET_BYTE_ARRAY = new byte[] {1, 2, 3, 4, 5, 6};
67     private static final int AUTHENTICATION_DURATION_SECONDS = 2;
68     private static final int CONFIRM_CREDENTIALS_REQUEST_CODE = 1;
69     private static final int BIOMETRIC_REQUEST_PERMISSION_CODE = 0;
70 
71     protected boolean useStrongBox;
72 
73     private FingerprintManager mFingerprintManager;
74     private KeyguardManager mKeyguardManager;
75     private FingerprintAuthDialogFragment mFingerprintDialog;
76     private Cipher mCipher;
77 
getTitleRes()78     protected int getTitleRes() {
79         return R.string.sec_fingerprint_bound_key_test;
80     }
81 
getDescriptionRes()82     protected int getDescriptionRes() {
83         return R.string.sec_fingerprint_bound_key_test_info;
84     }
85 
86     @Override
onCreate(Bundle savedInstanceState)87     protected void onCreate(Bundle savedInstanceState) {
88         super.onCreate(savedInstanceState);
89         setContentView(R.layout.sec_screen_lock_keys_main);
90         setPassFailButtonClickListeners();
91         setInfoResources(getTitleRes(), getDescriptionRes(), -1);
92         getPassButton().setEnabled(false);
93         requestPermissions(new String[]{Manifest.permission.USE_BIOMETRIC},
94                 BIOMETRIC_REQUEST_PERMISSION_CODE);
95     }
96 
97     @Override
onRequestPermissionsResult(int requestCode, String[] permissions, int[] state)98     public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] state) {
99         if (requestCode == BIOMETRIC_REQUEST_PERMISSION_CODE && state[0] == PackageManager.PERMISSION_GRANTED) {
100             useStrongBox = false;
101             mFingerprintManager = (FingerprintManager) getSystemService(Context.FINGERPRINT_SERVICE);
102             mKeyguardManager = getSystemService(KeyguardManager.class);
103             Button startTestButton = findViewById(R.id.sec_start_test_button);
104 
105             if (!mKeyguardManager.isKeyguardSecure()) {
106                 // Show a message that the user hasn't set up a lock screen.
107                 showToast( "Secure lock screen hasn't been set up.\n"
108                                 + "Go to 'Settings -> Security -> Screen lock' to set up a lock screen");
109                 startTestButton.setEnabled(false);
110                 return;
111             }
112 
113             onPermissionsGranted();
114 
115             startTestButton.setOnClickListener(new OnClickListener() {
116                 @Override
117                 public void onClick(View v) {
118                     startTest();
119                 }
120             });
121         }
122     }
123 
124     /**
125      * Fingerprint-specific check before allowing test to be started
126      */
onPermissionsGranted()127     protected void onPermissionsGranted() {
128         mFingerprintManager = getSystemService(FingerprintManager.class);
129         if (!mFingerprintManager.hasEnrolledFingerprints()) {
130             showToast("No fingerprints enrolled.\n"
131                     + "Go to 'Settings -> Security -> Fingerprint' to set up a fingerprint");
132             Button startTestButton = findViewById(R.id.sec_start_test_button);
133             startTestButton.setEnabled(false);
134         }
135     }
136 
startTest()137     protected void startTest() {
138         createKey(false /* hasValidityDuration */);
139         prepareEncrypt();
140         if (tryEncrypt()) {
141             showToast("Test failed. Key accessible without auth.");
142         } else {
143             prepareEncrypt();
144             showAuthenticationScreen();
145         }
146     }
147 
148     /**
149      * Creates a symmetric key in the Android Key Store which requires auth
150      */
createKey(boolean hasValidityDuration)151     private void createKey(boolean hasValidityDuration) {
152         // Generate a key to decrypt payment credentials, tokens, etc.
153         // This will most likely be a registration step for the user when they are setting up your app.
154         try {
155             KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
156             keyStore.load(null);
157             KeyGenerator keyGenerator = KeyGenerator.getInstance(
158                     KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore");
159 
160             // Set the alias of the entry in Android KeyStore where the key will appear
161             // and the constrains (purposes) in the constructor of the Builder
162             keyGenerator.init(new KeyGenParameterSpec.Builder(KEY_NAME,
163                     KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
164                     .setBlockModes(KeyProperties.BLOCK_MODE_CBC)
165                     .setUserAuthenticationRequired(true)
166                     .setUserAuthenticationValidityDurationSeconds(
167                         hasValidityDuration ? AUTHENTICATION_DURATION_SECONDS : -1)
168                     .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
169                     .setIsStrongBoxBacked(useStrongBox)
170                     .build());
171             keyGenerator.generateKey();
172             if (DEBUG) {
173                 Log.i(TAG, "createKey: [1]: done");
174             }
175         } catch (NoSuchAlgorithmException | NoSuchProviderException
176                 | InvalidAlgorithmParameterException | KeyStoreException
177                 | CertificateException | IOException e) {
178             if (DEBUG) {
179                 Log.i(TAG, "createKey: [2]: failed");
180             }
181             throw new RuntimeException("Failed to create a symmetric key", e);
182         }
183     }
184 
185     /**
186      * create and init cipher; has to be done before we do auth
187      */
prepareEncrypt()188     private boolean prepareEncrypt() {
189         return encryptInternal(false);
190     }
191 
192     /**
193      * Tries to encrypt some data with the generated key in {@link #createKey} which is
194      * only works if the user has just authenticated via device credentials.
195      * has to be run after successful auth, in order to succeed
196      */
tryEncrypt()197     protected boolean tryEncrypt() {
198         return encryptInternal(true);
199     }
200 
getCipher()201     protected Cipher getCipher() {
202         return mCipher;
203     }
204 
doValidityDurationTest(boolean useStrongBox)205     protected boolean doValidityDurationTest(boolean useStrongBox) {
206         mCipher = null;
207         createKey(true /* hasValidityDuration */);
208         if (prepareEncrypt()) {
209             return tryEncrypt();
210         }
211         return false;
212     }
213 
encryptInternal(boolean doEncrypt)214     private boolean encryptInternal(boolean doEncrypt) {
215         try {
216             if (!doEncrypt) {
217                 KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
218                 keyStore.load(null);
219                 SecretKey secretKey = (SecretKey) keyStore.getKey(KEY_NAME, null);
220                 if (DEBUG) {
221                     Log.i(TAG, "encryptInternal: [1]: key retrieved");
222                 }
223                 if (mCipher == null) {
224                     mCipher = Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/"
225                             + KeyProperties.BLOCK_MODE_CBC + "/"
226                             + KeyProperties.ENCRYPTION_PADDING_PKCS7);
227                 }
228                 mCipher.init(Cipher.ENCRYPT_MODE, secretKey);
229                 if (DEBUG) {
230                     Log.i(TAG, "encryptInternal: [2]: cipher initialized");
231                 }
232             } else {
233                 mCipher.doFinal(SECRET_BYTE_ARRAY);
234                 if (DEBUG) {
235                     Log.i(TAG, "encryptInternal: [3]: encryption performed");
236                 }
237             }
238             return true;
239         } catch (BadPaddingException | IllegalBlockSizeException e) {
240             // this happens in "no-error" scenarios routinely;
241             // All we want it to see the event in the log;
242             // Extra exception info is not valuable
243             if (DEBUG) {
244                 Log.w(TAG, "encryptInternal: [4]: Encryption failed", e);
245             }
246             return false;
247         } catch (KeyPermanentlyInvalidatedException e) {
248             // Extra exception info is not of big value, but let's have it,
249             // since this is an unlikely sutuation and potential error condition
250             Log.w(TAG, "encryptInternal: [5]: Key invalidated", e);
251             createKey(false /* hasValidityDuration */);
252             showToast("The key has been invalidated, please try again.\n");
253             return false;
254         } catch (UserNotAuthenticatedException e) {
255             Log.w(TAG, "encryptInternal: [6]: User not authenticated", e);
256             return false;
257         } catch (NoSuchPaddingException | KeyStoreException | CertificateException
258                  | UnrecoverableKeyException | IOException
259                  | NoSuchAlgorithmException | InvalidKeyException e) {
260             throw new RuntimeException("Failed to init Cipher", e);
261         }
262     }
263 
showAuthenticationScreen()264     protected void showAuthenticationScreen() {
265         mFingerprintDialog = new FingerprintAuthDialogFragment();
266         mFingerprintDialog.setActivity(this);
267         mFingerprintDialog.show(getFragmentManager(), "fingerprint_dialog");
268     }
269 
showToast(String message)270     protected void showToast(String message) {
271         Toast.makeText(this, message, Toast.LENGTH_LONG).show();
272     }
273 
274     public static class FingerprintAuthDialogFragment extends DialogFragment {
275 
276         private FingerprintBoundKeysTest mActivity;
277         private CancellationSignal mCancellationSignal;
278         private FingerprintManager mFingerprintManager;
279         private FingerprintManagerCallback mFingerprintManagerCallback;
280         private boolean mSelfCancelled;
281         private boolean hasStrongBox;
282 
283         class FingerprintManagerCallback extends FingerprintManager.AuthenticationCallback {
284             @Override
onAuthenticationError(int errMsgId, CharSequence errString)285             public void onAuthenticationError(int errMsgId, CharSequence errString) {
286                 if (DEBUG) {
287                     Log.i(TAG,"onAuthenticationError: id=" + errMsgId + "; str=" + errString);
288                 }
289                 if (!mSelfCancelled) {
290                     showToast(errString.toString());
291                 }
292             }
293 
294             @Override
onAuthenticationHelp(int helpMsgId, CharSequence helpString)295             public void onAuthenticationHelp(int helpMsgId, CharSequence helpString) {
296                 showToast(helpString.toString());
297             }
298 
299             @Override
onAuthenticationFailed()300             public void onAuthenticationFailed() {
301                 if (DEBUG) {
302                     Log.i(TAG,"onAuthenticationFailed");
303                 }
304                 showToast(getString(R.string.sec_fp_auth_failed));
305             }
306 
307             @Override
onAuthenticationSucceeded(FingerprintManager.AuthenticationResult result)308             public void onAuthenticationSucceeded(FingerprintManager.AuthenticationResult result) {
309                 if (DEBUG) {
310                     Log.i(TAG,"onAuthenticationSucceeded");
311                 }
312                 hasStrongBox = getContext().getPackageManager()
313                                     .hasSystemFeature(PackageManager.FEATURE_STRONGBOX_KEYSTORE);
314                 if (mActivity.tryEncrypt() &&
315                     mActivity.doValidityDurationTest(false)) {
316                     try {
317                         Thread.sleep(3000);
318                     } catch (Exception e) {
319                         throw new RuntimeException("Failed to sleep", e);
320                     }
321                     if (!mActivity.doValidityDurationTest(false)) {
322                         showToast(String.format("Test passed. useStrongBox: %b",
323                                                 mActivity.useStrongBox));
324                         if (mActivity.useStrongBox || !hasStrongBox) {
325                             mActivity.getPassButton().setEnabled(true);
326                         } else {
327                             showToast("Rerunning with StrongBox");
328                         }
329                         FingerprintAuthDialogFragment.this.dismiss();
330                     } else {
331                         showToast("Test failed. Key accessible after validity time limit.");
332                     }
333                 } else {
334                     showToast("Test failed. Key not accessible after auth");
335                 }
336             }
337         }
338 
339         @Override
onDismiss(DialogInterface dialog)340         public void onDismiss(DialogInterface dialog) {
341             mCancellationSignal.cancel();
342             mSelfCancelled = true;
343             // Start the test again, but with StrongBox if supported
344             if (!mActivity.useStrongBox && hasStrongBox) {
345                 mActivity.useStrongBox = true;
346                 mActivity.startTest();
347             }
348         }
349 
setActivity(FingerprintBoundKeysTest activity)350         private void setActivity(FingerprintBoundKeysTest activity) {
351             mActivity = activity;
352         }
353 
showToast(String message)354         private void showToast(String message) {
355             Toast.makeText(getContext(), message, Toast.LENGTH_LONG)
356                 .show();
357         }
358 
359         @Override
onCreateDialog(Bundle savedInstanceState)360         public Dialog onCreateDialog(Bundle savedInstanceState) {
361             mCancellationSignal = new CancellationSignal();
362             mSelfCancelled = false;
363             mFingerprintManager =
364                     (FingerprintManager) getContext().getSystemService(Context.FINGERPRINT_SERVICE);
365             mFingerprintManagerCallback = new FingerprintManagerCallback();
366             mFingerprintManager.authenticate(
367                     new FingerprintManager.CryptoObject(mActivity.mCipher),
368                     mCancellationSignal, 0, mFingerprintManagerCallback, null);
369             AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
370             builder.setMessage(R.string.sec_fp_dialog_message);
371             return builder.create();
372         }
373 
374     }
375 }
376 
377 
378