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.
This commit is contained in:
Alexander Bakker 2019-10-16 22:16:47 +02:00
parent a93ced6e34
commit 3be9aecb88
39 changed files with 499 additions and 716 deletions

View file

@ -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();
}
}
}