From 3be9aecb88d30e195c09b37e4746538e6a7652d0 Mon Sep 17 00:00:00 2001 From: Alexander Bakker Date: Wed, 16 Oct 2019 22:16:47 +0200 Subject: [PATCH] Replace the custom fingerprint auth UI with BiometricPrompt This patch replaces the usage of the deprecated FingerprintManager API with BiometricPrompt. This uses the Android X library, so we get the native biometric prompt on recent versions of Android and a Google-made one on older versions. By not working with custom prompts for biometric authentication like we do now, we can be sure that any issues like #70, #81, #237 are not actually our fault. Here's what it looks like: ![](https://alexbakker.me/u/b2rmf3x0b9.jpeg) As a nice aside, this also adds support for the new facial recognition as an authentication method on Pixel 4 phones. This is still a draft, but early feedback is welcome. --- .github/ISSUE_TEMPLATE/bug.md | 2 +- README.md | 3 +- app/build.gradle | 2 +- app/src/main/AndroidManifest.xml | 2 +- ...ingerprintSlot.java => BiometricSlot.java} | 8 +- .../beemdevelopment/aegis/db/slots/Slot.java | 6 +- .../helpers/BiometricSlotInitializer.java | 136 +++++++++++++++ .../aegis/helpers/BiometricsHelper.java | 30 ++++ .../aegis/helpers/FingerprintHelper.java | 34 ---- .../aegis/helpers/FingerprintUiHelper.java | 156 ------------------ .../aegis/helpers/UiThreadExecutor.java | 17 ++ .../aegis/ui/AuthActivity.java | 116 +++++++------ .../com/beemdevelopment/aegis/ui/Dialogs.java | 63 +------ .../aegis/ui/IntroActivity.java | 20 +-- .../aegis/ui/PreferencesFragment.java | 69 ++++---- .../aegis/ui/SlotManagerActivity.java | 103 ++++++++---- .../ui/slides/CustomAuthenticatedSlide.java | 155 ++++++----------- .../ui/slides/CustomAuthenticationSlide.java | 20 +-- .../aegis/ui/tasks/SlotListTask.java | 8 +- .../aegis/ui/views/SlotHolder.java | 14 +- app/src/main/res/layout/activity_auth.xml | 51 +++--- app/src/main/res/layout/activity_slots.xml | 4 +- .../main/res/layout/dialog_fingerprint.xml | 21 --- .../layout/fragment_authenticated_slide.xml | 39 ----- .../layout/fragment_authentication_slide.xml | 8 +- app/src/main/res/raw/notices.xml | 6 - app/src/main/res/values-de-rDE/strings.xml | 12 -- app/src/main/res/values-fr-rFR/strings.xml | 12 -- app/src/main/res/values-nl-rNL/strings.xml | 12 -- app/src/main/res/values-ru-rRU/strings.xml | 12 -- app/src/main/res/values-zh-rCN/strings.xml | 11 -- app/src/main/res/values/arrays.xml | 2 +- app/src/main/res/values/colors.xml | 5 - app/src/main/res/values/strings.xml | 26 ++- app/src/main/res/values/styles.xml | 8 - app/src/main/res/xml/preferences.xml | 6 +- docs/diagram.svg | 2 +- docs/vault.md | 10 +- metadata/en-US/full_description.txt | 4 +- 39 files changed, 499 insertions(+), 716 deletions(-) rename app/src/main/java/com/beemdevelopment/aegis/db/slots/{FingerprintSlot.java => BiometricSlot.java} (57%) create mode 100644 app/src/main/java/com/beemdevelopment/aegis/helpers/BiometricSlotInitializer.java create mode 100644 app/src/main/java/com/beemdevelopment/aegis/helpers/BiometricsHelper.java delete mode 100644 app/src/main/java/com/beemdevelopment/aegis/helpers/FingerprintHelper.java delete mode 100644 app/src/main/java/com/beemdevelopment/aegis/helpers/FingerprintUiHelper.java create mode 100644 app/src/main/java/com/beemdevelopment/aegis/helpers/UiThreadExecutor.java delete mode 100644 app/src/main/res/layout/dialog_fingerprint.xml diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md index faea5710..59583ce1 100644 --- a/.github/ISSUE_TEMPLATE/bug.md +++ b/.github/ISSUE_TEMPLATE/bug.md @@ -11,7 +11,7 @@ guidelines](/CONTRIBUTING.md#bug-reports) before submitting an issue. --> * __Version__: * __Source__: (Google Play/GitHub/F-Droid/?) -* __Vault encrypted:__ Yes (with fingerprint unlock)/Yes/No +* __Vault encrypted:__ Yes (with biometric unlock)/Yes/No * __Device:__ * __Android version and ROM:__ diff --git a/README.md b/README.md index 7bbe749b..4372f2e9 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ document](docs/vault.md). - Secure - Encryption (AES-256) - Password (scrypt) - - Fingerprint (Android Keystore) + - Biometrics (Android Keystore) - Screen capture prevention - Tap to reveal ability - Multiple ways to add new entries @@ -120,7 +120,6 @@ Certificate fingerprints: - [AppIntro](https://github.com/AppIntro/AppIntro) by Paolo Rotolo - [Krop](https://github.com/avito-tech/krop) by Avito Technology - [SpongyCastle](https://github.com/rtyley/spongycastle) by Roberto Tyley -- [Swirl](https://github.com/mattprecious/swirl) by Matthew Precious - [CircleImageView](https://github.com/hdodenhof/CircleImageView) by Henning Dodenhof - [barcodescanner](https://github.com/dm77/barcodescanner) by Dushyanth diff --git a/app/build.gradle b/app/build.gradle index af11b092..8170ab64 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -67,6 +67,7 @@ dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1' implementation 'androidx.appcompat:appcompat:1.1.0' + implementation "androidx.biometric:biometric:1.0.0" implementation 'androidx.cardview:cardview:1.0.0' implementation 'androidx.preference:preference:1.1.0' implementation 'com.google.android.material:material:1.0.0' @@ -75,7 +76,6 @@ dependencies { implementation 'com.github.apl-devs:appintro:5.1.0' implementation 'com.github.avito-tech:krop:0.44' implementation 'com.madgag.spongycastle:core:1.58.0.0' - implementation 'com.mattprecious.swirl:swirl:1.2.0' implementation 'de.hdodenhof:circleimageview:3.0.1' implementation 'me.dm7.barcodescanner:zxing:1.9.8' implementation "com.github.topjohnwu.libsu:core:${libsuVersion}" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index cdfd0848..099e395a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -4,7 +4,7 @@ package="com.beemdevelopment.aegis"> - + diff --git a/app/src/main/java/com/beemdevelopment/aegis/db/slots/FingerprintSlot.java b/app/src/main/java/com/beemdevelopment/aegis/db/slots/BiometricSlot.java similarity index 57% rename from app/src/main/java/com/beemdevelopment/aegis/db/slots/FingerprintSlot.java rename to app/src/main/java/com/beemdevelopment/aegis/db/slots/BiometricSlot.java index a846b262..79330de9 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/db/slots/FingerprintSlot.java +++ b/app/src/main/java/com/beemdevelopment/aegis/db/slots/BiometricSlot.java @@ -4,17 +4,17 @@ import com.beemdevelopment.aegis.crypto.CryptParameters; import java.util.UUID; -public class FingerprintSlot extends RawSlot { - public FingerprintSlot() { +public class BiometricSlot extends RawSlot { + public BiometricSlot() { super(); } - FingerprintSlot(UUID uuid, byte[] key, CryptParameters keyParams) { + BiometricSlot(UUID uuid, byte[] key, CryptParameters keyParams) { super(uuid, key, keyParams); } @Override public byte getType() { - return TYPE_FINGERPRINT; + return TYPE_BIOMETRIC; } } diff --git a/app/src/main/java/com/beemdevelopment/aegis/db/slots/Slot.java b/app/src/main/java/com/beemdevelopment/aegis/db/slots/Slot.java index 73781657..f2066604 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/db/slots/Slot.java +++ b/app/src/main/java/com/beemdevelopment/aegis/db/slots/Slot.java @@ -28,7 +28,7 @@ import javax.crypto.spec.SecretKeySpec; public abstract class Slot extends UUIDMap.Value { public final static byte TYPE_RAW = 0x00; public final static byte TYPE_DERIVED = 0x01; - public final static byte TYPE_FINGERPRINT = 0x02; + public final static byte TYPE_BIOMETRIC = 0x02; private byte[] _encryptedMasterKey; private CryptParameters _encryptedMasterKeyParams; @@ -138,8 +138,8 @@ public abstract class Slot extends UUIDMap.Value { boolean repaired = obj.optBoolean("repaired", false); slot = new PasswordSlot(uuid, key, keyParams, scryptParams, repaired); break; - case Slot.TYPE_FINGERPRINT: - slot = new FingerprintSlot(uuid, key, keyParams); + case Slot.TYPE_BIOMETRIC: + slot = new BiometricSlot(uuid, key, keyParams); break; default: throw new SlotException("unrecognized slot type"); diff --git a/app/src/main/java/com/beemdevelopment/aegis/helpers/BiometricSlotInitializer.java b/app/src/main/java/com/beemdevelopment/aegis/helpers/BiometricSlotInitializer.java new file mode 100644 index 00000000..7d634146 --- /dev/null +++ b/app/src/main/java/com/beemdevelopment/aegis/helpers/BiometricSlotInitializer.java @@ -0,0 +1,136 @@ +package com.beemdevelopment.aegis.helpers; + +import androidx.annotation.NonNull; +import androidx.biometric.BiometricPrompt; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentActivity; + +import com.beemdevelopment.aegis.crypto.KeyStoreHandle; +import com.beemdevelopment.aegis.crypto.KeyStoreHandleException; +import com.beemdevelopment.aegis.db.slots.BiometricSlot; +import com.beemdevelopment.aegis.db.slots.Slot; +import com.beemdevelopment.aegis.db.slots.SlotException; + +import java.util.Objects; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; + +/** + * A class that can prepare initialization of a BiometricSlot by generating a new + * key in the Android KeyStore and authenticating a cipher for it through a + * BiometricPrompt. + */ +public class BiometricSlotInitializer extends BiometricPrompt.AuthenticationCallback { + private BiometricSlot _slot; + private Listener _listener; + private BiometricPrompt _prompt; + + public BiometricSlotInitializer(Fragment fragment, Listener listener) { + _listener = listener; + _prompt = new BiometricPrompt(fragment, new UiThreadExecutor(), this); + } + + public BiometricSlotInitializer(FragmentActivity activity, Listener listener) { + _listener = listener; + _prompt = new BiometricPrompt(activity, new UiThreadExecutor(), this); + } + + /** + * Generates a new key in the Android KeyStore for the new BiometricSlot, + * initializes a cipher with it and shows a BiometricPrompt to the user for + * authentication. If authentication is successful, the new slot will be + * initialized and delivered back through the listener. + */ + public void authenticate(BiometricPrompt.PromptInfo info) { + if (_slot != null) { + throw new IllegalStateException("Biometric authentication already in progress"); + } + + KeyStoreHandle keyStore; + try { + keyStore = new KeyStoreHandle(); + } catch (KeyStoreHandleException e) { + fail(e); + return; + } + + // generate a new Android KeyStore key + // and assign it the UUID of the new slot as an alias + Cipher cipher; + BiometricSlot slot = new BiometricSlot(); + try { + SecretKey key = keyStore.generateKey(slot.getUUID().toString()); + cipher = Slot.createEncryptCipher(key); + } catch (KeyStoreHandleException | SlotException e) { + fail(e); + return; + } + + _slot = slot; + _prompt.authenticate(info, new BiometricPrompt.CryptoObject(cipher)); + } + + /** + * Cancels the BiometricPrompt and resets the state of the initializer. It will + * also attempt to delete the previously generated Android KeyStore key. + */ + public void cancelAuthentication() { + if (_slot == null) { + throw new IllegalStateException("Biometric authentication not in progress"); + } + + reset(); + _prompt.cancelAuthentication(); + } + + private void reset() { + if (_slot != null) { + try { + // clean up the unused KeyStore key + // this is non-critical, so just fail silently if an error occurs + String uuid = _slot.getUUID().toString(); + KeyStoreHandle keyStore = new KeyStoreHandle(); + if (keyStore.containsKey(uuid)) { + keyStore.deleteKey(uuid); + } + } catch (KeyStoreHandleException e) { + e.printStackTrace(); + } + + _slot = null; + } + } + + private void fail(int errorCode, CharSequence errString) { + reset(); + _listener.onSlotInitializationFailed(errorCode, errString); + } + + private void fail(Exception e) { + e.printStackTrace(); + fail(0, e.toString()); + } + + @Override + public void onAuthenticationError(int errorCode, @NonNull CharSequence errString) { + super.onAuthenticationError(errorCode, errString); + fail(errorCode, errString.toString()); + } + + @Override + public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) { + super.onAuthenticationSucceeded(result); + _listener.onInitializeSlot(_slot, Objects.requireNonNull(result.getCryptoObject()).getCipher()); + } + + @Override + public void onAuthenticationFailed() { + super.onAuthenticationFailed(); + } + + public interface Listener { + void onInitializeSlot(BiometricSlot slot, Cipher cipher); + void onSlotInitializationFailed(int errorCode, @NonNull CharSequence errString); + } +} diff --git a/app/src/main/java/com/beemdevelopment/aegis/helpers/BiometricsHelper.java b/app/src/main/java/com/beemdevelopment/aegis/helpers/BiometricsHelper.java new file mode 100644 index 00000000..eab2e5e4 --- /dev/null +++ b/app/src/main/java/com/beemdevelopment/aegis/helpers/BiometricsHelper.java @@ -0,0 +1,30 @@ +package com.beemdevelopment.aegis.helpers; + +import android.content.Context; + +import androidx.biometric.BiometricConstants; +import androidx.biometric.BiometricManager; + +public class BiometricsHelper { + private BiometricsHelper() { + + } + + public static BiometricManager getManager(Context context) { + BiometricManager manager = BiometricManager.from(context); + if (manager.canAuthenticate() == BiometricManager.BIOMETRIC_SUCCESS) { + return manager; + } + return null; + } + + public static boolean isCanceled(int errorCode) { + return errorCode == BiometricConstants.ERROR_CANCELED + || errorCode == BiometricConstants.ERROR_USER_CANCELED + || errorCode == BiometricConstants.ERROR_NEGATIVE_BUTTON; + } + + public static boolean isAvailable(Context context) { + return getManager(context) != null; + } +} diff --git a/app/src/main/java/com/beemdevelopment/aegis/helpers/FingerprintHelper.java b/app/src/main/java/com/beemdevelopment/aegis/helpers/FingerprintHelper.java deleted file mode 100644 index 0f377935..00000000 --- a/app/src/main/java/com/beemdevelopment/aegis/helpers/FingerprintHelper.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.beemdevelopment.aegis.helpers; - -import android.Manifest; -import android.content.Context; -import android.hardware.fingerprint.FingerprintManager; -import android.os.Build; - -import androidx.annotation.RequiresApi; - -public class FingerprintHelper { - private FingerprintHelper() { - - } - - @RequiresApi(api = Build.VERSION_CODES.M) - public static FingerprintManager getManager(Context context) { - if (PermissionHelper.granted(context, Manifest.permission.USE_FINGERPRINT)) { - FingerprintManager manager = (FingerprintManager) context.getSystemService(Context.FINGERPRINT_SERVICE); - if (manager != null && manager.isHardwareDetected() && manager.hasEnrolledFingerprints()) { - return manager; - } - } - return null; - } - - @RequiresApi(api = Build.VERSION_CODES.M) - public static boolean isAvailable(Context context) { - return getManager(context) != null; - } - - public static boolean isSupported() { - return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M; - } -} diff --git a/app/src/main/java/com/beemdevelopment/aegis/helpers/FingerprintUiHelper.java b/app/src/main/java/com/beemdevelopment/aegis/helpers/FingerprintUiHelper.java deleted file mode 100644 index 69a65c58..00000000 --- a/app/src/main/java/com/beemdevelopment/aegis/helpers/FingerprintUiHelper.java +++ /dev/null @@ -1,156 +0,0 @@ -// This file was originally taken from https://github.com/googlesamples/android-FingerprintDialog/blob/2feb02945ae220ebd1bc2c2b620a1d43e30daea8/Application/src/main/java/com/example/android/fingerprintdialog/FingerprintUiHelper.java -// It has been modified to suit Aegis' needs - -/* - * 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.beemdevelopment.aegis.helpers; - -import android.hardware.fingerprint.FingerprintManager; -import android.os.Build; -import android.os.CancellationSignal; -import android.widget.TextView; - -import com.beemdevelopment.aegis.R; -import com.mattprecious.swirl.SwirlView; - -import androidx.annotation.RequiresApi; - -/** - * Small helper class to manage text/icon around fingerprint authentication UI. - */ -@RequiresApi(api = Build.VERSION_CODES.M) -public class FingerprintUiHelper extends FingerprintManager.AuthenticationCallback { - - private static final long ERROR_TIMEOUT_MILLIS = 1600; - private static final long SUCCESS_DELAY_MILLIS = 100; - - private final FingerprintManager mFingerprintManager; - private final SwirlView mIcon; - private final TextView mErrorTextView; - private final Callback mCallback; - private CancellationSignal mCancellationSignal; - - private boolean mSelfCancelled; - - /** - * Constructor for {@link FingerprintUiHelper}. - */ - public FingerprintUiHelper(FingerprintManager fingerprintManager, - SwirlView icon, TextView errorTextView, Callback callback) { - mFingerprintManager = fingerprintManager; - mIcon = icon; - mErrorTextView = errorTextView; - mCallback = callback; - } - - public boolean isFingerprintAuthAvailable() { - // The line below prevents the false positive inspection from Android Studio - // noinspection ResourceType - return mFingerprintManager.isHardwareDetected() - && mFingerprintManager.hasEnrolledFingerprints(); - } - - public void startListening(FingerprintManager.CryptoObject cryptoObject) { - if (!isFingerprintAuthAvailable()) { - return; - } - mCancellationSignal = new CancellationSignal(); - mSelfCancelled = false; - // The line below prevents the false positive inspection from Android Studio - // noinspection ResourceType - mFingerprintManager - .authenticate(cryptoObject, mCancellationSignal, 0 /* flags */, this, null); - mIcon.setState(SwirlView.State.ON); - } - - public void stopListening() { - if (mCancellationSignal != null) { - mSelfCancelled = true; - mCancellationSignal.cancel(); - mCancellationSignal = null; - } - } - - @Override - public void onAuthenticationError(int errMsgId, CharSequence errString) { - if (!mSelfCancelled) { - showError(errString); - mIcon.postDelayed(new Runnable() { - @Override - public void run() { - mCallback.onError(); - } - }, ERROR_TIMEOUT_MILLIS); - } - } - - @Override - public void onAuthenticationHelp(int helpMsgId, CharSequence helpString) { - showError(helpString); - } - - @Override - public void onAuthenticationFailed() { - showError(mIcon.getResources().getString( - R.string.fingerprint_not_recognized)); - } - - @Override - public void onAuthenticationSucceeded(FingerprintManager.AuthenticationResult result) { - mErrorTextView.removeCallbacks(mResetErrorTextRunnable); - mIcon.setState(SwirlView.State.OFF); - mErrorTextView.setText( - mErrorTextView.getResources().getString(R.string.fingerprint_success)); - mIcon.postDelayed(new Runnable() { - @Override - public void run() { - mCallback.onAuthenticated(); - } - }, SUCCESS_DELAY_MILLIS); - - // ugly hack to keep the fingerprint icon visible while also giving visual feedback of success to the user - mIcon.postDelayed(new Runnable() { - @Override - public void run() { - mIcon.setState(SwirlView.State.ON); - } - }, 500); - } - - private void showError(CharSequence error) { - mIcon.setState(SwirlView.State.ERROR); - mErrorTextView.setText(error); - mErrorTextView.removeCallbacks(mResetErrorTextRunnable); - mErrorTextView.postDelayed(mResetErrorTextRunnable, ERROR_TIMEOUT_MILLIS); - } - - private Runnable mResetErrorTextRunnable = new Runnable() { - @Override - public void run() { - mErrorTextView.setText( - mErrorTextView.getResources().getString(R.string.fingerprint_hint)); - mIcon.setState(SwirlView.State.ON); - } - }; - - public interface Callback { - - void onAuthenticated(); - - void onError(); - } -} diff --git a/app/src/main/java/com/beemdevelopment/aegis/helpers/UiThreadExecutor.java b/app/src/main/java/com/beemdevelopment/aegis/helpers/UiThreadExecutor.java new file mode 100644 index 00000000..4cb0aa5b --- /dev/null +++ b/app/src/main/java/com/beemdevelopment/aegis/helpers/UiThreadExecutor.java @@ -0,0 +1,17 @@ +package com.beemdevelopment.aegis.helpers; + +import android.os.Handler; +import android.os.Looper; + +import androidx.annotation.NonNull; + +import java.util.concurrent.Executor; + +public class UiThreadExecutor implements Executor { + private final Handler _handler = new Handler(Looper.getMainLooper()); + + @Override + public void execute(@NonNull Runnable command) { + _handler.post(command); + } +} diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/AuthActivity.java b/app/src/main/java/com/beemdevelopment/aegis/ui/AuthActivity.java index d5e57b8f..e3034a13 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/ui/AuthActivity.java +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/AuthActivity.java @@ -1,61 +1,56 @@ package com.beemdevelopment.aegis.ui; -import android.annotation.SuppressLint; import android.content.Context; import android.content.Intent; -import android.hardware.fingerprint.FingerprintManager; -import android.os.Build; import android.os.Bundle; import android.view.KeyEvent; import android.view.View; -import android.view.ViewGroup; import android.view.WindowManager; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodManager; import android.widget.Button; import android.widget.EditText; import android.widget.LinearLayout; -import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; +import androidx.biometric.BiometricPrompt; -import com.beemdevelopment.aegis.R; import com.beemdevelopment.aegis.CancelAction; +import com.beemdevelopment.aegis.R; import com.beemdevelopment.aegis.crypto.KeyStoreHandle; import com.beemdevelopment.aegis.crypto.KeyStoreHandleException; import com.beemdevelopment.aegis.db.DatabaseFileCredentials; -import com.beemdevelopment.aegis.db.slots.FingerprintSlot; +import com.beemdevelopment.aegis.db.slots.BiometricSlot; import com.beemdevelopment.aegis.db.slots.PasswordSlot; import com.beemdevelopment.aegis.db.slots.Slot; import com.beemdevelopment.aegis.db.slots.SlotException; import com.beemdevelopment.aegis.db.slots.SlotList; +import com.beemdevelopment.aegis.helpers.BiometricsHelper; import com.beemdevelopment.aegis.helpers.EditTextHelper; -import com.beemdevelopment.aegis.helpers.FingerprintHelper; -import com.beemdevelopment.aegis.helpers.FingerprintUiHelper; +import com.beemdevelopment.aegis.helpers.UiThreadExecutor; import com.beemdevelopment.aegis.ui.tasks.SlotListTask; -import com.mattprecious.swirl.SwirlView; import javax.crypto.Cipher; import javax.crypto.SecretKey; -import androidx.appcompat.app.AlertDialog; - -public class AuthActivity extends AegisActivity implements FingerprintUiHelper.Callback, SlotListTask.Callback { +public class AuthActivity extends AegisActivity implements SlotListTask.Callback { private EditText _textPassword; private CancelAction _cancelAction; private SlotList _slots; - private FingerprintUiHelper _fingerHelper; - private FingerprintManager.CryptoObject _fingerCryptoObj; + private BiometricPrompt.CryptoObject _bioCryptoObj; + private BiometricPrompt _bioPrompt; @Override - @SuppressLint("NewApi") protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_auth); _textPassword = findViewById(R.id.text_password); - LinearLayout boxFingerprint = findViewById(R.id.box_fingerprint); - LinearLayout boxFingerprintInfo = findViewById(R.id.box_fingerprint_info); - TextView textFingerprint = findViewById(R.id.text_fingerprint); + LinearLayout boxBiometricInfo = findViewById(R.id.box_biometric_info); Button decryptButton = findViewById(R.id.button_decrypt); + Button biometricsButton = findViewById(R.id.button_biometrics); _textPassword.setOnEditorActionListener((v, actionId, event) -> { if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER)) || (actionId == EditorInfo.IME_ACTION_DONE)) { @@ -64,25 +59,17 @@ public class AuthActivity extends AegisActivity implements FingerprintUiHelper.C return false; }); - SwirlView imgFingerprint = null; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - ViewGroup insertPoint = findViewById(R.id.img_fingerprint_insert); - imgFingerprint = new SwirlView(this); - insertPoint.addView(imgFingerprint, 0, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); - } - Intent intent = getIntent(); _slots = (SlotList) intent.getSerializableExtra("slots"); _cancelAction = (CancelAction) intent.getSerializableExtra("cancelAction"); - // only show the fingerprint controls if the api version is new enough, permission is granted, a scanner is found and a fingerprint slot is found - if (_slots.has(FingerprintSlot.class) && FingerprintHelper.isSupported() && FingerprintHelper.isAvailable(this)) { + // only show the biometric prompt if the api version is new enough, permission is granted, a scanner is found and a biometric slot is found + if (_slots.has(BiometricSlot.class) && BiometricsHelper.isAvailable(this)) { boolean invalidated = false; - FingerprintManager manager = FingerprintHelper.getManager(this); try { - // find a fingerprint slot with an id that matches an alias in the keystore - for (FingerprintSlot slot : _slots.findAll(FingerprintSlot.class)) { + // find a biometric slot with an id that matches an alias in the keystore + for (BiometricSlot slot : _slots.findAll(BiometricSlot.class)) { String id = slot.getUUID().toString(); KeyStoreHandle handle = new KeyStoreHandle(); if (handle.containsKey(id)) { @@ -92,10 +79,11 @@ public class AuthActivity extends AegisActivity implements FingerprintUiHelper.C invalidated = true; continue; } + Cipher cipher = slot.createDecryptCipher(key); - _fingerCryptoObj = new FingerprintManager.CryptoObject(cipher); - _fingerHelper = new FingerprintUiHelper(manager, imgFingerprint, textFingerprint, this); - boxFingerprint.setVisibility(View.VISIBLE); + _bioCryptoObj = new BiometricPrompt.CryptoObject(cipher); + _bioPrompt = new BiometricPrompt(this, new UiThreadExecutor(), new BiometricPromptListener()); + biometricsButton.setVisibility(View.VISIBLE); invalidated = false; break; } @@ -106,7 +94,7 @@ public class AuthActivity extends AegisActivity implements FingerprintUiHelper.C // display a help message if a matching invalidated keystore entry was found if (invalidated) { - boxFingerprintInfo.setVisibility(View.VISIBLE); + boxBiometricInfo.setVisibility(View.VISIBLE); } } @@ -121,7 +109,11 @@ public class AuthActivity extends AegisActivity implements FingerprintUiHelper.C } }); - if (_fingerHelper == null) { + biometricsButton.setOnClickListener(v -> { + showBiometricPrompt(); + }); + + if (_bioCryptoObj == null) { _textPassword.requestFocus(); getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE); } @@ -144,7 +136,7 @@ public class AuthActivity extends AegisActivity implements FingerprintUiHelper.C private void selectPassword() { _textPassword.selectAll(); - InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0); } @@ -162,36 +154,31 @@ public class AuthActivity extends AegisActivity implements FingerprintUiHelper.C } @Override - @SuppressLint("NewApi") public void onResume() { super.onResume(); - if (_fingerHelper != null) { - _fingerHelper.startListening(_fingerCryptoObj); + if (_bioPrompt != null) { + showBiometricPrompt(); } } + public void showBiometricPrompt() { + BiometricPrompt.PromptInfo info = new BiometricPrompt.PromptInfo.Builder() + .setTitle(getString(R.string.authentication)) + .setNegativeButtonText(getString(android.R.string.cancel)) + .build(); + _bioPrompt.authenticate(info, _bioCryptoObj); + } + @Override - @SuppressLint("NewApi") public void onPause() { super.onPause(); - if (_fingerHelper != null) { - _fingerHelper.stopListening(); + if (_bioPrompt != null) { + _bioPrompt.cancelAuthentication(); } } - @Override - @SuppressLint("NewApi") - public void onAuthenticated() { - trySlots(FingerprintSlot.class, _fingerCryptoObj.getCipher()); - } - - @Override - public void onError() { - - } - @Override public void onTaskFinished(SlotListTask.Result result) { if (result != null) { @@ -210,4 +197,25 @@ public class AuthActivity extends AegisActivity implements FingerprintUiHelper.C showError(); } } + + private class BiometricPromptListener extends BiometricPrompt.AuthenticationCallback { + @Override + public void onAuthenticationError(int errorCode, @NonNull CharSequence errString) { + super.onAuthenticationError(errorCode, errString); + if (!BiometricsHelper.isCanceled(errorCode)) { + Toast.makeText(AuthActivity.this, errString, Toast.LENGTH_LONG).show(); + } + } + + @Override + public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) { + super.onAuthenticationSucceeded(result); + trySlots(BiometricSlot.class, _bioCryptoObj.getCipher()); + } + + @Override + public void onAuthenticationFailed() { + super.onAuthenticationFailed(); + } + } } diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/Dialogs.java b/app/src/main/java/com/beemdevelopment/aegis/ui/Dialogs.java index bf47dfa2..514d7404 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/ui/Dialogs.java +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/Dialogs.java @@ -4,8 +4,6 @@ import android.app.Activity; import android.app.Dialog; import android.content.Context; import android.content.DialogInterface; -import android.hardware.fingerprint.FingerprintManager; -import android.os.Build; import android.text.Editable; import android.text.InputType; import android.text.TextWatcher; @@ -18,30 +16,21 @@ import android.widget.CheckBox; import android.widget.CompoundButton; import android.widget.EditText; import android.widget.NumberPicker; -import android.widget.TextView; + +import androidx.annotation.StringRes; +import androidx.appcompat.app.AlertDialog; import com.beemdevelopment.aegis.Preferences; import com.beemdevelopment.aegis.R; -import com.beemdevelopment.aegis.crypto.KeyStoreHandle; -import com.beemdevelopment.aegis.crypto.KeyStoreHandleException; -import com.beemdevelopment.aegis.db.slots.FingerprintSlot; import com.beemdevelopment.aegis.db.slots.PasswordSlot; import com.beemdevelopment.aegis.db.slots.Slot; import com.beemdevelopment.aegis.db.slots.SlotException; import com.beemdevelopment.aegis.helpers.EditTextHelper; -import com.beemdevelopment.aegis.helpers.FingerprintHelper; -import com.beemdevelopment.aegis.helpers.FingerprintUiHelper; import com.beemdevelopment.aegis.ui.tasks.DerivationTask; -import com.mattprecious.swirl.SwirlView; import java.util.concurrent.atomic.AtomicReference; import javax.crypto.Cipher; -import javax.crypto.SecretKey; - -import androidx.annotation.RequiresApi; -import androidx.annotation.StringRes; -import androidx.appcompat.app.AlertDialog; public class Dialogs { private Dialogs() { @@ -195,52 +184,6 @@ public class Dialogs { showSecureDialog(dialog); } - @RequiresApi(api = Build.VERSION_CODES.M) - public static void showFingerprintDialog(Activity activity, Dialogs.SlotListener listener) { - View view = activity.getLayoutInflater().inflate(R.layout.dialog_fingerprint, null); - TextView textFingerprint = view.findViewById(R.id.text_fingerprint); - SwirlView imgFingerprint = view.findViewById(R.id.img_fingerprint); - - FingerprintManager.CryptoObject obj; - FingerprintSlot slot; - final AtomicReference helper = new AtomicReference<>(); - FingerprintManager manager = FingerprintHelper.getManager(activity); - - try { - slot = new FingerprintSlot(); - SecretKey key = new KeyStoreHandle().generateKey(slot.getUUID().toString()); - Cipher cipher = Slot.createEncryptCipher(key); - obj = new FingerprintManager.CryptoObject(cipher); - } catch (KeyStoreHandleException | SlotException e) { - throw new RuntimeException(e); - } - - AlertDialog dialog = new AlertDialog.Builder(activity) - .setTitle(R.string.register_fingerprint) - .setView(view) - .setNegativeButton(android.R.string.cancel, null) - .setOnDismissListener(d -> { - helper.get().stopListening(); - }) - .create(); - - helper.set(new FingerprintUiHelper(manager, imgFingerprint, textFingerprint, new FingerprintUiHelper.Callback() { - @Override - public void onAuthenticated() { - listener.onSlotResult(slot, obj.getCipher()); - dialog.dismiss(); - } - - @Override - public void onError() { - - } - })); - - helper.get().startListening(obj); - showSecureDialog(dialog); - } - public interface NumberInputListener { void onNumberInputResult(int number); } diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/IntroActivity.java b/app/src/main/java/com/beemdevelopment/aegis/ui/IntroActivity.java index 96d228a4..059b26d6 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/ui/IntroActivity.java +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/IntroActivity.java @@ -4,6 +4,8 @@ import android.content.Intent; import android.os.Bundle; import android.view.WindowManager; +import androidx.fragment.app.Fragment; + import com.beemdevelopment.aegis.Preferences; import com.beemdevelopment.aegis.R; import com.beemdevelopment.aegis.db.Database; @@ -12,11 +14,10 @@ import com.beemdevelopment.aegis.db.DatabaseFileCredentials; import com.beemdevelopment.aegis.db.DatabaseFileException; import com.beemdevelopment.aegis.db.DatabaseManager; import com.beemdevelopment.aegis.db.DatabaseManagerException; -import com.beemdevelopment.aegis.db.slots.FingerprintSlot; +import com.beemdevelopment.aegis.db.slots.BiometricSlot; import com.beemdevelopment.aegis.db.slots.PasswordSlot; import com.beemdevelopment.aegis.db.slots.Slot; import com.beemdevelopment.aegis.db.slots.SlotException; -import com.beemdevelopment.aegis.db.slots.SlotList; import com.beemdevelopment.aegis.ui.slides.CustomAuthenticatedSlide; import com.beemdevelopment.aegis.ui.slides.CustomAuthenticationSlide; import com.beemdevelopment.aegis.ui.tasks.DerivationTask; @@ -29,8 +30,6 @@ import org.json.JSONObject; import javax.crypto.Cipher; import javax.crypto.SecretKey; -import androidx.fragment.app.Fragment; - public class IntroActivity extends AppIntro2 implements DerivationTask.Callback { public static final int RESULT_OK = 0; public static final int RESULT_EXCEPTION = 1; @@ -135,7 +134,6 @@ public class IntroActivity extends AppIntro2 implements DerivationTask.Callback creds = new DatabaseFileCredentials(); } - SlotList slots = null; if (cryptType != CustomAuthenticationSlide.CRYPT_TYPE_NONE) { // encrypt the master key with a key derived from the user's password // and add it to the list of slots @@ -150,18 +148,14 @@ public class IntroActivity extends AppIntro2 implements DerivationTask.Callback } } - if (cryptType == CustomAuthenticationSlide.CRYPT_TYPE_FINGER) { + if (cryptType == CustomAuthenticationSlide.CRYPT_TYPE_BIOMETRIC) { + BiometricSlot slot = _authenticatedSlide.getBiometricSlot(); try { - // encrypt the master key with the fingerprint key - // and add it to the list of slots - FingerprintSlot slot = _authenticatedSlide.getFingerSlot(); - Cipher cipher = _authenticatedSlide.getFingerCipher(); - slot.setKey(creds.getKey(), cipher); - creds.getSlots().add(slot); + slot.setKey(creds.getKey(), _authenticatedSlide.getBiometriCipher()); } catch (SlotException e) { setException(e); - return; } + creds.getSlots().add(slot); } // finally, save the database diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/PreferencesFragment.java b/app/src/main/java/com/beemdevelopment/aegis/ui/PreferencesFragment.java index 74efd5c0..eb249ec6 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/ui/PreferencesFragment.java +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/PreferencesFragment.java @@ -12,6 +12,12 @@ import android.view.Window; import android.view.WindowManager; import android.widget.Toast; +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; +import androidx.biometric.BiometricPrompt; +import androidx.preference.Preference; +import androidx.preference.PreferenceFragmentCompat; + import com.beemdevelopment.aegis.AegisApplication; import com.beemdevelopment.aegis.BuildConfig; import com.beemdevelopment.aegis.CancelAction; @@ -25,12 +31,13 @@ import com.beemdevelopment.aegis.db.DatabaseFileCredentials; import com.beemdevelopment.aegis.db.DatabaseFileException; import com.beemdevelopment.aegis.db.DatabaseManager; import com.beemdevelopment.aegis.db.DatabaseManagerException; -import com.beemdevelopment.aegis.db.slots.FingerprintSlot; +import com.beemdevelopment.aegis.db.slots.BiometricSlot; import com.beemdevelopment.aegis.db.slots.PasswordSlot; import com.beemdevelopment.aegis.db.slots.Slot; import com.beemdevelopment.aegis.db.slots.SlotException; import com.beemdevelopment.aegis.db.slots.SlotList; -import com.beemdevelopment.aegis.helpers.FingerprintHelper; +import com.beemdevelopment.aegis.helpers.BiometricSlotInitializer; +import com.beemdevelopment.aegis.helpers.BiometricsHelper; import com.beemdevelopment.aegis.helpers.PermissionHelper; import com.beemdevelopment.aegis.importers.AegisImporter; import com.beemdevelopment.aegis.importers.DatabaseImporter; @@ -56,10 +63,6 @@ import java.util.concurrent.atomic.AtomicReference; import javax.crypto.Cipher; -import androidx.appcompat.app.AlertDialog; -import androidx.preference.Preference; -import androidx.preference.PreferenceFragmentCompat; - public class PreferencesFragment extends PreferenceFragmentCompat { // activity request codes private static final int CODE_IMPORT = 0; @@ -81,7 +84,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat { private UUIDMap _importerEntries; private SwitchPreference _encryptionPreference; - private SwitchPreference _fingerprintPreference; + private SwitchPreference _biometricsPreference; private Preference _autoLockPreference; private Preference _setPasswordPreference; private Preference _slotsPreference; @@ -289,20 +292,25 @@ public class PreferencesFragment extends PreferenceFragmentCompat { } }); - _fingerprintPreference = (SwitchPreference) findPreference("pref_fingerprint"); - _fingerprintPreference.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { + _biometricsPreference = (SwitchPreference) findPreference("pref_biometrics"); + _biometricsPreference.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { @Override public boolean onPreferenceChange(Preference preference, Object newValue) { DatabaseFileCredentials creds = _db.getCredentials(); SlotList slots = creds.getSlots(); - if (!slots.has(FingerprintSlot.class)) { - if (FingerprintHelper.isSupported() && FingerprintHelper.isAvailable(getContext())) { - Dialogs.showFingerprintDialog(getActivity(), new RegisterFingerprintListener()); + if (!slots.has(BiometricSlot.class)) { + if (BiometricsHelper.isAvailable(getContext())) { + BiometricSlotInitializer initializer = new BiometricSlotInitializer(PreferencesFragment.this, new RegisterBiometricsListener()); + BiometricPrompt.PromptInfo info = new BiometricPrompt.PromptInfo.Builder() + .setTitle(getString(R.string.set_up_biometric)) + .setNegativeButtonText(getString(android.R.string.cancel)) + .build(); + initializer.authenticate(info); } } else { - // remove the fingerprint slot - FingerprintSlot slot = slots.find(FingerprintSlot.class); + // remove the biometric slot + BiometricSlot slot = slots.find(BiometricSlot.class); slots.remove(slot); _db.setCredentials(creds); @@ -676,24 +684,24 @@ public class PreferencesFragment extends PreferenceFragmentCompat { boolean encrypted = _db.isEncryptionEnabled(); _encryptionPreference.setChecked(encrypted, true); _setPasswordPreference.setVisible(encrypted); - _fingerprintPreference.setVisible(encrypted); + _biometricsPreference.setVisible(encrypted); _slotsPreference.setEnabled(encrypted); _autoLockPreference.setVisible(encrypted); if (encrypted) { SlotList slots = _db.getCredentials().getSlots(); boolean multiPassword = slots.findAll(PasswordSlot.class).size() > 1; - boolean multiFinger = slots.findAll(FingerprintSlot.class).size() > 1; - boolean showSlots = BuildConfig.DEBUG || multiPassword || multiFinger; - boolean canUseFinger = FingerprintHelper.isSupported() && FingerprintHelper.isAvailable(getContext()); + boolean multiBio = slots.findAll(BiometricSlot.class).size() > 1; + boolean showSlots = BuildConfig.DEBUG || multiPassword || multiBio; + boolean canUseBio = BiometricsHelper.isAvailable(getContext()); _setPasswordPreference.setEnabled(!multiPassword); - _fingerprintPreference.setEnabled(canUseFinger && !multiFinger); - _fingerprintPreference.setChecked(slots.has(FingerprintSlot.class), true); + _biometricsPreference.setEnabled(canUseBio && !multiBio); + _biometricsPreference.setChecked(slots.has(BiometricSlot.class), true); _slotsPreference.setVisible(showSlots); } else { _setPasswordPreference.setEnabled(false); - _fingerprintPreference.setEnabled(false); - _fingerprintPreference.setChecked(false, true); + _biometricsPreference.setEnabled(false); + _biometricsPreference.setChecked(false, true); _slotsPreference.setVisible(false); } } @@ -730,20 +738,17 @@ public class PreferencesFragment extends PreferenceFragmentCompat { } } - private class RegisterFingerprintListener implements Dialogs.SlotListener { + private class RegisterBiometricsListener implements BiometricSlotInitializer.Listener { @Override - public void onSlotResult(Slot slot, Cipher cipher) { + public void onInitializeSlot(BiometricSlot slot, Cipher cipher) { DatabaseFileCredentials creds = _db.getCredentials(); - SlotList slots = creds.getSlots(); - try { slot.setKey(creds.getKey(), cipher); } catch (SlotException e) { - onException(e); + onSlotInitializationFailed(0, e.toString()); return; } - - slots.add(slot); + creds.getSlots().add(slot); _db.setCredentials(creds); saveDatabase(); @@ -751,8 +756,10 @@ public class PreferencesFragment extends PreferenceFragmentCompat { } @Override - public void onException(Exception e) { - Toast.makeText(getActivity(), getString(R.string.encryption_enable_fingerprint_error) + e.getMessage(), Toast.LENGTH_SHORT).show(); + public void onSlotInitializationFailed(int errorCode, @NonNull CharSequence errString) { + if (!BiometricsHelper.isCanceled(errorCode)) { + Toast.makeText(getActivity(), String.format("%s: %s", getString(R.string.encryption_enable_biometrics_error), errString), Toast.LENGTH_LONG).show(); + } } } diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/SlotManagerActivity.java b/app/src/main/java/com/beemdevelopment/aegis/ui/SlotManagerActivity.java index 31367f76..7b9cd1b6 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/ui/SlotManagerActivity.java +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/SlotManagerActivity.java @@ -7,26 +7,29 @@ import android.view.MenuItem; import android.view.View; import android.widget.Toast; +import androidx.annotation.NonNull; +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.app.AlertDialog; +import androidx.biometric.BiometricPrompt; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + import com.beemdevelopment.aegis.R; import com.beemdevelopment.aegis.crypto.KeyStoreHandle; import com.beemdevelopment.aegis.crypto.KeyStoreHandleException; import com.beemdevelopment.aegis.db.DatabaseFileCredentials; -import com.beemdevelopment.aegis.db.slots.FingerprintSlot; +import com.beemdevelopment.aegis.db.slots.BiometricSlot; import com.beemdevelopment.aegis.db.slots.PasswordSlot; import com.beemdevelopment.aegis.db.slots.Slot; import com.beemdevelopment.aegis.db.slots.SlotException; import com.beemdevelopment.aegis.db.slots.SlotList; -import com.beemdevelopment.aegis.helpers.FingerprintHelper; +import com.beemdevelopment.aegis.helpers.BiometricSlotInitializer; +import com.beemdevelopment.aegis.helpers.BiometricsHelper; import com.beemdevelopment.aegis.ui.views.SlotAdapter; import javax.crypto.Cipher; -import androidx.appcompat.app.ActionBar; -import androidx.appcompat.app.AlertDialog; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; - -public class SlotManagerActivity extends AegisActivity implements SlotAdapter.Listener, Dialogs.SlotListener { +public class SlotManagerActivity extends AegisActivity implements SlotAdapter.Listener { private DatabaseFileCredentials _creds; private SlotAdapter _adapter; @@ -42,14 +45,19 @@ public class SlotManagerActivity extends AegisActivity implements SlotAdapter.Li bar.setHomeAsUpIndicator(R.drawable.ic_close); bar.setDisplayHomeAsUpEnabled(true); - findViewById(R.id.button_add_fingerprint).setOnClickListener(view -> { - if (FingerprintHelper.isSupported() && FingerprintHelper.isAvailable(this)) { - Dialogs.showFingerprintDialog(this ,this); + findViewById(R.id.button_add_biometric).setOnClickListener(view -> { + if (BiometricsHelper.isAvailable(this)) { + BiometricSlotInitializer initializer = new BiometricSlotInitializer(SlotManagerActivity.this, new RegisterBiometricsListener()); + BiometricPrompt.PromptInfo info = new BiometricPrompt.PromptInfo.Builder() + .setTitle(getString(R.string.add_biometric_slot)) + .setNegativeButtonText(getString(android.R.string.cancel)) + .build(); + initializer.authenticate(info); } }); findViewById(R.id.button_add_password).setOnClickListener(view -> { - Dialogs.showSetPasswordDialog(this, this); + Dialogs.showSetPasswordDialog(this, new PasswordListener()); }); // set up the recycler view @@ -66,17 +74,17 @@ public class SlotManagerActivity extends AegisActivity implements SlotAdapter.Li _adapter.addSlot(slot); } - updateFingerprintButton(); + updateBiometricsButton(); } - private void updateFingerprintButton() { - // only show the fingerprint option if we can get an instance of the fingerprint manager + private void updateBiometricsButton() { + // only show the biometrics option if we can get an instance of the biometrics manager // and if none of the slots in the collection has a matching alias in the keystore int visibility = View.VISIBLE; - if (FingerprintHelper.isSupported() && FingerprintHelper.isAvailable(this)) { + if (BiometricsHelper.isAvailable(this)) { try { KeyStoreHandle keyStore = new KeyStoreHandle(); - for (FingerprintSlot slot : _creds.getSlots().findAll(FingerprintSlot.class)) { + for (BiometricSlot slot : _creds.getSlots().findAll(BiometricSlot.class)) { if (keyStore.containsKey(slot.getUUID().toString())) { visibility = View.GONE; break; @@ -88,7 +96,7 @@ public class SlotManagerActivity extends AegisActivity implements SlotAdapter.Li } else { visibility = View.GONE; } - findViewById(R.id.button_add_fingerprint).setVisibility(visibility); + findViewById(R.id.button_add_biometric).setVisibility(visibility); } private void onSave() { @@ -164,29 +172,60 @@ public class SlotManagerActivity extends AegisActivity implements SlotAdapter.Li slots.remove(slot); _adapter.removeSlot(slot); _edited = true; - updateFingerprintButton(); + updateBiometricsButton(); }) .setNegativeButton(android.R.string.no, null) .create()); } - @Override - public void onSlotResult(Slot slot, Cipher cipher) { - try { - slot.setKey(_creds.getKey(), cipher); - } catch (SlotException e) { - onException(e); - return; - } - + private void addSlot(Slot slot) { _creds.getSlots().add(slot); _adapter.addSlot(slot); _edited = true; - updateFingerprintButton(); + updateBiometricsButton(); } - @Override - public void onException(Exception e) { - Toast.makeText(this, getString(R.string.adding_new_slot_error) + e.getMessage(), Toast.LENGTH_SHORT).show(); + private void showSlotError(String error) { + Toast.makeText(SlotManagerActivity.this, getString(R.string.adding_new_slot_error) + error, Toast.LENGTH_SHORT).show(); + } + + private class RegisterBiometricsListener implements BiometricSlotInitializer.Listener { + + @Override + public void onInitializeSlot(BiometricSlot slot, Cipher cipher) { + try { + slot.setKey(_creds.getKey(), cipher); + addSlot(slot); + } catch (SlotException e) { + onSlotInitializationFailed(0, e.toString()); + } + } + + @Override + public void onSlotInitializationFailed(int errorCode, @NonNull CharSequence errString) { + if (!BiometricsHelper.isCanceled(errorCode)) { + showSlotError(errString.toString()); + } + } + } + + private class PasswordListener implements Dialogs.SlotListener { + + @Override + public void onSlotResult(Slot slot, Cipher cipher) { + try { + slot.setKey(_creds.getKey(), cipher); + } catch (SlotException e) { + onException(e); + return; + } + + addSlot(slot); + } + + @Override + public void onException(Exception e) { + showSlotError(e.toString()); + } } } diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/slides/CustomAuthenticatedSlide.java b/app/src/main/java/com/beemdevelopment/aegis/ui/slides/CustomAuthenticatedSlide.java index dbb782f3..ba44058b 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/ui/slides/CustomAuthenticatedSlide.java +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/slides/CustomAuthenticatedSlide.java @@ -1,53 +1,39 @@ package com.beemdevelopment.aegis.ui.slides; -import android.annotation.SuppressLint; -import android.content.Context; import android.content.Intent; -import android.hardware.fingerprint.FingerprintManager; -import android.os.Build; import android.os.Bundle; import android.text.method.PasswordTransformationMethod; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.CheckBox; -import android.widget.CompoundButton; import android.widget.EditText; -import android.widget.LinearLayout; -import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.biometric.BiometricPrompt; +import androidx.fragment.app.Fragment; import com.beemdevelopment.aegis.R; -import com.beemdevelopment.aegis.crypto.KeyStoreHandle; -import com.beemdevelopment.aegis.crypto.KeyStoreHandleException; -import com.beemdevelopment.aegis.db.slots.FingerprintSlot; -import com.beemdevelopment.aegis.db.slots.Slot; +import com.beemdevelopment.aegis.db.slots.BiometricSlot; +import com.beemdevelopment.aegis.helpers.BiometricSlotInitializer; +import com.beemdevelopment.aegis.helpers.BiometricsHelper; import com.beemdevelopment.aegis.helpers.EditTextHelper; -import com.beemdevelopment.aegis.helpers.FingerprintUiHelper; import com.github.paolorotolo.appintro.ISlidePolicy; import com.github.paolorotolo.appintro.ISlideSelectionListener; import com.google.android.material.snackbar.Snackbar; -import com.mattprecious.swirl.SwirlView; import javax.crypto.Cipher; -import javax.crypto.SecretKey; -import androidx.fragment.app.Fragment; - -public class CustomAuthenticatedSlide extends Fragment implements FingerprintUiHelper.Callback, ISlidePolicy, ISlideSelectionListener { +public class CustomAuthenticatedSlide extends Fragment implements ISlidePolicy, ISlideSelectionListener { private int _cryptType; private EditText _textPassword; private EditText _textPasswordConfirm; private CheckBox _checkPasswordVisibility; private int _bgColor; - private LinearLayout _boxFingerprint; - private SwirlView _imgFingerprint; - private TextView _textFingerprint; - private FingerprintUiHelper _fingerHelper; - private KeyStoreHandle _storeHandle; - private FingerprintSlot _fingerSlot; - private FingerprintManager.CryptoObject _fingerCryptoObj; - private boolean _fingerAuthenticated; + private BiometricSlot _bioSlot; + private Cipher _bioCipher; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -55,13 +41,6 @@ public class CustomAuthenticatedSlide extends Fragment implements FingerprintUiH _textPassword = view.findViewById(R.id.text_password); _textPasswordConfirm = view.findViewById(R.id.text_password_confirm); _checkPasswordVisibility = view.findViewById(R.id.check_toggle_visibility); - _boxFingerprint = view.findViewById(R.id.box_fingerprint); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - ViewGroup insertPoint = view.findViewById(R.id.img_fingerprint_insert); - _imgFingerprint = new SwirlView(getContext()); - insertPoint.addView(_imgFingerprint, 0, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); - } _checkPasswordVisibility.setOnCheckedChangeListener((buttonView, isChecked) -> { if (isChecked) { @@ -74,7 +53,6 @@ public class CustomAuthenticatedSlide extends Fragment implements FingerprintUiH } }); - _textFingerprint = view.findViewById(R.id.text_fingerprint); view.findViewById(R.id.main).setBackgroundColor(_bgColor); return view; } @@ -83,82 +61,53 @@ public class CustomAuthenticatedSlide extends Fragment implements FingerprintUiH return _cryptType; } + public BiometricSlot getBiometricSlot() { + return _bioSlot; + } + + public Cipher getBiometriCipher() { + return _bioCipher; + } + public char[] getPassword() { return EditTextHelper.getEditTextChars(_textPassword); } - @SuppressLint("NewApi") - public Cipher getFingerCipher() { - return _fingerCryptoObj.getCipher(); - } - - public FingerprintSlot getFingerSlot() { - return _fingerSlot; - } - public void setBgColor(int color) { _bgColor = color; } + public void showBiometricPrompt() { + BiometricSlotInitializer initializer = new BiometricSlotInitializer(this, new BiometricsListener()); + BiometricPrompt.PromptInfo info = new BiometricPrompt.PromptInfo.Builder() + .setTitle(getString(R.string.set_up_biometric)) + .setNegativeButtonText(getString(android.R.string.cancel)) + .build(); + initializer.authenticate(info); + } + @Override - @SuppressLint("NewApi") public void onSlideSelected() { Intent intent = getActivity().getIntent(); _cryptType = intent.getIntExtra("cryptType", CustomAuthenticationSlide.CRYPT_TYPE_INVALID); - switch(_cryptType) { - case CustomAuthenticationSlide.CRYPT_TYPE_NONE: - case CustomAuthenticationSlide.CRYPT_TYPE_PASS: - break; - case CustomAuthenticationSlide.CRYPT_TYPE_FINGER: - _boxFingerprint.setVisibility(View.VISIBLE); - - SecretKey key; - try { - if (_storeHandle == null) { - _storeHandle = new KeyStoreHandle(); - _fingerSlot = new FingerprintSlot(); - } - key = _storeHandle.generateKey(_fingerSlot.getUUID().toString()); - } catch (KeyStoreHandleException e) { - throw new RuntimeException(e); - } - - if (_fingerHelper == null) { - FingerprintManager fingerManager = (FingerprintManager) getContext().getSystemService(Context.FINGERPRINT_SERVICE); - _fingerHelper = new FingerprintUiHelper(fingerManager, _imgFingerprint, _textFingerprint, this); - } - - try { - Cipher cipher = Slot.createEncryptCipher(key); - _fingerCryptoObj = new FingerprintManager.CryptoObject(cipher); - } catch (Exception e) { - throw new RuntimeException(e); - } - _fingerHelper.startListening(_fingerCryptoObj); - break; - default: - throw new RuntimeException(); + if (_cryptType == CustomAuthenticationSlide.CRYPT_TYPE_BIOMETRIC) { + showBiometricPrompt(); } } @Override - @SuppressLint("NewApi") public void onSlideDeselected() { - if (_fingerHelper != null) { - _fingerAuthenticated = false; - _boxFingerprint.setVisibility(View.INVISIBLE); - _fingerHelper.stopListening(); - } + } @Override public boolean isPolicyRespected() { - switch(_cryptType) { + switch (_cryptType) { case CustomAuthenticationSlide.CRYPT_TYPE_NONE: return true; - case CustomAuthenticationSlide.CRYPT_TYPE_FINGER: - if (!_fingerAuthenticated) { + case CustomAuthenticationSlide.CRYPT_TYPE_BIOMETRIC: + if (_bioSlot == null) { return false; } // intentional fallthrough @@ -169,7 +118,7 @@ public class CustomAuthenticatedSlide extends Fragment implements FingerprintUiH return false; default: - throw new RuntimeException(); + return false; } } @@ -178,26 +127,30 @@ public class CustomAuthenticatedSlide extends Fragment implements FingerprintUiH String message; if (!EditTextHelper.areEditTextsEqual(_textPassword, _textPasswordConfirm) && !_checkPasswordVisibility.isChecked()) { message = getString(R.string.password_equality_error); - } else if (!_fingerAuthenticated) { - message = getString(R.string.register_fingerprint); - } else { - return; - } - View view = getView(); - if (view != null) { - Snackbar snackbar = Snackbar.make(view, message, Snackbar.LENGTH_LONG); - snackbar.show(); + View view = getView(); + if (view != null) { + Snackbar snackbar = Snackbar.make(view, message, Snackbar.LENGTH_LONG); + snackbar.show(); + } + } else if (_bioSlot == null) { + showBiometricPrompt(); } } - @Override - public void onAuthenticated() { - _fingerAuthenticated = true; - } + private class BiometricsListener implements BiometricSlotInitializer.Listener { - @Override - public void onError() { + @Override + public void onInitializeSlot(BiometricSlot slot, Cipher cipher) { + _bioSlot = slot; + _bioCipher = cipher; + } + @Override + public void onSlotInitializationFailed(int errorCode, @NonNull CharSequence errString) { + if (!BiometricsHelper.isCanceled(errorCode)) { + Toast.makeText(CustomAuthenticatedSlide.this.getContext(), errString, Toast.LENGTH_LONG).show(); + } + } } } diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/slides/CustomAuthenticationSlide.java b/app/src/main/java/com/beemdevelopment/aegis/ui/slides/CustomAuthenticationSlide.java index 85d8f0b4..289b5980 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/ui/slides/CustomAuthenticationSlide.java +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/slides/CustomAuthenticationSlide.java @@ -1,8 +1,6 @@ package com.beemdevelopment.aegis.ui.slides; -import android.annotation.SuppressLint; import android.content.Intent; -import android.hardware.fingerprint.FingerprintManager; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; @@ -11,18 +9,18 @@ import android.widget.RadioButton; import android.widget.RadioGroup; import android.widget.TextView; +import androidx.fragment.app.Fragment; + import com.beemdevelopment.aegis.R; -import com.beemdevelopment.aegis.helpers.FingerprintHelper; +import com.beemdevelopment.aegis.helpers.BiometricsHelper; import com.github.paolorotolo.appintro.ISlidePolicy; import com.google.android.material.snackbar.Snackbar; -import androidx.fragment.app.Fragment; - public class CustomAuthenticationSlide extends Fragment implements ISlidePolicy, RadioGroup.OnCheckedChangeListener { public static final int CRYPT_TYPE_INVALID = 0; public static final int CRYPT_TYPE_NONE = 1; public static final int CRYPT_TYPE_PASS = 2; - public static final int CRYPT_TYPE_FINGER = 3; + public static final int CRYPT_TYPE_BIOMETRIC = 3; private RadioGroup _buttonGroup; private int _bgColor; @@ -35,9 +33,9 @@ public class CustomAuthenticationSlide extends Fragment implements ISlidePolicy, onCheckedChanged(_buttonGroup, _buttonGroup.getCheckedRadioButtonId()); // only enable the fingerprint option if the api version is new enough, permission is granted and a scanner is found - if (FingerprintHelper.isSupported() && FingerprintHelper.isAvailable(getContext())) { - RadioButton button = view.findViewById(R.id.rb_fingerprint); - TextView text = view.findViewById(R.id.text_rb_fingerprint); + if (BiometricsHelper.isAvailable(getContext())) { + RadioButton button = view.findViewById(R.id.rb_biometrics); + TextView text = view.findViewById(R.id.text_rb_biometrics); button.setEnabled(true); text.setEnabled(true); } @@ -75,8 +73,8 @@ public class CustomAuthenticationSlide extends Fragment implements ISlidePolicy, case R.id.rb_password: id = CRYPT_TYPE_PASS; break; - case R.id.rb_fingerprint: - id = CRYPT_TYPE_FINGER; + case R.id.rb_biometrics: + id = CRYPT_TYPE_BIOMETRIC; break; default: throw new RuntimeException(); diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/tasks/SlotListTask.java b/app/src/main/java/com/beemdevelopment/aegis/ui/tasks/SlotListTask.java index fad63bd3..5bcf6f81 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/ui/tasks/SlotListTask.java +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/tasks/SlotListTask.java @@ -6,7 +6,7 @@ import android.content.Context; import com.beemdevelopment.aegis.R; import com.beemdevelopment.aegis.crypto.CryptoUtils; import com.beemdevelopment.aegis.crypto.MasterKey; -import com.beemdevelopment.aegis.db.slots.FingerprintSlot; +import com.beemdevelopment.aegis.db.slots.BiometricSlot; import com.beemdevelopment.aegis.db.slots.PasswordSlot; import com.beemdevelopment.aegis.db.slots.Slot; import com.beemdevelopment.aegis.db.slots.SlotException; @@ -38,8 +38,8 @@ public class SlotListTask extends ProgressDialogTask extends ProgressDialogTask -