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