From f1785a1c0f454c3c754a9a41ae06eac344af8daf Mon Sep 17 00:00:00 2001 From: Yuichi Araki Date: Tue, 15 Sep 2015 01:56:26 +0000 Subject: Revert "FingerprintDialog: Use asymmetric keys" This reverts commit 69a36a78ca293b94452c69d06c638de804815c40. Change-Id: I50741f5f028cb16cab6fbbebe3918ae77ce93abe --- .../FingerprintAuthenticationDialogFragment.java | 83 ++++--------------- .../fingerprintdialog/FingerprintModule.java | 30 +++---- .../android/fingerprintdialog/MainActivity.java | 93 ++++++++++++++-------- .../fingerprintdialog/server/StoreBackend.java | 61 -------------- .../fingerprintdialog/server/StoreBackendImpl.java | 69 ---------------- .../fingerprintdialog/server/Transaction.java | 66 --------------- .../Application/src/main/res/values/strings.xml | 1 - 7 files changed, 87 insertions(+), 316 deletions(-) delete mode 100644 security/FingerprintDialog/Application/src/main/java/com/example/android/fingerprintdialog/server/StoreBackend.java delete mode 100644 security/FingerprintDialog/Application/src/main/java/com/example/android/fingerprintdialog/server/StoreBackendImpl.java delete mode 100644 security/FingerprintDialog/Application/src/main/java/com/example/android/fingerprintdialog/server/Transaction.java (limited to 'security/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 17df7217..b17ebb0b 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 @@ -16,9 +16,6 @@ package com.example.android.fingerprintdialog; -import com.example.android.fingerprintdialog.server.StoreBackend; -import com.example.android.fingerprintdialog.server.Transaction; - import android.app.Activity; import android.app.DialogFragment; import android.content.SharedPreferences; @@ -36,18 +33,6 @@ import android.widget.EditText; import android.widget.ImageView; import android.widget.TextView; -import java.io.IOException; -import java.security.KeyFactory; -import java.security.KeyStore; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.security.PublicKey; -import java.security.Signature; -import java.security.SignatureException; -import java.security.cert.CertificateException; -import java.security.spec.InvalidKeySpecException; -import java.security.spec.X509EncodedKeySpec; - import javax.inject.Inject; /** @@ -75,7 +60,6 @@ public class FingerprintAuthenticationDialogFragment extends DialogFragment @Inject FingerprintUiHelper.FingerprintUiHelperBuilder mFingerprintUiHelperBuilder; @Inject InputMethodManager mInputMethodManager; @Inject SharedPreferences mSharedPreferences; - @Inject StoreBackend mStoreBackend; @Inject public FingerprintAuthenticationDialogFragment() {} @@ -87,9 +71,6 @@ public class FingerprintAuthenticationDialogFragment extends DialogFragment // Do not create a new Fragment when the Activity is re-created such as orientation changes. setRetainInstance(true); setStyle(DialogFragment.STYLE_NORMAL, android.R.style.Theme_Material_Light_Dialog); - - // We register a new user account here. Real apps should do this with proper UIs. - enroll(); } @Override @@ -187,38 +168,11 @@ public class FingerprintAuthenticationDialogFragment extends DialogFragment } /** - * Enrolls a user to the fake backend. - */ - private void enroll() { - try { - KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore"); - keyStore.load(null); - PublicKey publicKey = keyStore.getCertificate(MainActivity.KEY_NAME).getPublicKey(); - // Provide the public key to the backend. In most cases, the key needs to be transmitted - // to the backend over the network, for which Key.getEncoded provides a suitable wire - // format (X.509 DER-encoded). The backend can then create a PublicKey instance from the - // X.509 encoded form using KeyFactory.generatePublic. This conversion is also currently - // needed on API Level 23 (Android M) due to a platform bug which prevents the use of - // Android Keystore public keys when their private keys require user authentication. - // This conversion creates a new public key which is not backed by Android Keystore and - // thus does not is not affected by the bug. - KeyFactory factory = KeyFactory.getInstance(publicKey.getAlgorithm()); - X509EncodedKeySpec spec = new X509EncodedKeySpec(publicKey.getEncoded()); - PublicKey verificationKey = factory.generatePublic(spec); - mStoreBackend.enroll("user", "password", verificationKey); - } catch (KeyStoreException | CertificateException | NoSuchAlgorithmException | - IOException | InvalidKeySpecException e) { - e.printStackTrace(); - } - } - - /** - * Checks whether the current entered password is correct, and dismisses the the dialog and lets - * the activity know about the result. + * Checks whether the current entered password is correct, and dismisses the the dialog and + * let's the activity know about the result. */ private void verifyPassword() { - Transaction transaction = new Transaction("user", 1); - if (!mStoreBackend.verify(transaction, mPassword.getText().toString())) { + if (!checkPassword(mPassword.getText().toString())) { return; } if (mStage == Stage.NEW_FINGERPRINT_ENROLLED) { @@ -229,15 +183,24 @@ public class FingerprintAuthenticationDialogFragment extends DialogFragment if (mUseFingerprintFutureCheckBox.isChecked()) { // Re-create the key so that fingerprints including new ones are validated. - mActivity.createKeyPair(); + mActivity.createKey(); mStage = Stage.FINGERPRINT; } } mPassword.setText(""); - mActivity.onPurchased(null); + mActivity.onPurchased(false /* without Fingerprint */); dismiss(); } + /** + * @return true if {@code password} is correct, false otherwise + */ + private boolean checkPassword(String password) { + // Assume the password is always correct. + // In the real world situation, the password needs to be verified in the server side. + return password.length() > 0; + } + private final Runnable mShowKeyboardRunnable = new Runnable() { @Override public void run() { @@ -282,22 +245,8 @@ public class FingerprintAuthenticationDialogFragment extends DialogFragment public void onAuthenticated() { // Callback from FingerprintUiHelper. Let the activity know that authentication was // successful. - mPassword.setText(""); - Signature signature = mCryptoObject.getSignature(); - Transaction transaction = new Transaction("user", 1); - try { - signature.update(transaction.toByteArray()); - byte[] sigBytes = signature.sign(); - if (mStoreBackend.verify(transaction, sigBytes)) { - mActivity.onPurchased(sigBytes); - dismiss(); - } else { - mActivity.onPurchaseFailed(); - dismiss(); - } - } catch (SignatureException e) { - throw new RuntimeException(e); - } + mActivity.onPurchased(true /* withFingerprint */); + dismiss(); } @Override diff --git a/security/FingerprintDialog/Application/src/main/java/com/example/android/fingerprintdialog/FingerprintModule.java b/security/FingerprintDialog/Application/src/main/java/com/example/android/fingerprintdialog/FingerprintModule.java index ddfe35c3..964e1f6d 100644 --- a/security/FingerprintDialog/Application/src/main/java/com/example/android/fingerprintdialog/FingerprintModule.java +++ b/security/FingerprintDialog/Application/src/main/java/com/example/android/fingerprintdialog/FingerprintModule.java @@ -16,9 +16,6 @@ package com.example.android.fingerprintdialog; -import com.example.android.fingerprintdialog.server.StoreBackend; -import com.example.android.fingerprintdialog.server.StoreBackendImpl; - import android.app.KeyguardManager; import android.content.Context; import android.content.SharedPreferences; @@ -27,12 +24,14 @@ import android.preference.PreferenceManager; import android.security.keystore.KeyProperties; import android.view.inputmethod.InputMethodManager; -import java.security.KeyPairGenerator; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.NoSuchProviderException; -import java.security.Signature; + +import javax.crypto.Cipher; +import javax.crypto.KeyGenerator; +import javax.crypto.NoSuchPaddingException; import dagger.Module; import dagger.Provides; @@ -77,20 +76,22 @@ public class FingerprintModule { } @Provides - public KeyPairGenerator providesKeyPairGenerator() { + public KeyGenerator providesKeyGenerator() { try { - return KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore"); + return KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore"); } catch (NoSuchAlgorithmException | NoSuchProviderException e) { - throw new RuntimeException("Failed to get an instance of KeyPairGenerator", e); + throw new RuntimeException("Failed to get an instance of KeyGenerator", e); } } @Provides - public Signature providesSignature(KeyStore keyStore) { + public Cipher providesCipher(KeyStore keyStore) { try { - return Signature.getInstance("SHA256withECDSA"); - } catch (NoSuchAlgorithmException e) { - throw new RuntimeException("Failed to get an instance of Signature", e); + return Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/" + + KeyProperties.BLOCK_MODE_CBC + "/" + + KeyProperties.ENCRYPTION_PADDING_PKCS7); + } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { + throw new RuntimeException("Failed to get an instance of Cipher", e); } } @@ -103,9 +104,4 @@ public class FingerprintModule { public SharedPreferences providesSharedPreferences(Context context) { return PreferenceManager.getDefaultSharedPreferences(context); } - - @Provides - public StoreBackend providesStoreBackend() { - return new StoreBackendImpl(); - } } 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 caccd823..c954bfa7 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 @@ -28,6 +28,7 @@ import android.security.keystore.KeyGenParameterSpec; import android.security.keystore.KeyPermanentlyInvalidatedException; import android.security.keystore.KeyProperties; import android.util.Base64; +import android.util.Log; import android.view.Menu; import android.view.MenuItem; import android.view.View; @@ -38,16 +39,17 @@ import android.widget.Toast; import java.io.IOException; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; -import java.security.KeyPairGenerator; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; -import java.security.PrivateKey; -import java.security.Signature; import java.security.UnrecoverableKeyException; import java.security.cert.CertificateException; -import java.security.spec.ECGenParameterSpec; +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; import javax.inject.Inject; /** @@ -60,7 +62,7 @@ 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 */ - public static final String KEY_NAME = "my_key"; + private static final String KEY_NAME = "my_key"; private static final int FINGERPRINT_PERMISSION_REQUEST_CODE = 0; @@ -68,8 +70,8 @@ public class MainActivity extends Activity { @Inject FingerprintManager mFingerprintManager; @Inject FingerprintAuthenticationDialogFragment mFragment; @Inject KeyStore mKeyStore; - @Inject KeyPairGenerator mKeyPairGenerator; - @Inject Signature mSignature; + @Inject KeyGenerator mKeyGenerator; + @Inject Cipher mCipher; @Inject SharedPreferences mSharedPreferences; @Override @@ -104,7 +106,7 @@ public class MainActivity extends Activity { Toast.LENGTH_LONG).show(); return; } - createKeyPair(); + createKey(); purchaseButton.setEnabled(true); purchaseButton.setOnClickListener(new View.OnClickListener() { @Override @@ -114,11 +116,11 @@ public class MainActivity extends Activity { // Set up the crypto object for later. The object will be authenticated by use // of the fingerprint. - if (initSignature()) { + 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. - mFragment.setCryptoObject(new FingerprintManager.CryptoObject(mSignature)); + mFragment.setCryptoObject(new FingerprintManager.CryptoObject(mCipher)); boolean useFingerprintPreference = mSharedPreferences .getBoolean(getString(R.string.use_fingerprint_to_authenticate_key), true); @@ -135,6 +137,7 @@ public class MainActivity extends Activity { // 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 + mFragment.setCryptoObject(new FingerprintManager.CryptoObject(mCipher)); mFragment.setStage( FingerprintAuthenticationDialogFragment.Stage.NEW_FINGERPRINT_ENROLLED); mFragment.show(getFragmentManager(), DIALOG_FRAGMENT_TAG); @@ -145,18 +148,18 @@ public class MainActivity extends Activity { } /** - * Initialize the {@link Signature} instance with the created key in the - * {@link #createKeyPair()} method. + * Initialize the {@link Cipher} instance with the created key in the {@link #createKey()} + * method. * * @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 initSignature() { + private boolean initCipher() { try { mKeyStore.load(null); - PrivateKey key = (PrivateKey) mKeyStore.getKey(KEY_NAME, null); - mSignature.initSign(key); + SecretKey key = (SecretKey) mKeyStore.getKey(KEY_NAME, null); + mCipher.init(Cipher.ENCRYPT_MODE, key); return true; } catch (KeyPermanentlyInvalidatedException e) { return false; @@ -166,12 +169,15 @@ public class MainActivity extends Activity { } } - public void onPurchased(byte[] signature) { - showConfirmation(signature); - } - - public void onPurchaseFailed() { - Toast.makeText(this, R.string.purchase_fail, Toast.LENGTH_SHORT).show(); + public void onPurchased(boolean withFingerprint) { + if (withFingerprint) { + // If the user has authenticated with fingerprint, verify that using cryptography and + // then show the confirmation message. + tryEncrypt(); + } else { + // Authentication happened with backup password. Just show the confirmation message. + showConfirmation(null); + } } // Show confirmation, if fingerprint was used show crypto information. @@ -185,27 +191,44 @@ public class MainActivity extends Activity { } /** - * Generates an asymmetric key pair in the Android Keystore. Every use of the private key must - * be authorized by the user authenticating with fingerprint. Public key use is unrestricted. + * 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() { + try { + byte[] encrypted = mCipher.doFinal(SECRET_MESSAGE.getBytes()); + showConfirmation(encrypted); + } catch (BadPaddingException | IllegalBlockSizeException e) { + Toast.makeText(this, "Failed to encrypt the data with the generated key. " + + "Retry the purchase", Toast.LENGTH_LONG).show(); + Log.e(TAG, "Failed to encrypt the data with the generated key." + e.getMessage()); + } + } + + /** + * Creates a symmetric key in the Android Key Store which can only be used after the user has + * authenticated with fingerprint. */ - public void createKeyPair() { + public void createKey() { // 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. try { + 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 - mKeyPairGenerator.initialize( - new KeyGenParameterSpec.Builder(KEY_NAME, - KeyProperties.PURPOSE_SIGN) - .setDigests(KeyProperties.DIGEST_SHA256) - .setAlgorithmParameterSpec(new ECGenParameterSpec("secp256r1")) - // Require the user to authenticate with a fingerprint to authorize - // every use of the private key - .setUserAuthenticationRequired(true) - .build()); - mKeyPairGenerator.generateKeyPair(); - } catch (InvalidAlgorithmParameterException e) { + mKeyGenerator.init(new KeyGenParameterSpec.Builder(KEY_NAME, + 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()); + mKeyGenerator.generateKey(); + } catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException + | CertificateException | IOException e) { throw new RuntimeException(e); } } diff --git a/security/FingerprintDialog/Application/src/main/java/com/example/android/fingerprintdialog/server/StoreBackend.java b/security/FingerprintDialog/Application/src/main/java/com/example/android/fingerprintdialog/server/StoreBackend.java deleted file mode 100644 index 5ecfa9e7..00000000 --- a/security/FingerprintDialog/Application/src/main/java/com/example/android/fingerprintdialog/server/StoreBackend.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.example.android.fingerprintdialog.server; - -import java.security.PublicKey; - -/** - * An interface that defines the methods required for the store backend. - */ -public interface StoreBackend { - - /** - * Verifies the authenticity of the provided transaction by confirming that it was signed with - * the private key enrolled for the userId. - * - * @param transaction the contents of the purchase transaction, its contents are - * signed - * by the - * private key in the client side. - * @param transactionSignature the signature of the transaction's contents. - * @return true if the signedSignature was verified, false otherwise. If this method returns - * true, the server can consider the transaction is successful. - */ - boolean verify(Transaction transaction, byte[] transactionSignature); - - /** - * Verifies the authenticity of the provided transaction by password. - * - * @param transaction the contents of the purchase transaction, its contents are signed by the - * private key in the client side. - * @param password the password for the user associated with the {@code transaction}. - * @return true if the password is verified. - */ - boolean verify(Transaction transaction, String password); - - /** - * Enrolls a public key associated with the userId - * - * @param userId the unique ID of the user within the app including server side - * implementation - * @param password the password for the user for the server side - * @param publicKey the public key object to verify the signature from the user - * @return true if the enrollment was successful, false otherwise - */ - boolean enroll(String userId, String password, PublicKey publicKey); - -} diff --git a/security/FingerprintDialog/Application/src/main/java/com/example/android/fingerprintdialog/server/StoreBackendImpl.java b/security/FingerprintDialog/Application/src/main/java/com/example/android/fingerprintdialog/server/StoreBackendImpl.java deleted file mode 100644 index 986479e0..00000000 --- a/security/FingerprintDialog/Application/src/main/java/com/example/android/fingerprintdialog/server/StoreBackendImpl.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.example.android.fingerprintdialog.server; - - -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; -import java.security.PublicKey; -import java.security.Signature; -import java.security.SignatureException; -import java.util.HashMap; -import java.util.Map; - -/** - * A fake backend implementation of {@link StoreBackend}. - */ -public class StoreBackendImpl implements StoreBackend { - - private final Map mPublicKeys = new HashMap<>(); - - @Override - public boolean verify(Transaction transaction, byte[] transactionSignature) { - try { - PublicKey publicKey = mPublicKeys.get(transaction.getUserId()); - Signature verificationFunction = Signature.getInstance("SHA256withECDSA"); - verificationFunction.initVerify(publicKey); - verificationFunction.update(transaction.toByteArray()); - if (verificationFunction.verify(transactionSignature)) { - // Transaction is verified with the public key associated with the user - // Do some post purchase processing in the server - return true; - } - } catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException e) { - // In a real world, better to send some error message to the user - } - return false; - } - - @Override - public boolean verify(Transaction transaction, String password) { - // As this is just a sample, we always assume that the password is right. - return true; - } - - @Override - public boolean enroll(String userId, String password, PublicKey publicKey) { - if (publicKey != null) { - mPublicKeys.put(userId, publicKey); - } - // We just ignore the provided password here, but in real life, it is registered to the - // backend. - return true; - } - -} diff --git a/security/FingerprintDialog/Application/src/main/java/com/example/android/fingerprintdialog/server/Transaction.java b/security/FingerprintDialog/Application/src/main/java/com/example/android/fingerprintdialog/server/Transaction.java deleted file mode 100644 index 724102ec..00000000 --- a/security/FingerprintDialog/Application/src/main/java/com/example/android/fingerprintdialog/server/Transaction.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.example.android.fingerprintdialog.server; - -import java.io.ByteArrayOutputStream; -import java.io.DataOutputStream; -import java.io.IOException; - -/** - * An entity that represents a single transaction (purchase) of an item. - */ -public class Transaction { - - /** The unique ID of the item of the purchase */ - private final Long mItemId; - - /** The unique user ID who made the transaction */ - private final String mUserId; - - public Transaction(String userId, long itemId) { - mItemId = itemId; - mUserId = userId; - } - - public String getUserId() { - return mUserId; - } - - public byte[] toByteArray() { - ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); - DataOutputStream dataOutputStream = null; - try { - dataOutputStream = new DataOutputStream(byteArrayOutputStream); - dataOutputStream.writeLong(mItemId); - dataOutputStream.writeUTF(mUserId); - return byteArrayOutputStream.toByteArray(); - } catch (IOException e) { - throw new RuntimeException(e); - } finally { - try { - if (dataOutputStream != null) { - dataOutputStream.close(); - } - } catch (IOException ignore) { - } - try { - byteArrayOutputStream.close(); - } catch (IOException ignore) { - } - } - } -} diff --git a/security/FingerprintDialog/Application/src/main/res/values/strings.xml b/security/FingerprintDialog/Application/src/main/res/values/strings.xml index f44c06d6..9f5a6fd1 100644 --- a/security/FingerprintDialog/Application/src/main/res/values/strings.xml +++ b/security/FingerprintDialog/Application/src/main/res/values/strings.xml @@ -31,7 +31,6 @@ $62.68 Mesh backpack in white. Black textile trim throughout. Purchase successful - Purchase failed A new fingerprint was added to this device, so your password is required. Use fingerprint in the future Use fingerprint to authenticate -- cgit v1.2.3