diff options
author | Takeshi Hagikura <thagikura@google.com> | 2016-07-07 18:23:43 +0900 |
---|---|---|
committer | Takeshi Hagikura <thagikura@google.com> | 2016-07-07 18:26:48 +0900 |
commit | 821f126d7c5df09a2e97f42042f944e178ebf9bd (patch) | |
tree | 8015d238b16e10e5da5b97c31404b18000e460cd /security | |
parent | 4ebae4e3472e82612b27a5618790a94af2ec134f (diff) | |
download | android-821f126d7c5df09a2e97f42042f944e178ebf9bd.tar.gz |
Add another purchase button that uses a key which isn't invalidated even if a
new fingerprint is enrolled.
This CL includes the changes in the previous http://ag/913148 and the change
to fix the build by "make FingerprintDialog" (The previous http://ag/913148
broke the build so was reverted)
Change-Id: Ia2dd793ce041b4720b7c7105041e942d82a35f86
Diffstat (limited to 'security')
6 files changed, 155 insertions, 63 deletions
diff --git a/security/FingerprintDialog/Application/src/main/Android.mk b/security/FingerprintDialog/Application/src/main/Android.mk index 1f694d87..1b020d98 100644 --- a/security/FingerprintDialog/Application/src/main/Android.mk +++ b/security/FingerprintDialog/Application/src/main/Android.mk @@ -14,7 +14,8 @@ LOCAL_PATH:= $(call my-dir) include $(CLEAR_VARS) -LOCAL_STATIC_JAVA_LIBRARIES := android-support-v7-appcompat +LOCAL_STATIC_JAVA_LIBRARIES := android-support-v7-appcompat \ + android-support-annotations LOCAL_MODULE_TAGS := samples LOCAL_SRC_FILES := $(call all-subdir-java-files) LOCAL_PACKAGE_NAME := FingerprintDialog diff --git a/security/FingerprintDialog/Application/src/main/java/com/example/android/fingerprintdialog/FingerprintAuthenticationDialogFragment.java b/security/FingerprintDialog/Application/src/main/java/com/example/android/fingerprintdialog/FingerprintAuthenticationDialogFragment.java index cb240dba..05375733 100644 --- a/security/FingerprintDialog/Application/src/main/java/com/example/android/fingerprintdialog/FingerprintAuthenticationDialogFragment.java +++ b/security/FingerprintDialog/Application/src/main/java/com/example/android/fingerprintdialog/FingerprintAuthenticationDialogFragment.java @@ -181,12 +181,12 @@ public class FingerprintAuthenticationDialogFragment extends DialogFragment if (mUseFingerprintFutureCheckBox.isChecked()) { // Re-create the key so that fingerprints including new ones are validated. - mActivity.createKey(); + mActivity.createKey(MainActivity.DEFAULT_KEY_NAME, true); mStage = Stage.FINGERPRINT; } } mPassword.setText(""); - mActivity.onPurchased(false /* without Fingerprint */); + mActivity.onPurchased(false /* without Fingerprint */, null); dismiss(); } @@ -243,7 +243,7 @@ public class FingerprintAuthenticationDialogFragment extends DialogFragment public void onAuthenticated() { // Callback from FingerprintUiHelper. Let the activity know that authentication was // successful. - mActivity.onPurchased(true /* withFingerprint */); + mActivity.onPurchased(true /* withFingerprint */, mCryptoObject); dismiss(); } diff --git a/security/FingerprintDialog/Application/src/main/java/com/example/android/fingerprintdialog/MainActivity.java b/security/FingerprintDialog/Application/src/main/java/com/example/android/fingerprintdialog/MainActivity.java index 77c87886..400b2d69 100644 --- a/security/FingerprintDialog/Application/src/main/java/com/example/android/fingerprintdialog/MainActivity.java +++ b/security/FingerprintDialog/Application/src/main/java/com/example/android/fingerprintdialog/MainActivity.java @@ -21,11 +21,13 @@ import android.app.KeyguardManager; import android.content.Intent; import android.content.SharedPreferences; import android.hardware.fingerprint.FingerprintManager; +import android.os.Build; import android.os.Bundle; import android.preference.PreferenceManager; import android.security.keystore.KeyGenParameterSpec; import android.security.keystore.KeyPermanentlyInvalidatedException; import android.security.keystore.KeyProperties; +import android.support.annotation.Nullable; import android.util.Base64; import android.util.Log; import android.view.Menu; @@ -61,12 +63,11 @@ public class MainActivity extends Activity { private static final String DIALOG_FRAGMENT_TAG = "myFragment"; private static final String SECRET_MESSAGE = "Very secret message"; - /** Alias for our key in the Android Key Store */ - private static final String KEY_NAME = "my_key"; + private static final String KEY_NAME_NOT_INVALIDATED = "key_not_invalidated"; + static final String DEFAULT_KEY_NAME = "default_key"; private KeyStore mKeyStore; private KeyGenerator mKeyGenerator; - private Cipher mCipher; private SharedPreferences mSharedPreferences; @Override @@ -85,8 +86,13 @@ public class MainActivity extends Activity { } catch (NoSuchAlgorithmException | NoSuchProviderException e) { throw new RuntimeException("Failed to get an instance of KeyGenerator", e); } + Cipher defaultCipher; + Cipher cipherNotInvalidated; try { - mCipher = Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/" + defaultCipher = Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/" + + KeyProperties.BLOCK_MODE_CBC + "/" + + KeyProperties.ENCRYPTION_PADDING_PKCS7); + cipherNotInvalidated = Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/" + KeyProperties.BLOCK_MODE_CBC + "/" + KeyProperties.ENCRYPTION_PADDING_PKCS7); } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { @@ -97,6 +103,22 @@ public class MainActivity extends Activity { KeyguardManager keyguardManager = getSystemService(KeyguardManager.class); FingerprintManager fingerprintManager = getSystemService(FingerprintManager.class); Button purchaseButton = (Button) findViewById(R.id.purchase_button); + Button purchaseButtonNotInvalidated = (Button) findViewById( + R.id.purchase_button_not_invalidated); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + purchaseButtonNotInvalidated.setEnabled(true); + purchaseButtonNotInvalidated.setOnClickListener( + new PurchaseButtonClickListener(cipherNotInvalidated, + KEY_NAME_NOT_INVALIDATED)); + } else { + // Hide the purchase button which uses a non-invalidated key + // if the app doesn't work on Android N preview + purchaseButtonNotInvalidated.setVisibility(View.GONE); + findViewById(R.id.purchase_button_not_invalidated_description) + .setVisibility(View.GONE); + } + if (!keyguardManager.isKeyguardSecure()) { // Show a message that the user hasn't set up a fingerprint or lock screen. Toast.makeText(this, @@ -104,6 +126,7 @@ public class MainActivity extends Activity { + "Go to 'Settings -> Security -> Fingerprint' to set up a fingerprint", Toast.LENGTH_LONG).show(); purchaseButton.setEnabled(false); + purchaseButtonNotInvalidated.setEnabled(false); return; } @@ -119,62 +142,27 @@ public class MainActivity extends Activity { Toast.LENGTH_LONG).show(); return; } - createKey(); + createKey(DEFAULT_KEY_NAME, true); + createKey(KEY_NAME_NOT_INVALIDATED, false); purchaseButton.setEnabled(true); - purchaseButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - findViewById(R.id.confirmation_message).setVisibility(View.GONE); - findViewById(R.id.encrypted_message).setVisibility(View.GONE); - - // Set up the crypto object for later. The object will be authenticated by use - // of the fingerprint. - if (initCipher()) { - // Show the fingerprint dialog. The user has the option to use the fingerprint - // with crypto, or you can fall back to using a server-side verified password. - FingerprintAuthenticationDialogFragment fragment - = new FingerprintAuthenticationDialogFragment(); - fragment.setCryptoObject(new FingerprintManager.CryptoObject(mCipher)); - boolean useFingerprintPreference = mSharedPreferences - .getBoolean(getString(R.string.use_fingerprint_to_authenticate_key), - true); - if (useFingerprintPreference) { - fragment.setStage( - FingerprintAuthenticationDialogFragment.Stage.FINGERPRINT); - } else { - fragment.setStage( - FingerprintAuthenticationDialogFragment.Stage.PASSWORD); - } - fragment.show(getFragmentManager(), DIALOG_FRAGMENT_TAG); - } else { - // This happens if the lock screen has been disabled or or a fingerprint got - // enrolled. Thus show the dialog to authenticate with their password first - // and ask the user if they want to authenticate with fingerprints in the - // future - FingerprintAuthenticationDialogFragment fragment - = new FingerprintAuthenticationDialogFragment(); - fragment.setCryptoObject(new FingerprintManager.CryptoObject(mCipher)); - fragment.setStage( - FingerprintAuthenticationDialogFragment.Stage.NEW_FINGERPRINT_ENROLLED); - fragment.show(getFragmentManager(), DIALOG_FRAGMENT_TAG); - } - } - }); + purchaseButton.setOnClickListener( + new PurchaseButtonClickListener(defaultCipher, DEFAULT_KEY_NAME)); } /** - * Initialize the {@link Cipher} instance with the created key in the {@link #createKey()} - * method. + * Initialize the {@link Cipher} instance with the created key in the + * {@link #createKey(String, boolean)} method. * + * @param keyName the key name to init the cipher * @return {@code true} if initialization is successful, {@code false} if the lock screen has * been disabled or reset after the key was generated, or if a fingerprint got enrolled after * the key was generated. */ - private boolean initCipher() { + private boolean initCipher(Cipher cipher, String keyName) { try { mKeyStore.load(null); - SecretKey key = (SecretKey) mKeyStore.getKey(KEY_NAME, null); - mCipher.init(Cipher.ENCRYPT_MODE, key); + SecretKey key = (SecretKey) mKeyStore.getKey(keyName, null); + cipher.init(Cipher.ENCRYPT_MODE, key); return true; } catch (KeyPermanentlyInvalidatedException e) { return false; @@ -184,11 +172,19 @@ public class MainActivity extends Activity { } } - public void onPurchased(boolean withFingerprint) { + /** + * Proceed the purchase operation + * + * @param withFingerprint {@code true} if the purchase was made by using a fingerprint + * @param cryptoObject the Crypto object + */ + public void onPurchased(boolean withFingerprint, + @Nullable FingerprintManager.CryptoObject cryptoObject) { if (withFingerprint) { // If the user has authenticated with fingerprint, verify that using cryptography and // then show the confirmation message. - tryEncrypt(); + assert cryptoObject != null; + tryEncrypt(cryptoObject.getCipher()); } else { // Authentication happened with backup password. Just show the confirmation message. showConfirmation(null); @@ -209,9 +205,9 @@ public class MainActivity extends Activity { * Tries to encrypt some data with the generated key in {@link #createKey} which is * only works if the user has just authenticated via fingerprint. */ - private void tryEncrypt() { + private void tryEncrypt(Cipher cipher) { try { - byte[] encrypted = mCipher.doFinal(SECRET_MESSAGE.getBytes()); + byte[] encrypted = cipher.doFinal(SECRET_MESSAGE.getBytes()); showConfirmation(encrypted); } catch (BadPaddingException | IllegalBlockSizeException e) { Toast.makeText(this, "Failed to encrypt the data with the generated key. " @@ -223,8 +219,18 @@ public class MainActivity extends Activity { /** * Creates a symmetric key in the Android Key Store which can only be used after the user has * authenticated with fingerprint. + * + * @param keyName the name of the key to be created + * @param invalidatedByBiometricEnrollment if {@code false} is passed, the created key will not + * be invalidated even if a new fingerprint is enrolled. + * The default value is {@code true}, so passing + * {@code true} doesn't change the behavior + * (the key will be invalidated if a new fingerprint is + * enrolled.). Note that this parameter is only valid if + * the app works on Android N developer preview. + * */ - public void createKey() { + public void createKey(String keyName, boolean invalidatedByBiometricEnrollment) { // The enrolling flow for fingerprint. This is where you ask the user to set up fingerprint // for your flow. Use of keys is necessary if you need to know if the set of // enrolled fingerprints has changed. @@ -232,15 +238,25 @@ public class MainActivity extends Activity { mKeyStore.load(null); // Set the alias of the entry in Android KeyStore where the key will appear // and the constrains (purposes) in the constructor of the Builder - mKeyGenerator.init(new KeyGenParameterSpec.Builder(KEY_NAME, + + KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec.Builder(keyName, KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) .setBlockModes(KeyProperties.BLOCK_MODE_CBC) // Require the user to authenticate with a fingerprint to authorize every use // of the key .setUserAuthenticationRequired(true) - .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7) - .build()); + .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7); + + // This is a workaround to avoid crashes on devices whose API level is < 24 + // because KeyGenParameterSpec.Builder#setInvalidatedByBiometricEnrollment is only + // visible on API level +24. + // Ideally there should be a compat library for KeyGenParameterSpec.Builder but + // which isn't available yet. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + builder.setInvalidatedByBiometricEnrollment(invalidatedByBiometricEnrollment); + } + mKeyGenerator.init(builder.build()); mKeyGenerator.generateKey(); } catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException | CertificateException | IOException e) { @@ -265,4 +281,54 @@ public class MainActivity extends Activity { } return super.onOptionsItemSelected(item); } + + private class PurchaseButtonClickListener implements View.OnClickListener { + + Cipher mCipher; + String mKeyName; + + PurchaseButtonClickListener(Cipher cipher, String keyName) { + mCipher = cipher; + mKeyName = keyName; + } + + @Override + public void onClick(View view) { + findViewById(R.id.confirmation_message).setVisibility(View.GONE); + findViewById(R.id.encrypted_message).setVisibility(View.GONE); + + // Set up the crypto object for later. The object will be authenticated by use + // of the fingerprint. + if (initCipher(mCipher, mKeyName)) { + + // Show the fingerprint dialog. The user has the option to use the fingerprint with + // crypto, or you can fall back to using a server-side verified password. + FingerprintAuthenticationDialogFragment fragment + = new FingerprintAuthenticationDialogFragment(); + fragment.setCryptoObject(new FingerprintManager.CryptoObject(mCipher)); + boolean useFingerprintPreference = mSharedPreferences + .getBoolean(getString(R.string.use_fingerprint_to_authenticate_key), + true); + if (useFingerprintPreference) { + fragment.setStage( + FingerprintAuthenticationDialogFragment.Stage.FINGERPRINT); + } else { + fragment.setStage( + FingerprintAuthenticationDialogFragment.Stage.PASSWORD); + } + fragment.show(getFragmentManager(), DIALOG_FRAGMENT_TAG); + } else { + // This happens if the lock screen has been disabled or or a fingerprint got + // enrolled. Thus show the dialog to authenticate with their password first + // and ask the user if they want to authenticate with fingerprints in the + // future + FingerprintAuthenticationDialogFragment fragment + = new FingerprintAuthenticationDialogFragment(); + fragment.setCryptoObject(new FingerprintManager.CryptoObject(mCipher)); + fragment.setStage( + FingerprintAuthenticationDialogFragment.Stage.NEW_FINGERPRINT_ENROLLED); + fragment.show(getFragmentManager(), DIALOG_FRAGMENT_TAG); + } + } + } } diff --git a/security/FingerprintDialog/Application/src/main/res/layout/activity_main.xml b/security/FingerprintDialog/Application/src/main/res/layout/activity_main.xml index 8f30557b..075899f8 100644 --- a/security/FingerprintDialog/Application/src/main/res/layout/activity_main.xml +++ b/security/FingerprintDialog/Application/src/main/res/layout/activity_main.xml @@ -77,8 +77,27 @@ android:layout_gravity="end" android:textColor="?android:attr/textColorPrimaryInverse" android:text="@string/purchase" - android:id="@+id/purchase_button" - android:layout_alignParentEnd="true"/> + android:id="@+id/purchase_button" /> + + <Button style="@android:style/Widget.Material.Button.Colored" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + android:layout_marginEnd="4dp" + android:layout_gravity="end" + android:textColor="?android:attr/textColorPrimaryInverse" + android:text="@string/purchase_not_invalidated" + android:id="@+id/purchase_button_not_invalidated" /> + + <TextView + android:id="@+id/purchase_button_not_invalidated_description" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginEnd="4dp" + android:gravity="end" + android:textAlignment="gravity" + android:text="@string/purchase_button_not_invalidated_description" + /> <TextView android:id="@+id/confirmation_message" diff --git a/security/FingerprintDialog/Application/src/main/res/values/strings.xml b/security/FingerprintDialog/Application/src/main/res/values/strings.xml index 77d1176e..86f200c9 100644 --- a/security/FingerprintDialog/Application/src/main/res/values/strings.xml +++ b/security/FingerprintDialog/Application/src/main/res/values/strings.xml @@ -31,6 +31,10 @@ <string name="fingerprint_hint">Touch sensor</string> <string name="password_description">Enter your store password to continue</string> <string name="purchase">Purchase</string> + <string name="purchase_not_invalidated">Purchase not invalidated</string> + <string name="purchase_button_not_invalidated_description"> + You can proceed to purchase with this button \n even if a new fingerprint is enrolled + </string> <string name="fingerprint_not_recognized">Fingerprint not recognized. Try again</string> <string name="fingerprint_success">Fingerprint recognized</string> <string name="item_title">White Mesh Pluto Backpack</string> diff --git a/security/FingerprintDialog/template-params.xml b/security/FingerprintDialog/template-params.xml index c13a82f6..78184922 100644 --- a/security/FingerprintDialog/template-params.xml +++ b/security/FingerprintDialog/template-params.xml @@ -25,6 +25,8 @@ <package>com.example.android.fingerprintdialog</package> <minSdk>23</minSdk> + <targetSdkVersion>24</targetSdkVersion> + <compileSdkVersion>24</compileSdkVersion> <strings> <intro> |