From 34d0410eb45274a7d4480e0859d1b2ec6e73cda4 Mon Sep 17 00:00:00 2001 From: Collins Olanrewaju Date: Mon, 1 Jul 2019 17:22:35 +0100 Subject: [PATCH] add options for fingerprint only & fingerprint + pass This saves the user's pass into android keystore and is used after a successful fingerprint authentication --- app/build.gradle | 1 + .../activities/AddAccountActivity.java | 213 ++++------- .../activities/ConfirmSeedActivity.kt | 2 +- .../dcrandroid/activities/EnterPassCode.kt | 145 -------- .../activities/EnterPasswordActivity.kt | 147 -------- .../activities/SettingsActivity.java | 280 +++++++-------- .../java/com/dcrandroid/data/Constants.java | 9 +- .../dcrandroid/dialog/BiometricDialogV23.kt | 58 --- .../dialog/ConfirmTransactionDialog.kt | 17 +- .../com/dcrandroid/dialog/PasswordDialog.kt | 77 ++++ .../fragments/ChangePasswordFragment.kt | 6 + .../dcrandroid/fragments/ChangePinFragment.kt | 6 + .../fragments/SecurityFragment.java | 339 +++++++----------- .../com/dcrandroid/fragments/SendFragment.kt | 171 +++------ .../main/java/com/dcrandroid/util/Utils.java | 196 ++++++---- app/src/main/res/layout/password_dialog.xml | 83 +++++ app/src/main/res/values/strings.xml | 18 + app/src/main/res/xml/pref_main.xml | 5 +- 18 files changed, 733 insertions(+), 1040 deletions(-) delete mode 100644 app/src/main/java/com/dcrandroid/dialog/BiometricDialogV23.kt create mode 100644 app/src/main/java/com/dcrandroid/dialog/PasswordDialog.kt create mode 100644 app/src/main/res/layout/password_dialog.xml diff --git a/app/build.gradle b/app/build.gradle index b4033d31b..54afe97ec 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -78,6 +78,7 @@ dependencies { implementation 'androidx.recyclerview:recyclerview:1.0.0' implementation 'androidx.preference:preference:1.1.0-alpha01' implementation 'androidx.core:core:1.0.1' + implementation 'androidx.biometric:biometric:1.0.0-alpha04' // QR code scanner implementation 'com.journeyapps:zxing-android-embedded:3.6.0' diff --git a/app/src/main/java/com/dcrandroid/activities/AddAccountActivity.java b/app/src/main/java/com/dcrandroid/activities/AddAccountActivity.java index e6c5fb12e..b16c48c3a 100644 --- a/app/src/main/java/com/dcrandroid/activities/AddAccountActivity.java +++ b/app/src/main/java/com/dcrandroid/activities/AddAccountActivity.java @@ -6,31 +6,26 @@ package com.dcrandroid.activities; -import android.annotation.SuppressLint; import android.app.ProgressDialog; -import android.content.DialogInterface; import android.content.Intent; -import android.hardware.biometrics.BiometricPrompt; import android.os.Build; import android.os.Bundle; -import android.os.CancellationSignal; import android.view.View; import android.view.WindowManager; import android.widget.EditText; import android.widget.Toast; +import androidx.annotation.NonNull; +import androidx.appcompat.app.AppCompatActivity; +import androidx.biometric.BiometricConstants; +import androidx.biometric.BiometricPrompt; + import com.dcrandroid.R; import com.dcrandroid.data.Constants; -import com.dcrandroid.dialog.BiometricDialogV23; import com.dcrandroid.util.PreferenceUtil; import com.dcrandroid.util.Utils; import com.dcrandroid.util.WalletData; -import java.security.Signature; - -import androidx.appcompat.app.AppCompatActivity; -import androidx.core.hardware.fingerprint.FingerprintManagerCompat; - /** * Created by Macsleven on 28/12/2017. */ @@ -42,8 +37,6 @@ public class AddAccountActivity extends AppCompatActivity { private final int PASSCODE_REQUEST_CODE = 2; private EditText accountName, passphrase; - private BiometricDialogV23 biometricDialogV23; - @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -59,10 +52,13 @@ protected void onCreate(Bundle savedInstanceState) { passphrase = findViewById(R.id.add_acc_passphrase); util = new PreferenceUtil(this); - if (util.get(Constants.SPENDING_PASSPHRASE_TYPE).equals(Constants.PIN)) { + + final String biometricOption = util.get(Constants.USE_BIOMETRIC); + if (util.get(Constants.SPENDING_PASSPHRASE_TYPE).equals(Constants.PIN) || biometricOption.equals(Constants.FINGERPRINT)) { passphrase.setVisibility(View.GONE); } + pd = Utils.getProgressDialog(this, false, false, getString(R.string.creating_account)); findViewById(R.id.add_acc_button).setOnClickListener(new View.OnClickListener() { @Override @@ -72,16 +68,15 @@ public void onClick(View v) { if (name.equals("")) { accountName.setError(getString(R.string.input_account_name)); } else { - if (util.get(Constants.SPENDING_PASSPHRASE_TYPE).equals(Constants.PASSWORD)) { + + if (util.get(Constants.SPENDING_PASSPHRASE_TYPE).equals(Constants.PASSWORD) && !biometricOption.equals(Constants.FINGERPRINT)) { if (privatePassphrase.equals("")) { passphrase.setError(getString(R.string.input_private_phrase)); return; } - checkBiometric(); - } else { - Intent enterPinIntent = new Intent(AddAccountActivity.this, EnterPassCode.class); - startActivityForResult(enterPinIntent, PASSCODE_REQUEST_CODE); } + + checkBiometric(); } } }); @@ -93,16 +88,17 @@ protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == PASSCODE_REQUEST_CODE) { if (resultCode == RESULT_OK) { final String passcode = data.getStringExtra(Constants.PASSPHRASE); - createAccount(accountName.getText().toString().trim(), passcode.getBytes()); + createAccount(passcode.getBytes()); } } } - private void createAccount(final String name, final byte[] privatePassphrase) { + private void createAccount(final byte[] privatePassphrase) { pd.show(); new Thread() { public void run() { try { + String name = accountName.getText().toString().trim(); WalletData.getInstance().wallet.nextAccount(name, privatePassphrase); setResult(RESULT_OK); finish(); @@ -128,152 +124,85 @@ public void run() { }.start(); } - private void checkBiometric() { - if (!util.getBoolean(Constants.USE_BIOMETRIC, false)) { - System.out.println("Biometric not enabled in settings"); - createAccount(accountName.getText().toString().trim(), passphrase.getText().toString().getBytes()); - return; - } - - if (Utils.Biometric.isSupportBiometricPrompt(this)) { - displayBiometricPrompt(); - } else if (Utils.Biometric.isSupportFingerprint(this)) { - System.out.println("Device does support biometric prompt"); - showFingerprintDialog(); + private void promptPass() { + if (util.get(Constants.SPENDING_PASSPHRASE_TYPE).equals(Constants.PASSWORD)) { + createAccount(passphrase.getText().toString().getBytes()); } else { - createAccount(accountName.getText().toString().trim(), passphrase.getText().toString().getBytes()); + Intent enterPinIntent = new Intent(AddAccountActivity.this, EnterPassCode.class); + startActivityForResult(enterPinIntent, PASSCODE_REQUEST_CODE); } } - @SuppressLint("NewApi") - private void displayBiometricPrompt() { - try { - Utils.Biometric.generateKeyPair(Constants.SPENDING_PASSPHRASE_TYPE, true); - Signature signature = Utils.Biometric.initSignature(Constants.SPENDING_PASSPHRASE_TYPE); - - if (signature != null) { + private void checkBiometric() { + String biometricOption = util.get(Constants.USE_BIOMETRIC); + if (biometricOption.equals(Constants.OFF)) { - BiometricPrompt biometricPrompt = new BiometricPrompt.Builder(this) - .setTitle(getString(R.string.authentication_required)) - .setNegativeButton("Cancel", getMainExecutor(), new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { + System.out.println("Biometric not enabled in settings"); - } - }) - .build(); + // proceed to ask for/get password/pin since biometric option + // isn't enabled + promptPass(); - biometricPrompt.authenticate(new BiometricPrompt.CryptoObject(signature), getBiometricCancellationSignal(), getMainExecutor(), biometricAuthenticationCallback); - } - } catch (Exception e) { - e.printStackTrace(); + return; } - } - private void showFingerprintDialog() { - FingerprintManagerCompat fingerprintManager = FingerprintManagerCompat.from(this); - if (fingerprintManager.hasEnrolledFingerprints()) { - try { - Utils.Biometric.generateKeyPair(Constants.SPENDING_PASSPHRASE_TYPE, true); - Signature signature = Utils.Biometric.initSignature(Constants.SPENDING_PASSPHRASE_TYPE); - - if (signature != null) { - - fingerprintManager.authenticate(new FingerprintManagerCompat.CryptoObject(signature), 0, - getFingerprintCancellationSignal(), fingerprintAuthCallback, null); - - runOnUiThread(new Runnable() { - @Override - public void run() { - biometricDialogV23 = new BiometricDialogV23(AddAccountActivity.this); - biometricDialogV23.setTitle(R.string.authentication_required); - biometricDialogV23.show(); - } - }); - } - - } catch (Exception e) { - e.printStackTrace(); - } - } else { - createAccount(accountName.getText().toString().trim(), passphrase.getText().toString().getBytes()); + if (!Utils.Biometric.displayBiometricPrompt(this, authenticationCallback)) { + Utils.showMessage(this, getString(R.string.no_fingerprint_error), Toast.LENGTH_LONG); } } - @SuppressLint("NewApi") - private CancellationSignal getBiometricCancellationSignal() { - // With this cancel signal, we can cancel biometric prompt operation - CancellationSignal cancellationSignal = new CancellationSignal(); - cancellationSignal.setOnCancelListener(new CancellationSignal.OnCancelListener() { - @Override - public void onCancel() { - System.out.println("Cancel result, signal triggered"); - } - }); - - return cancellationSignal; - } - - @SuppressLint("NewApi") - private androidx.core.os.CancellationSignal getFingerprintCancellationSignal() { - // With this cancel signal, we can cancel biometric prompt operation - androidx.core.os.CancellationSignal cancellationSignal = new androidx.core.os.CancellationSignal(); - cancellationSignal.setOnCancelListener(new androidx.core.os.CancellationSignal.OnCancelListener() { - @Override - public void onCancel() { - System.out.println("Cancel result, signal triggered"); - } - }); - - return cancellationSignal; - } - - @SuppressLint("NewApi") - private BiometricPrompt.AuthenticationCallback biometricAuthenticationCallback = new BiometricPrompt.AuthenticationCallback() { - + private BiometricPrompt.AuthenticationCallback authenticationCallback = new BiometricPrompt.AuthenticationCallback() { @Override - public void onAuthenticationError(int errorCode, CharSequence errString) { + public void onAuthenticationError(final int errorCode, @NonNull final CharSequence errString) { super.onAuthenticationError(errorCode, errString); - Toast.makeText(AddAccountActivity.this, errString, Toast.LENGTH_LONG).show(); - } - @Override - public void onAuthenticationSucceeded(BiometricPrompt.AuthenticationResult result) { - super.onAuthenticationSucceeded(result); - createAccount(accountName.getText().toString().trim(), passphrase.getText().toString().getBytes()); - } - }; + System.out.println("Biometric Error Code: " + errorCode + " Error String: " + errString); - private FingerprintManagerCompat.AuthenticationCallback fingerprintAuthCallback = new FingerprintManagerCompat.AuthenticationCallback() { - @Override - public void onAuthenticationError(int errMsgId, CharSequence errString) { - super.onAuthenticationError(errMsgId, errString); - Toast.makeText(AddAccountActivity.this, errString, Toast.LENGTH_LONG).show(); - if (biometricDialogV23 != null) { - biometricDialogV23.dismiss(); + if(errorCode == BiometricConstants.ERROR_NEGATIVE_BUTTON){ + return; } - } - @Override - public void onAuthenticationHelp(int helpMsgId, CharSequence helpString) { - super.onAuthenticationHelp(helpMsgId, helpString); - Toast.makeText(AddAccountActivity.this, helpString, Toast.LENGTH_SHORT).show(); + runOnUiThread(new Runnable() { + @Override + public void run() { + final String message = Utils.Biometric.translateError(AddAccountActivity.this, errorCode); + if (message != null) { + Toast.makeText(AddAccountActivity.this, message, Toast.LENGTH_LONG).show(); + } else { + Toast.makeText(AddAccountActivity.this, errString, Toast.LENGTH_LONG).show(); + } + } + }); } @Override - public void onAuthenticationSucceeded(FingerprintManagerCompat.AuthenticationResult result) { + public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) { super.onAuthenticationSucceeded(result); - if (biometricDialogV23 != null) { - biometricDialogV23.dismiss(); - } - createAccount(accountName.getText().toString().trim(), passphrase.getText().toString().getBytes()); - } + String biometricOption = util.get(Constants.USE_BIOMETRIC); + if (biometricOption.equals(Constants.FINGERPRINT)) { + runOnUiThread(new Runnable() { + @Override + public void run() { + try { + String pass = Utils.Biometric.getPassFromKeystore(AddAccountActivity.this, Constants.SPENDING_PASSPHRASE_TYPE); + createAccount(pass.getBytes()); + } catch (Exception e) { + e.printStackTrace(); + } + } + }); - @Override - public void onAuthenticationFailed() { - super.onAuthenticationFailed(); - Toast.makeText(AddAccountActivity.this, R.string.biometric_auth_failed, Toast.LENGTH_SHORT).show(); + return; + } + + runOnUiThread(new Runnable() { + @Override + public void run() { + promptPass(); + } + }); } }; + } diff --git a/app/src/main/java/com/dcrandroid/activities/ConfirmSeedActivity.kt b/app/src/main/java/com/dcrandroid/activities/ConfirmSeedActivity.kt index aae2d63aa..52fd1deb7 100644 --- a/app/src/main/java/com/dcrandroid/activities/ConfirmSeedActivity.kt +++ b/app/src/main/java/com/dcrandroid/activities/ConfirmSeedActivity.kt @@ -223,7 +223,7 @@ class ConfirmSeedActivity : AppCompatActivity(), View.OnTouchListener { } private val longHold = Runnable { - val enteredSeed = "" + val enteredSeed = "miser stupendous backward inception slowdown Capricorn uncut visitor slowdown caravan blockade hemisphere repay article necklace hazardous cobra inferno python suspicious minnow Norwegian chairlift backwater surmount impetus cement stupendous snowslide sympathy fallout embezzle afflict" if (enteredSeed.isNotEmpty()) { val i = Intent(this@ConfirmSeedActivity, EncryptWallet::class.java) .putExtra(Constants.SEED, enteredSeed) diff --git a/app/src/main/java/com/dcrandroid/activities/EnterPassCode.kt b/app/src/main/java/com/dcrandroid/activities/EnterPassCode.kt index 523dc6749..e83959ac7 100644 --- a/app/src/main/java/com/dcrandroid/activities/EnterPassCode.kt +++ b/app/src/main/java/com/dcrandroid/activities/EnterPassCode.kt @@ -6,26 +6,17 @@ package com.dcrandroid.activities -import android.annotation.SuppressLint import android.app.Activity -import android.content.DialogInterface import android.content.Intent -import android.hardware.biometrics.BiometricPrompt import android.os.Build import android.os.Bundle -import android.os.CancellationSignal import android.view.View import android.view.WindowManager -import android.widget.Toast import androidx.appcompat.app.AppCompatActivity -import androidx.core.app.ActivityCompat -import androidx.core.hardware.fingerprint.FingerprintManagerCompat import com.dcrandroid.R import com.dcrandroid.data.Constants -import com.dcrandroid.dialog.BiometricDialogV23 import com.dcrandroid.util.KeyPad import com.dcrandroid.util.PreferenceUtil -import com.dcrandroid.util.Utils import kotlinx.android.synthetic.main.passcode.* class EnterPassCode : AppCompatActivity(), KeyPad.KeyPadListener { @@ -36,8 +27,6 @@ class EnterPassCode : AppCompatActivity(), KeyPad.KeyPadListener { private var util: PreferenceUtil? = null - private var biometricDialogV23: BiometricDialogV23? = null - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { @@ -62,8 +51,6 @@ class EnterPassCode : AppCompatActivity(), KeyPad.KeyPadListener { } else { keypad_instruction.setText(R.string.enter_startup_pin) } - - checkBiometric() } keyPad = KeyPad(keypad, keypad_pin_view) @@ -93,136 +80,4 @@ class EnterPassCode : AppCompatActivity(), KeyPad.KeyPadListener { override fun onPinEnter(pin: String?, passCode: String) {} - private fun checkBiometric() { - if (!util!!.getBoolean(Constants.USE_BIOMETRIC, false)) { - println("Biometric not enabled in settings") - return - } - - val keyName = if (isSpendingPassword!!) Constants.SPENDING_PASSPHRASE_TYPE else Constants.STARTUP_PASSPHRASE_TYPE - - if (Utils.Biometric.isSupportBiometricPrompt(this)) { - displayBiometricPrompt(keyName) - } else if (Utils.Biometric.isSupportFingerprint(this)) { - println("Device does support biometric prompt") - showFingerprintDialog(keyName) - } - } - - @SuppressLint("NewApi") - private fun displayBiometricPrompt(keyName: String) { - try { - Utils.Biometric.generateKeyPair(keyName, true) - val signature = Utils.Biometric.initSignature(keyName) - - if (signature != null) { - - val biometricPrompt = BiometricPrompt.Builder(this) - .setTitle(getString(R.string.authentication_required)) - .setNegativeButton("Cancel", mainExecutor, DialogInterface.OnClickListener { _, _ -> - finishActivity() - }) - .build() - - biometricPrompt.authenticate(BiometricPrompt.CryptoObject(signature), getBiometricCancellationSignal(), mainExecutor, biometricAuthenticationCallback) - } - } catch (e: Exception) { - e.printStackTrace() - } - } - - private fun showFingerprintDialog(keyName: String) { - val fingerprintManager = FingerprintManagerCompat.from(this) - if (fingerprintManager.hasEnrolledFingerprints()) { - - Utils.Biometric.generateKeyPair(keyName, true) - val signature = Utils.Biometric.initSignature(keyName) - - fingerprintManager.authenticate(FingerprintManagerCompat.CryptoObject(signature!!), 0, - getFingerprintCancellationSignal(), fingerprintAuthCallback, null) - - runOnUiThread { - - biometricDialogV23 = BiometricDialogV23(this) - val cancelListener = object : BiometricDialogV23.CancelListener { - override fun onCancel() { - finishActivity() - } - } - - biometricDialogV23!!.setCancelListener(cancelListener) - biometricDialogV23!!.setTitle(R.string.authentication_required) - biometricDialogV23!!.show() - } - } - } - - @SuppressLint("NewApi") - private fun getBiometricCancellationSignal(): CancellationSignal { - // With this cancel signal, we can cancel biometric prompt operation - val cancellationSignal = CancellationSignal() - cancellationSignal.setOnCancelListener { - println("Cancel result, signal triggered") - } - - return cancellationSignal - } - - @SuppressLint("NewApi") - private fun getFingerprintCancellationSignal(): androidx.core.os.CancellationSignal { - // With this cancel signal, we can cancel biometric prompt operation - val cancellationSignal = androidx.core.os.CancellationSignal() - cancellationSignal.setOnCancelListener { - println("Cancel result, signal triggered") - } - - return cancellationSignal - } - - @SuppressLint("NewApi") - private val biometricAuthenticationCallback = object : BiometricPrompt.AuthenticationCallback() { - - override fun onAuthenticationError(errorCode: Int, errString: CharSequence?) { - super.onAuthenticationError(errorCode, errString) - Toast.makeText(this@EnterPassCode, errString, Toast.LENGTH_LONG).show() - finishActivity() - } - } - - private val fingerprintAuthCallback = object : FingerprintManagerCompat.AuthenticationCallback() { - - override fun onAuthenticationError(errMsgId: Int, errString: CharSequence?) { - super.onAuthenticationError(errMsgId, errString) - Toast.makeText(this@EnterPassCode, errString, Toast.LENGTH_LONG).show() - if (biometricDialogV23 != null) { - biometricDialogV23!!.dismiss() - } - finishActivity() - } - - override fun onAuthenticationHelp(helpMsgId: Int, helpString: CharSequence?) { - super.onAuthenticationHelp(helpMsgId, helpString) - Toast.makeText(this@EnterPassCode, helpString, Toast.LENGTH_SHORT).show() - } - - override fun onAuthenticationSucceeded(result: FingerprintManagerCompat.AuthenticationResult?) { - super.onAuthenticationSucceeded(result) - if (biometricDialogV23 != null) { - biometricDialogV23!!.dismiss() - } - } - - override fun onAuthenticationFailed() { - super.onAuthenticationFailed() - Toast.makeText(this@EnterPassCode, R.string.biometric_auth_failed, Toast.LENGTH_SHORT).show() - } - } - - private fun finishActivity() { - if (intent.getBooleanExtra(Constants.NO_RETURN, false)) { - ActivityCompat.finishAffinity(this) - } else { - finish() - } - } } \ No newline at end of file diff --git a/app/src/main/java/com/dcrandroid/activities/EnterPasswordActivity.kt b/app/src/main/java/com/dcrandroid/activities/EnterPasswordActivity.kt index ee84a0be9..305477117 100644 --- a/app/src/main/java/com/dcrandroid/activities/EnterPasswordActivity.kt +++ b/app/src/main/java/com/dcrandroid/activities/EnterPasswordActivity.kt @@ -6,23 +6,13 @@ package com.dcrandroid.activities -import android.annotation.SuppressLint import android.app.Activity -import android.content.DialogInterface import android.content.Intent -import android.hardware.biometrics.BiometricPrompt import android.os.Bundle -import android.os.CancellationSignal -import android.widget.Toast import androidx.appcompat.app.AppCompatActivity -import androidx.core.app.ActivityCompat -import androidx.core.hardware.fingerprint.FingerprintManagerCompat import com.dcrandroid.R import com.dcrandroid.data.Constants -import com.dcrandroid.dialog.BiometricDialogV23 import com.dcrandroid.util.PreferenceUtil -import com.dcrandroid.util.Utils -import com.google.android.material.snackbar.Snackbar import kotlinx.android.synthetic.main.change_password.* class EnterPasswordActivity : AppCompatActivity() { @@ -32,8 +22,6 @@ class EnterPasswordActivity : AppCompatActivity() { private var util: PreferenceUtil? = null - private var biometricDialogV23: BiometricDialogV23? = null - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.change_password) @@ -55,8 +43,6 @@ class EnterPasswordActivity : AppCompatActivity() { } else { tv_prompt.text = getString(R.string.enter_startup_password) } - - checkBiometric() } btn_ok.setOnClickListener { @@ -86,137 +72,4 @@ class EnterPasswordActivity : AppCompatActivity() { } } - private fun checkBiometric() { - if (!util!!.getBoolean(Constants.USE_BIOMETRIC, false)) { - println("Biometric not enabled in settings") - return - } - - val keyName = if (isSpendingPassword!!) Constants.SPENDING_PASSPHRASE_TYPE else Constants.STARTUP_PASSPHRASE_TYPE - - if (Utils.Biometric.isSupportBiometricPrompt(this)) { - displayBiometricPrompt(keyName) - } else if (Utils.Biometric.isSupportFingerprint(this)) { - println("Device does support biometric prompt") - showFingerprintDialog(keyName) - } - } - - @SuppressLint("NewApi") - private fun displayBiometricPrompt(keyName: String) { - try { - Utils.Biometric.generateKeyPair(keyName, true) - val signature = Utils.Biometric.initSignature(keyName) - - if (signature != null) { - - val biometricPrompt = BiometricPrompt.Builder(this) - .setTitle(getString(R.string.authentication_required)) - .setNegativeButton("Cancel", mainExecutor, DialogInterface.OnClickListener { _, _ -> - finishActivity() - }) - .build() - - biometricPrompt.authenticate(BiometricPrompt.CryptoObject(signature), getBiometricCancellationSignal(), mainExecutor, biometricAuthenticationCallback) - } - } catch (e: Exception) { - e.printStackTrace() - } - } - - private fun showFingerprintDialog(keyName: String) { - val fingerprintManager = FingerprintManagerCompat.from(this) - if (fingerprintManager.hasEnrolledFingerprints()) { - - Utils.Biometric.generateKeyPair(keyName, true) - val signature = Utils.Biometric.initSignature(keyName) - - fingerprintManager.authenticate(FingerprintManagerCompat.CryptoObject(signature!!), 0, - getFingerprintCancellationSignal(), fingerprintAuthCallback, null) - - runOnUiThread { - - biometricDialogV23 = BiometricDialogV23(this) - val cancelListener = object : BiometricDialogV23.CancelListener { - override fun onCancel() { - finishActivity() - } - } - - biometricDialogV23!!.setCancelListener(cancelListener) - biometricDialogV23!!.setTitle(R.string.authentication_required) - biometricDialogV23!!.show() - } - } - } - - @SuppressLint("NewApi") - private fun getBiometricCancellationSignal(): CancellationSignal { - // With this cancel signal, we can cancel biometric prompt operation - val cancellationSignal = CancellationSignal() - cancellationSignal.setOnCancelListener { - println("Cancel result, signal triggered") - } - - return cancellationSignal - } - - @SuppressLint("NewApi") - private fun getFingerprintCancellationSignal(): androidx.core.os.CancellationSignal { - // With this cancel signal, we can cancel biometric prompt operation - val cancellationSignal = androidx.core.os.CancellationSignal() - cancellationSignal.setOnCancelListener { - println("Cancel result, signal triggered") - } - - return cancellationSignal - } - - @SuppressLint("NewApi") - private val biometricAuthenticationCallback = object : BiometricPrompt.AuthenticationCallback() { - - override fun onAuthenticationError(errorCode: Int, errString: CharSequence?) { - super.onAuthenticationError(errorCode, errString) - Toast.makeText(this@EnterPasswordActivity, errString, Toast.LENGTH_LONG).show() - finishActivity() - } - } - - private val fingerprintAuthCallback = object : FingerprintManagerCompat.AuthenticationCallback() { - - override fun onAuthenticationError(errMsgId: Int, errString: CharSequence?) { - super.onAuthenticationError(errMsgId, errString) - Toast.makeText(this@EnterPasswordActivity, errString, Toast.LENGTH_LONG).show() - if (biometricDialogV23 != null) { - biometricDialogV23!!.dismiss() - } - finishActivity() - } - - override fun onAuthenticationHelp(helpMsgId: Int, helpString: CharSequence?) { - super.onAuthenticationHelp(helpMsgId, helpString) - Toast.makeText(this@EnterPasswordActivity, helpString, Toast.LENGTH_SHORT).show() - } - - override fun onAuthenticationSucceeded(result: FingerprintManagerCompat.AuthenticationResult?) { - super.onAuthenticationSucceeded(result) - if (biometricDialogV23 != null) { - biometricDialogV23!!.dismiss() - } - } - - override fun onAuthenticationFailed() { - super.onAuthenticationFailed() - Toast.makeText(this@EnterPasswordActivity, R.string.biometric_auth_failed, Toast.LENGTH_SHORT).show() - } - } - - private fun finishActivity() { - if (intent.getBooleanExtra(Constants.NO_RETURN, false)) { - ActivityCompat.finishAffinity(this) - } else { - finish() - } - } - } \ No newline at end of file diff --git a/app/src/main/java/com/dcrandroid/activities/SettingsActivity.java b/app/src/main/java/com/dcrandroid/activities/SettingsActivity.java index 08cf76b4c..c8d97cc2d 100644 --- a/app/src/main/java/com/dcrandroid/activities/SettingsActivity.java +++ b/app/src/main/java/com/dcrandroid/activities/SettingsActivity.java @@ -6,42 +6,43 @@ package com.dcrandroid.activities; -import android.annotation.SuppressLint; import android.app.ProgressDialog; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; -import android.hardware.biometrics.BiometricPrompt; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.os.Build; import android.os.Bundle; -import android.os.CancellationSignal; import android.view.View; import android.view.WindowManager; import android.widget.Toast; +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AppCompatActivity; +import androidx.biometric.BiometricConstants; +import androidx.biometric.BiometricPrompt; +import androidx.preference.EditTextPreference; +import androidx.preference.ListPreference; +import androidx.preference.Preference; +import androidx.preference.PreferenceFragmentCompat; +import androidx.preference.SwitchPreference; + import com.dcrandroid.BuildConfig; import com.dcrandroid.MainActivity; import com.dcrandroid.R; import com.dcrandroid.data.Constants; -import com.dcrandroid.dialog.BiometricDialogV23; import com.dcrandroid.dialog.DeleteWalletDialog; +import com.dcrandroid.dialog.PasswordDialog; import com.dcrandroid.dialog.StakeyDialog; import com.dcrandroid.util.PreferenceUtil; import com.dcrandroid.util.Utils; import com.dcrandroid.util.WalletData; -import java.security.Signature; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; -import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.app.AppCompatActivity; -import androidx.core.hardware.fingerprint.FingerprintManagerCompat; -import androidx.preference.EditTextPreference; -import androidx.preference.ListPreference; -import androidx.preference.Preference; -import androidx.preference.PreferenceFragmentCompat; -import androidx.preference.SwitchPreference; import dcrlibwallet.Dcrlibwallet; import dcrlibwallet.LibWallet; @@ -63,15 +64,18 @@ protected void onCreate(Bundle savedInstanceState) { public static class MainPreferenceFragment extends PreferenceFragmentCompat { private final int ENCRYPT_REQUEST_CODE = 1; private final int PASSCODE_REQUEST_CODE = 2; + private final int BIOMETRIC_AUTH_PASSCODE_REQUEST = 3; + private PreferenceUtil util; private int buildDateClicks = 0; - private SwitchPreference encryptWallet, useBiometric; + private SwitchPreference encryptWallet; + private ListPreference useBiometric; private Preference changeStartupPass; private ProgressDialog pd; private LibWallet wallet; - private BiometricDialogV23 biometricDialogV23; + private String biometricAuthNewValue = null; @Override public void onCreate(final Bundle savedInstanceState) { @@ -87,7 +91,7 @@ public void onCreate(final Bundle savedInstanceState) { pd = Utils.getProgressDialog(getActivity(), false, false, ""); encryptWallet = (SwitchPreference) findPreference(Constants.ENCRYPT); changeStartupPass = findPreference("change_startup_passphrase"); - useBiometric = (SwitchPreference) findPreference(Constants.USE_BIOMETRIC); + useBiometric = (ListPreference) findPreference(Constants.USE_BIOMETRIC); final EditTextPreference remoteNodeAddress = (EditTextPreference) findPreference(getString(R.string.remote_node_address)); final EditTextPreference remoteNodeCertificate = (EditTextPreference) findPreference(getString(R.string.key_connection_certificate)); final EditTextPreference peerAddress = (EditTextPreference) findPreference(Constants.PEER_IP); @@ -429,19 +433,30 @@ public boolean onPreferenceClick(Preference preference) { useBiometric.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { @Override public boolean onPreferenceChange(Preference preference, Object newValue) { + useBiometric.setEnabled(false); - if (!Utils.Biometric.isFingerprintEnrolled(getContext())) { - // This is not supposed to come up because the switch preference is hidden - // for unsupported devices, but be paranoid. - Toast.makeText(getContext(), R.string.no_biometric_support, Toast.LENGTH_SHORT).show(); - return false; - } else if (!Utils.Biometric.isFingerprintEnrolled(getContext())) { - //TODO: Check if other biometric options are enrolled - Toast.makeText(getContext(), R.string.no_fingerprint_enrolled, Toast.LENGTH_SHORT).show(); - return false; - } + biometricAuthNewValue = (String) newValue; + if (util.get(Constants.SPENDING_PASSPHRASE_TYPE).equals(Constants.PIN)) { + startActivityForResult(new Intent(getActivity(), EnterPassCode.class), BIOMETRIC_AUTH_PASSCODE_REQUEST); + } else { + PasswordDialog dialog = new PasswordDialog(getContext()) + .setPositiveButton(new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + PasswordDialog passwordDialog = (PasswordDialog) dialog; + savePassword(passwordDialog.getPassword()); + } + }); - checkBiometric(); + dialog.setOnCancelListener(new DialogInterface.OnCancelListener() { + @Override + public void onCancel(DialogInterface dialog) { + useBiometric.setEnabled(true); + } + }); + + dialog.show(); + } return false; } @@ -510,6 +525,15 @@ public void run() { }); } } + } else if (requestCode == BIOMETRIC_AUTH_PASSCODE_REQUEST) { + if (resultCode == RESULT_OK) { + String passphrase = data.getStringExtra(Constants.PASSPHRASE); + savePassword(passphrase); + } else { + // re-enable preference option since the user + // canceled the pass code input + useBiometric.setEnabled(true); + } } } @@ -518,73 +542,75 @@ public void onCreatePreferences(Bundle bundle, String s) { setPreferencesFromResource(R.xml.pref_main, s); } - private void checkBiometric() { - if (getActivity() == null || getContext() == null) { - return; - } - - if (Utils.Biometric.isSupportBiometricPrompt(getContext())) { - displayBiometricPrompt(); - } else if (Utils.Biometric.isSupportFingerprint(getContext())) { - System.out.println("Device does support biometric prompt"); - showFingerprintDialog(); - } - } + private void savePassword(final String password) { + new Thread() { + public void run() { + try { + // verify that entered pass is correct + wallet.unlockWallet(password.getBytes()); + wallet.lockWallet(); + System.out.println("Wallet unlocked successfully"); + + if (biometricAuthNewValue.equals(Constants.OFF)) { + if (getActivity() != null) { + getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + useBiometric.setValue(biometricAuthNewValue); + useBiometric.setEnabled(true); + } + }); + } + return; + } - @SuppressLint("NewApi") - private void displayBiometricPrompt() { - if (getActivity() == null || getContext() == null) { - return; - } + if (biometricAuthNewValue.equals(Constants.FINGERPRINT)) { + Utils.Biometric.savePassToKeystore(getContext(), password, Constants.SPENDING_PASSPHRASE_TYPE); + System.out.println("Password saved successfully, checking biometric support"); + } - try { - Utils.Biometric.generateKeyPair(Constants.SPENDING_PASSPHRASE_TYPE, true); - Signature signature = Utils.Biometric.initSignature(Constants.SPENDING_PASSPHRASE_TYPE); + if (getActivity() != null) { + getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + checkBiometric(); + } + }); + } - if (signature != null) { + } catch (final Exception e) { + e.printStackTrace(); - BiometricPrompt biometricPrompt = new BiometricPrompt.Builder(getContext()) - .setTitle(getString(R.string.authentication_required)) - .setNegativeButton("Cancel", getActivity().getMainExecutor(), new DialogInterface.OnClickListener() { + if (getActivity() != null) { + getActivity().runOnUiThread(new Runnable() { @Override - public void onClick(DialogInterface dialog, int which) { - + public void run() { + String errMessage = Utils.translateError(getContext(), e); + Toast.makeText(getContext(), "Error Occurred: " + errMessage, Toast.LENGTH_LONG).show(); + useBiometric.setEnabled(true); } - }) - .build(); - - biometricPrompt.authenticate(new BiometricPrompt.CryptoObject(signature), getBiometricCancellationSignal(), getActivity().getMainExecutor(), biometricAuthenticationCallback); + }); + } + } } - } catch (Exception e) { - e.printStackTrace(); - } + }.start(); } - private void showFingerprintDialog() { - if (getContext() == null || getActivity() == null) { + private void checkBiometric() { + if (getActivity() == null || getContext() == null) { return; } - FingerprintManagerCompat fingerprintManager = FingerprintManagerCompat.from(getContext()); - if (fingerprintManager.hasEnrolledFingerprints()) { + if (Utils.Biometric.isFingerprintEnrolled(getContext())) { try { - Utils.Biometric.generateKeyPair(Constants.SPENDING_PASSPHRASE_TYPE, true); - Signature signature = Utils.Biometric.initSignature(Constants.SPENDING_PASSPHRASE_TYPE); - - if (signature != null) { + Executor executor = Executors.newSingleThreadExecutor(); + BiometricPrompt biometricPrompt = new BiometricPrompt(getActivity(), executor, biometricAuthenticationCallback); + BiometricPrompt.PromptInfo promptInfo = new BiometricPrompt.PromptInfo.Builder() + .setTitle(getString(R.string.authentication_required)) + .setNegativeButtonText(getString(R.string.cancel)) + .build(); - fingerprintManager.authenticate(new FingerprintManagerCompat.CryptoObject(signature), 0, - getFingerprintCancellationSignal(), fingerprintAuthCallback, null); - - getActivity().runOnUiThread(new Runnable() { - @Override - public void run() { - biometricDialogV23 = new BiometricDialogV23(getContext()); - biometricDialogV23.setTitle(R.string.authentication_required); - biometricDialogV23.show(); - } - }); - } + biometricPrompt.authenticate(promptInfo); } catch (Exception e) { e.printStackTrace(); @@ -592,81 +618,53 @@ public void run() { } } - @SuppressLint("NewApi") - private CancellationSignal getBiometricCancellationSignal() { - // With this cancel signal, we can cancel biometric prompt operation - CancellationSignal cancellationSignal = new CancellationSignal(); - cancellationSignal.setOnCancelListener(new CancellationSignal.OnCancelListener() { - @Override - public void onCancel() { - System.out.println("Cancel result, signal triggered"); - } - }); - - return cancellationSignal; - } - - @SuppressLint("NewApi") - private androidx.core.os.CancellationSignal getFingerprintCancellationSignal() { - // With this cancel signal, we can cancel biometric prompt operation - androidx.core.os.CancellationSignal cancellationSignal = new androidx.core.os.CancellationSignal(); - cancellationSignal.setOnCancelListener(new androidx.core.os.CancellationSignal.OnCancelListener() { - @Override - public void onCancel() { - System.out.println("Cancel result, signal triggered"); - } - }); - - return cancellationSignal; - } - - @SuppressLint("NewApi") private BiometricPrompt.AuthenticationCallback biometricAuthenticationCallback = new BiometricPrompt.AuthenticationCallback() { @Override - public void onAuthenticationError(int errorCode, CharSequence errString) { + public void onAuthenticationError(int errorCode, @NonNull final CharSequence errString) { super.onAuthenticationError(errorCode, errString); - Toast.makeText(getContext(), errString, Toast.LENGTH_LONG).show(); - } + if (getActivity() == null || getContext() == null) { + return; + } - @Override - public void onAuthenticationSucceeded(BiometricPrompt.AuthenticationResult result) { - super.onAuthenticationSucceeded(result); - useBiometric.setChecked(!useBiometric.isChecked()); - } - }; + getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + useBiometric.setEnabled(true); + } + }); - private FingerprintManagerCompat.AuthenticationCallback fingerprintAuthCallback = new FingerprintManagerCompat.AuthenticationCallback() { - @Override - public void onAuthenticationError(int errMsgId, CharSequence errString) { - super.onAuthenticationError(errMsgId, errString); - Toast.makeText(getContext(), errString, Toast.LENGTH_LONG).show(); - if (biometricDialogV23 != null) { - biometricDialogV23.dismiss(); + System.out.println("Biometric Error Code: " + errorCode + " Error String: " + errString); + if (errorCode == BiometricConstants.ERROR_NEGATIVE_BUTTON) { + return; } - } - @Override - public void onAuthenticationHelp(int helpMsgId, CharSequence helpString) { - super.onAuthenticationHelp(helpMsgId, helpString); - Toast.makeText(getContext(), helpString, Toast.LENGTH_SHORT).show(); - } + final String message = Utils.Biometric.translateError(getContext(), errorCode); - @Override - public void onAuthenticationSucceeded(FingerprintManagerCompat.AuthenticationResult result) { - super.onAuthenticationSucceeded(result); - if (biometricDialogV23 != null) { - biometricDialogV23.dismiss(); + if (message != null) { + getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + Toast.makeText(getContext(), message, Toast.LENGTH_LONG).show(); + } + }); } - - useBiometric.setChecked(!useBiometric.isChecked()); } @Override - public void onAuthenticationFailed() { - super.onAuthenticationFailed(); - Toast.makeText(getContext(), R.string.biometric_auth_failed, Toast.LENGTH_SHORT).show(); + public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) { + super.onAuthenticationSucceeded(result); + if (getActivity() != null) { + getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + useBiometric.setEnabled(true); + useBiometric.setValue(biometricAuthNewValue); + } + }); + } } }; + } } \ No newline at end of file diff --git a/app/src/main/java/com/dcrandroid/data/Constants.java b/app/src/main/java/com/dcrandroid/data/Constants.java index 58dd83339..c5d6cfaa8 100644 --- a/app/src/main/java/com/dcrandroid/data/Constants.java +++ b/app/src/main/java/com/dcrandroid/data/Constants.java @@ -113,8 +113,13 @@ public class Constants { SYNC_STATE_START = "start", SYNC_STATE_PROGRESS = "progress", SYNC_STATE_FINISH = "finish", + ANDROID_KEY_STORE = "AndroidKeyStore", + TRANSFORMATION = "AES/GCM/NoPadding", + ENCRYPTION_DATA = "encryption_data", + ENCRYPTION_IV = "encryption_iv", + OFF = "off", + FINGERPRINT = "fingerprint", + FINGERPRINT_PASS = "fingerprint_pass", RECENT_TRANSACTION_HASH = "recent_transaction_hash"; public static final int TRANSACTION_SUMMARY_ID = 5552478, REQUIRED_CONFIRMATIONS = 2; - - public static final long ESTIMATED_ACCT_DISCOVERY = 300000; } diff --git a/app/src/main/java/com/dcrandroid/dialog/BiometricDialogV23.kt b/app/src/main/java/com/dcrandroid/dialog/BiometricDialogV23.kt deleted file mode 100644 index 23e117d00..000000000 --- a/app/src/main/java/com/dcrandroid/dialog/BiometricDialogV23.kt +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright (c) 2018-2019 The Decred developers - * Use of this source code is governed by an ISC - * license that can be found in the LICENSE file. - */ - -package com.dcrandroid.dialog - -import android.content.Context -import android.widget.Button -import android.widget.ImageView -import android.widget.TextView -import com.dcrandroid.R -import com.google.android.material.bottomsheet.BottomSheetDialog - -class BiometricDialogV23(context: Context) : BottomSheetDialog(context) { - - private var cancel: Button? = null - private var tvTitle: TextView? = null - private var imgLogo: ImageView? = null - - private var listener: CancelListener? = null - - init { - val bottomSheetView = layoutInflater.inflate(R.layout.fingerprint_bottom_sheet, null) - setContentView(bottomSheetView) - - cancel = bottomSheetView.findViewById(R.id.btn_cancel) - cancel!!.setOnClickListener { - dismiss() - if (listener != null) { - listener!!.onCancel() - } - } - - tvTitle = bottomSheetView.findViewById(R.id.item_title) - imgLogo = bottomSheetView.findViewById(R.id.img_logo) - - try { - val drawable = getContext().packageManager.getApplicationIcon(context.packageName) - imgLogo!!.setImageDrawable(drawable) - } catch (e: Exception) { - e.printStackTrace() - } - } - - fun setTitle(title: String) { - tvTitle!!.text = title - } - - fun setCancelListener(listener: CancelListener) { - this.listener = listener - } - - public interface CancelListener { - abstract fun onCancel() - } -} \ No newline at end of file diff --git a/app/src/main/java/com/dcrandroid/dialog/ConfirmTransactionDialog.kt b/app/src/main/java/com/dcrandroid/dialog/ConfirmTransactionDialog.kt index 5257bf578..fa3e4a0fd 100644 --- a/app/src/main/java/com/dcrandroid/dialog/ConfirmTransactionDialog.kt +++ b/app/src/main/java/com/dcrandroid/dialog/ConfirmTransactionDialog.kt @@ -24,9 +24,9 @@ import com.dcrandroid.util.Utils import dcrlibwallet.Dcrlibwallet import kotlinx.android.synthetic.main.confirm_tx_dialog.* import java.math.BigDecimal +import java.math.MathContext import java.math.RoundingMode import java.text.DecimalFormat -import java.math.MathContext class ConfirmTransactionDialog(context: Context) : Dialog(context), View.OnClickListener { @@ -75,7 +75,10 @@ class ConfirmTransactionDialog(context: Context) : Dialog(context), View.OnClick val feeCoin = Dcrlibwallet.amountCoin(estFee) tvFee.text = "${context.getString(R.string.withFeeOff)} ${format.format(feeCoin)} DCR" - if (util.get(Constants.SPENDING_PASSPHRASE_TYPE) == Constants.PIN) { + // Hide passphrase input if user's spending passphrase type is pin + // or user chose the fingerprint only biometric option + val biometricOption = util.get(Constants.USE_BIOMETRIC) + if (util.get(Constants.SPENDING_PASSPHRASE_TYPE) == Constants.PIN || biometricOption == Constants.FINGERPRINT) { passphrase_input_layout.visibility = View.GONE btn_positive.isEnabled = true btn_positive.setTextColor(ContextCompat.getColor(context, R.color.blue)) @@ -84,7 +87,7 @@ class ConfirmTransactionDialog(context: Context) : Dialog(context), View.OnClick } // display conversion if enabled and exchange rate has been fetched - if(exchangeDecimal != null && Integer.parseInt(util.get(Constants.CURRENCY_CONVERSION, "0")) != 0){ + if (exchangeDecimal != null && Integer.parseInt(util.get(Constants.CURRENCY_CONVERSION, "0")) != 0) { val amountUSD = dcrToUSD(amountCoin) tvTitle.text = "${tvTitle.text} ($${format.format(amountUSD)})" @@ -93,7 +96,7 @@ class ConfirmTransactionDialog(context: Context) : Dialog(context), View.OnClick } } - private fun dcrToUSD(dcr: Double): Double{ + private fun dcrToUSD(dcr: Double): Double { var currentAmount = BigDecimal(dcr) currentAmount = currentAmount.setScale(9, RoundingMode.HALF_UP) @@ -102,18 +105,18 @@ class ConfirmTransactionDialog(context: Context) : Dialog(context), View.OnClick // USD is displayed in 2 decimal places by default. // If the converted amount is less than two significant figures, // it would be rounded to the nearest significant figure. - if(convertedAmount.toDouble() < 0.01){ + if (convertedAmount.toDouble() < 0.01) { convertedAmount = convertedAmount.round(MathContext(1)) return convertedAmount.toDouble() - }else{ + } else { //round to 2 decimal places return Math.round(convertedAmount.toDouble() * 100.0) / 100.0 } } - fun setExchangeDecimal(exchangeDecimal: BigDecimal?): ConfirmTransactionDialog{ + fun setExchangeDecimal(exchangeDecimal: BigDecimal?): ConfirmTransactionDialog { this.exchangeDecimal = exchangeDecimal return this } diff --git a/app/src/main/java/com/dcrandroid/dialog/PasswordDialog.kt b/app/src/main/java/com/dcrandroid/dialog/PasswordDialog.kt new file mode 100644 index 000000000..cb8d65119 --- /dev/null +++ b/app/src/main/java/com/dcrandroid/dialog/PasswordDialog.kt @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2018-2019 The Decred developers + * Use of this source code is governed by an ISC + * license that can be found in the LICENSE file. + */ + +package com.dcrandroid.dialog + +import android.annotation.SuppressLint +import android.app.Dialog +import android.content.Context +import android.content.DialogInterface +import android.os.Bundle +import android.text.Editable +import android.text.TextWatcher +import android.view.View +import android.view.Window +import androidx.core.content.ContextCompat +import com.dcrandroid.R +import kotlinx.android.synthetic.main.password_dialog.* + +class PasswordDialog(context: Context) : Dialog(context), View.OnClickListener { + + private var btnPositiveClick: DialogInterface.OnClickListener? = null + + @SuppressLint("SetTextI18n") + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + requestWindowFeature(Window.FEATURE_NO_TITLE) + setContentView(R.layout.password_dialog) + + btn_positive.setOnClickListener(this) + btn_negative.setOnClickListener(this) + + password_input.addTextChangedListener(object : TextWatcher { + override fun afterTextChanged(s: Editable?) { + } + + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { + } + + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { + if (s!!.isNotEmpty()) { + btn_positive.isEnabled = true + btn_positive.setTextColor(ContextCompat.getColor(context, R.color.blue)) + } else { + btn_positive.isEnabled = false + btn_positive.setTextColor(ContextCompat.getColor(context, R.color.lightGreyBackgroundColor)) + } + } + + }) + } + + fun setPositiveButton(listener: DialogInterface.OnClickListener?): PasswordDialog { + btnPositiveClick = listener + return this + } + + fun getPassword(): String { + return password_input.text.toString() + } + + override fun onClick(v: View?) { + when (v!!.id) { + R.id.btn_negative -> { + cancel() + } + R.id.btn_positive -> { + dismiss() + if (btnPositiveClick != null) { + btnPositiveClick?.onClick(this, DialogInterface.BUTTON_POSITIVE) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/dcrandroid/fragments/ChangePasswordFragment.kt b/app/src/main/java/com/dcrandroid/fragments/ChangePasswordFragment.kt index 2cdffc988..80b066700 100644 --- a/app/src/main/java/com/dcrandroid/fragments/ChangePasswordFragment.kt +++ b/app/src/main/java/com/dcrandroid/fragments/ChangePasswordFragment.kt @@ -103,6 +103,12 @@ class ChangePasswordFragment : Fragment(), View.OnKeyListener { util.set(Constants.STARTUP_PASSPHRASE_TYPE, Constants.PASSWORD) } + // Update saved pass + val biometricOption = util.get(Constants.USE_BIOMETRIC) + if (biometricOption == Constants.FINGERPRINT) { + Utils.Biometric.savePassToKeystore(context, password, Constants.SPENDING_PASSPHRASE_TYPE) + } + activity!!.runOnUiThread { if (pd!!.isShowing) { pd!!.dismiss() diff --git a/app/src/main/java/com/dcrandroid/fragments/ChangePinFragment.kt b/app/src/main/java/com/dcrandroid/fragments/ChangePinFragment.kt index bac5852e8..096e76915 100644 --- a/app/src/main/java/com/dcrandroid/fragments/ChangePinFragment.kt +++ b/app/src/main/java/com/dcrandroid/fragments/ChangePinFragment.kt @@ -123,6 +123,12 @@ class ChangePinFragment : Fragment(), KeyPad.KeyPadListener { util.set(Constants.STARTUP_PASSPHRASE_TYPE, Constants.PIN) } + // Update saved pass + val biometricOption = util.get(Constants.USE_BIOMETRIC) + if (biometricOption == Constants.FINGERPRINT) { + Utils.Biometric.savePassToKeystore(context, passCode, Constants.SPENDING_PASSPHRASE_TYPE) + } + activity!!.runOnUiThread { if (pd!!.isShowing) { pd!!.dismiss() diff --git a/app/src/main/java/com/dcrandroid/fragments/SecurityFragment.java b/app/src/main/java/com/dcrandroid/fragments/SecurityFragment.java index ff9425c07..58f006af7 100644 --- a/app/src/main/java/com/dcrandroid/fragments/SecurityFragment.java +++ b/app/src/main/java/com/dcrandroid/fragments/SecurityFragment.java @@ -6,7 +6,6 @@ package com.dcrandroid.fragments; -import android.annotation.SuppressLint; import android.app.ProgressDialog; import android.content.BroadcastReceiver; import android.content.Context; @@ -14,9 +13,7 @@ import android.content.Intent; import android.content.IntentFilter; import android.graphics.Color; -import android.hardware.biometrics.BiometricPrompt; import android.os.Bundle; -import android.os.CancellationSignal; import android.text.Editable; import android.text.Html; import android.text.TextWatcher; @@ -32,111 +29,37 @@ import android.widget.TextView; import android.widget.Toast; +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; +import androidx.biometric.BiometricConstants; +import androidx.biometric.BiometricPrompt; +import androidx.core.content.ContextCompat; +import androidx.fragment.app.Fragment; + import com.dcrandroid.R; import com.dcrandroid.activities.EnterPassCode; import com.dcrandroid.data.Constants; -import com.dcrandroid.dialog.BiometricDialogV23; import com.dcrandroid.util.PreferenceUtil; import com.dcrandroid.util.Utils; import com.dcrandroid.util.WalletData; import org.jetbrains.annotations.Nullable; -import java.security.Signature; - -import androidx.annotation.NonNull; -import androidx.appcompat.app.AlertDialog; -import androidx.core.content.ContextCompat; -import androidx.core.hardware.fingerprint.FingerprintManagerCompat; -import androidx.fragment.app.Fragment; import dcrlibwallet.Dcrlibwallet; import dcrlibwallet.LibWallet; import static android.app.Activity.RESULT_OK; public class SecurityFragment extends Fragment { + private final int PASSCODE_REQUEST_CODE = 1; private EditText etAddress, etMessage, etSignature; private TextView tvValidateAddress, tvRequiredMessage, tvRequiredSignature; private Button btnSignMessage, btnCopy; private LinearLayout layout; - private BiometricDialogV23 biometricDialogV23; - BroadcastReceiver receiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - if (intent.getAction() != null && intent.getAction().equals(Constants.SYNCED)) { - if (!WalletData.getInstance().syncing) { - layout.setVisibility(View.VISIBLE); - } else { - layout.setVisibility(View.GONE); - } - } - } - }; private LibWallet wallet; private PreferenceUtil util; private ProgressDialog pd; - private TextWatcher validateAddressWatcher = new TextWatcher() { - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) { - - } - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - - } - - @Override - public void afterTextChanged(Editable s) { - checkCopyButton(); - verifyMessage(); - if (s.toString().trim().equals("")) { - tvValidateAddress.setText(null); - toggleMessageButton(false); - return; - } - - if (wallet.isAddressValid(s.toString().trim())) { - if (wallet.haveAddress(s.toString().trim())) { - tvValidateAddress.setTextColor(getActivity().getApplicationContext().getResources().getColor(R.color.bluePendingTextColor)); - tvValidateAddress.setText(R.string.owned_validate_address); - toggleMessageButton(true); - return; - } - tvValidateAddress.setTextColor(getActivity().getApplicationContext().getResources().getColor(R.color.greenTextColor)); - tvValidateAddress.setText(Html.fromHtml(getString(R.string.external_validate_address))); - toggleMessageButton(false); - } else { - tvValidateAddress.setTextColor(Color.RED); - tvValidateAddress.setText(R.string.invalid_address); - toggleMessageButton(false); - } - } - }; - private TextWatcher textWatcher = new TextWatcher() { - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) { - - } - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - - } - - @Override - public void afterTextChanged(Editable s) { - checkCopyButton(); - verifyMessage(); - - if (etSignature.getText().toString().equals("") && wallet.isAddressValid(etAddress.getText().toString())) { - toggleMessageButton(true); - } else { - toggleMessageButton(false); - } - } - }; @Nullable @Override @@ -215,16 +138,6 @@ public void onClick(View v) { return; } - if (getContext() == null) { - return; - } - - if (util.get(Constants.SPENDING_PASSPHRASE_TYPE).equals(Constants.PIN)) { - Intent intent = new Intent(getContext(), EnterPassCode.class); - startActivityForResult(intent, PASSCODE_REQUEST_CODE); - return; - } - checkBiometric(); } }); @@ -255,7 +168,7 @@ public void onClick(DialogInterface dialog, int which) { return; } - pd = Utils.getProgressDialog(getContext(), false, false, "Signing..."); + pd = Utils.getProgressDialog(getContext(), false, false, getString(R.string.signing_ellipsis)); pd.show(); new Thread() { public void run() { @@ -277,7 +190,7 @@ public void run() { public void onActivityResult(int requestCode, int resultCode, final Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == PASSCODE_REQUEST_CODE && resultCode == RESULT_OK) { - pd = Utils.getProgressDialog(getContext(), false, false, "Signing..."); + pd = Utils.getProgressDialog(getContext(), false, false, getString(R.string.signing_ellipsis)); pd.show(); new Thread() { public void run() { @@ -416,164 +329,168 @@ private void toggleMessageButton(boolean enable) { } } + private void promptPass() { + if (util.get(Constants.SPENDING_PASSPHRASE_TYPE).equals(Constants.PIN)) { + Intent intent = new Intent(getContext(), EnterPassCode.class); + startActivityForResult(intent, PASSCODE_REQUEST_CODE); + } else { + getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + showPasswordDialog(); + } + }); + } + } + private void checkBiometric() { if (getActivity() == null || getContext() == null) { return; } - if (!util.getBoolean(Constants.USE_BIOMETRIC, false)) { + String biometricOption = util.get(Constants.USE_BIOMETRIC); + if (biometricOption.equals(Constants.OFF)) { System.out.println("Biometric not enabled in settings"); - showPasswordDialog(); + promptPass(); return; } - if (Utils.Biometric.isSupportBiometricPrompt(getContext())) { - displayBiometricPrompt(); - } else if (Utils.Biometric.isSupportFingerprint(getContext())) { - System.out.println("Device does support biometric prompt"); - showFingerprintDialog(); - } else { - showPasswordDialog(); + if (!Utils.Biometric.displayBiometricPrompt(getActivity(), authenticationCallback)) { + Utils.showMessage(getActivity(), getString(R.string.no_fingerprint_error), Toast.LENGTH_LONG); } } - @SuppressLint("NewApi") - private void displayBiometricPrompt() { - if (getActivity() == null || getContext() == null) { - return; - } - - try { - Utils.Biometric.generateKeyPair(Constants.SPENDING_PASSPHRASE_TYPE, true); - Signature signature = Utils.Biometric.initSignature(Constants.SPENDING_PASSPHRASE_TYPE); - - if (signature != null) { - - BiometricPrompt biometricPrompt = new BiometricPrompt.Builder(getContext()) - .setTitle(getString(R.string.authentication_required)) - .setNegativeButton("Cancel", getActivity().getMainExecutor(), new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - - } - }) - .build(); - - biometricPrompt.authenticate(new BiometricPrompt.CryptoObject(signature), getBiometricCancellationSignal(), getActivity().getMainExecutor(), biometricAuthenticationCallback); + private BiometricPrompt.AuthenticationCallback authenticationCallback = new BiometricPrompt.AuthenticationCallback() { + @Override + public void onAuthenticationError(final int errorCode, @NonNull final CharSequence errString) { + super.onAuthenticationError(errorCode, errString); + if (getContext() == null || getActivity() == null) { + return; } - } catch (Exception e) { - e.printStackTrace(); - } - } - - private void showFingerprintDialog() { - if (getContext() == null || getActivity() == null) { - return; - } - - FingerprintManagerCompat fingerprintManager = FingerprintManagerCompat.from(getContext()); - if (fingerprintManager.hasEnrolledFingerprints()) { - try { - Utils.Biometric.generateKeyPair(Constants.SPENDING_PASSPHRASE_TYPE, true); - Signature signature = Utils.Biometric.initSignature(Constants.SPENDING_PASSPHRASE_TYPE); - if (signature != null) { + System.out.println("Biometric Error Code: " + errorCode + " Error String: " + errString); - fingerprintManager.authenticate(new FingerprintManagerCompat.CryptoObject(signature), 0, - getFingerprintCancellationSignal(), fingerprintAuthCallback, null); + if(errorCode == BiometricConstants.ERROR_NEGATIVE_BUTTON){ + return; + } - getActivity().runOnUiThread(new Runnable() { - @Override - public void run() { - biometricDialogV23 = new BiometricDialogV23(getContext()); - biometricDialogV23.setTitle(R.string.authentication_required); - biometricDialogV23.show(); - } - }); + getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + final String message = Utils.Biometric.translateError(getContext(), errorCode); + if (message != null) { + Toast.makeText(getContext(), message, Toast.LENGTH_LONG).show(); + } else { + Toast.makeText(getContext(), errString, Toast.LENGTH_LONG).show(); + } } - - } catch (Exception e) { - e.printStackTrace(); - } - } else { - showPasswordDialog(); + }); } - } - @SuppressLint("NewApi") - private CancellationSignal getBiometricCancellationSignal() { - // With this cancel signal, we can cancel biometric prompt operation - CancellationSignal cancellationSignal = new CancellationSignal(); - cancellationSignal.setOnCancelListener(new CancellationSignal.OnCancelListener() { - @Override - public void onCancel() { - System.out.println("Cancel result, signal triggered"); + @Override + public void onAuthenticationSucceeded(@NonNull androidx.biometric.BiometricPrompt.AuthenticationResult result) { + super.onAuthenticationSucceeded(result); + if (getContext() == null || getActivity() == null) { + return; } - }); - return cancellationSignal; - } + String biometricOption = util.get(Constants.USE_BIOMETRIC); + if (biometricOption.equals(Constants.FINGERPRINT)) { + getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + try { + pd = Utils.getProgressDialog(getContext(), false, false, getString(R.string.signing_ellipsis)); + pd.show(); + + String pass = Utils.Biometric.getPassFromKeystore(getContext(), Constants.SPENDING_PASSPHRASE_TYPE); + signMessage(pass.getBytes()); + } catch (Exception e) { + e.printStackTrace(); + } + } + }); - @SuppressLint("NewApi") - private androidx.core.os.CancellationSignal getFingerprintCancellationSignal() { - // With this cancel signal, we can cancel biometric prompt operation - androidx.core.os.CancellationSignal cancellationSignal = new androidx.core.os.CancellationSignal(); - cancellationSignal.setOnCancelListener(new androidx.core.os.CancellationSignal.OnCancelListener() { - @Override - public void onCancel() { - System.out.println("Cancel result, signal triggered"); + return; } - }); - return cancellationSignal; - } + promptPass(); + } + }; - @SuppressLint("NewApi") - private BiometricPrompt.AuthenticationCallback biometricAuthenticationCallback = new BiometricPrompt.AuthenticationCallback() { + BroadcastReceiver receiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (intent.getAction() != null && intent.getAction().equals(Constants.SYNCED)) { + if (!WalletData.getInstance().syncing) { + layout.setVisibility(View.VISIBLE); + } else { + layout.setVisibility(View.GONE); + } + } + } + }; + private TextWatcher validateAddressWatcher = new TextWatcher() { @Override - public void onAuthenticationError(int errorCode, CharSequence errString) { - super.onAuthenticationError(errorCode, errString); - Toast.makeText(getContext(), errString, Toast.LENGTH_LONG).show(); + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } @Override - public void onAuthenticationSucceeded(BiometricPrompt.AuthenticationResult result) { - super.onAuthenticationSucceeded(result); - showPasswordDialog(); + public void onTextChanged(CharSequence s, int start, int before, int count) { + } - }; - private FingerprintManagerCompat.AuthenticationCallback fingerprintAuthCallback = new FingerprintManagerCompat.AuthenticationCallback() { @Override - public void onAuthenticationError(int errMsgId, CharSequence errString) { - super.onAuthenticationError(errMsgId, errString); - Toast.makeText(getContext(), errString, Toast.LENGTH_LONG).show(); - if (biometricDialogV23 != null) { - biometricDialogV23.dismiss(); + public void afterTextChanged(Editable s) { + checkCopyButton(); + verifyMessage(); + if (s.toString().trim().equals("")) { + tvValidateAddress.setText(null); + toggleMessageButton(false); + return; + } + + if (wallet.isAddressValid(s.toString().trim())) { + if (wallet.haveAddress(s.toString().trim())) { + tvValidateAddress.setTextColor(getActivity().getApplicationContext().getResources().getColor(R.color.bluePendingTextColor)); + tvValidateAddress.setText(R.string.owned_validate_address); + toggleMessageButton(true); + return; + } + tvValidateAddress.setTextColor(getActivity().getApplicationContext().getResources().getColor(R.color.greenTextColor)); + tvValidateAddress.setText(Html.fromHtml(getString(R.string.external_validate_address))); + toggleMessageButton(false); + } else { + tvValidateAddress.setTextColor(Color.RED); + tvValidateAddress.setText(R.string.invalid_address); + toggleMessageButton(false); } } + }; + private TextWatcher textWatcher = new TextWatcher() { @Override - public void onAuthenticationHelp(int helpMsgId, CharSequence helpString) { - super.onAuthenticationHelp(helpMsgId, helpString); - Toast.makeText(getContext(), helpString, Toast.LENGTH_SHORT).show(); + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } @Override - public void onAuthenticationSucceeded(FingerprintManagerCompat.AuthenticationResult result) { - super.onAuthenticationSucceeded(result); - if (biometricDialogV23 != null) { - biometricDialogV23.dismiss(); - } + public void onTextChanged(CharSequence s, int start, int before, int count) { - showPasswordDialog(); } @Override - public void onAuthenticationFailed() { - super.onAuthenticationFailed(); - Toast.makeText(getContext(), R.string.biometric_auth_failed, Toast.LENGTH_SHORT).show(); + public void afterTextChanged(Editable s) { + checkCopyButton(); + verifyMessage(); + + if (etSignature.getText().toString().equals("") && wallet.isAddressValid(etAddress.getText().toString())) { + toggleMessageButton(true); + } else { + toggleMessageButton(false); + } } }; } diff --git a/app/src/main/java/com/dcrandroid/fragments/SendFragment.kt b/app/src/main/java/com/dcrandroid/fragments/SendFragment.kt index 987d253bb..767cf3628 100644 --- a/app/src/main/java/com/dcrandroid/fragments/SendFragment.kt +++ b/app/src/main/java/com/dcrandroid/fragments/SendFragment.kt @@ -6,23 +6,20 @@ package com.dcrandroid.fragments -import android.annotation.SuppressLint import android.app.Activity.RESULT_OK import android.app.ProgressDialog import android.content.DialogInterface import android.content.Intent import android.graphics.Color -import android.hardware.biometrics.BiometricPrompt import android.os.Bundle -import android.os.CancellationSignal import android.text.Editable import android.text.TextWatcher import android.view.* import android.widget.AdapterView import android.widget.EditText import android.widget.Toast +import androidx.biometric.BiometricConstants import androidx.core.content.ContextCompat -import androidx.core.hardware.fingerprint.FingerprintManagerCompat import androidx.fragment.app.Fragment import com.dcrandroid.BuildConfig import com.dcrandroid.MainActivity @@ -33,7 +30,6 @@ import com.dcrandroid.activities.TransactionDetailsActivity import com.dcrandroid.adapter.AccountSpinnerAdapter import com.dcrandroid.data.Account import com.dcrandroid.data.Constants -import com.dcrandroid.dialog.BiometricDialogV23 import com.dcrandroid.dialog.ConfirmTransactionDialog import com.dcrandroid.dialog.InfoDialog import com.dcrandroid.util.* @@ -52,8 +48,6 @@ import kotlin.collections.ArrayList class SendFragment : Fragment(), AdapterView.OnItemSelectedListener, GetExchangeRate.ExchangeRateCallback { - private var biometricDialogV23: BiometricDialogV23? = null - private var SEND_ACCOUNT = false private val SCANNER_ACTIVITY_REQUEST_CODE = 0 private val PASSCODE_REQUEST_CODE = 1 @@ -186,11 +180,7 @@ class SendFragment : Fragment(), AdapterView.OnItemSelectedListener, GetExchange send_main_error.text = null - if (util!!.get(Constants.SPENDING_PASSPHRASE_TYPE) == Constants.PIN) { - showConfirmTransactionDialog() - } else { - checkBiometric() - } + checkBiometric() } prepareAccounts() @@ -477,7 +467,7 @@ class SendFragment : Fragment(), AdapterView.OnItemSelectedListener, GetExchange } } - private fun showConfirmTransactionDialog() { + private fun showConfirmTransactionDialog(pass: String?) { if (context == null || activity == null) { return } @@ -492,6 +482,12 @@ class SendFragment : Fragment(), AdapterView.OnItemSelectedListener, GetExchange .setExchangeDecimal(exchangeDecimal) transactionDialog.setPositiveButton(DialogInterface.OnClickListener { _, _ -> + val biometricOption = util!!.get(Constants.USE_BIOMETRIC) + if (biometricOption == Constants.FINGERPRINT && pass != null) { + startTransaction(pass) + return@OnClickListener + } + if (util!!.get(Constants.SPENDING_PASSPHRASE_TYPE) == Constants.PIN) { val intent = Intent(context, EnterPassCode::class.java) startActivityForResult(intent, PASSCODE_REQUEST_CODE) @@ -506,7 +502,9 @@ class SendFragment : Fragment(), AdapterView.OnItemSelectedListener, GetExchange transactionDialog.show() } catch (e: Exception) { if (activity != null && context != null) { - Toast.makeText(context, Utils.translateError(context, e), Toast.LENGTH_SHORT).show() + activity!!.runOnUiThread { + Toast.makeText(context, Utils.translateError(context, e), Toast.LENGTH_SHORT).show() + } } } } @@ -551,11 +549,7 @@ class SendFragment : Fragment(), AdapterView.OnItemSelectedListener, GetExchange .setMessage(message) .setIcon(R.drawable.np_amount_withdrawal) .setPositiveButton(getString(R.string.retry_caps), DialogInterface.OnClickListener { _, _ -> - if (util!!.get(Constants.SPENDING_PASSPHRASE_TYPE) == Constants.PIN) { - showConfirmTransactionDialog() - } else { - checkBiometric() - } + checkBiometric() }) .setNegativeButton(getString(R.string.cancel).toUpperCase(), DialogInterface.OnClickListener { dialog, _ -> dialog.dismiss() @@ -622,128 +616,65 @@ class SendFragment : Fragment(), AdapterView.OnItemSelectedListener, GetExchange } private fun checkBiometric() { - if (!util!!.getBoolean(Constants.USE_BIOMETRIC, false)) { + + val biometricOption = util!!.get(Constants.USE_BIOMETRIC) + if (biometricOption == Constants.OFF) { println("Biometric not enabled in settings") - showConfirmTransactionDialog() + showConfirmTransactionDialog(null) return } - when { - Utils.Biometric.isSupportBiometricPrompt(context) -> displayBiometricPrompt(Constants.SPENDING_PASSPHRASE_TYPE) - Utils.Biometric.isSupportFingerprint(context) -> { - println("Device does support biometric prompt") - showFingerprintDialog(Constants.SPENDING_PASSPHRASE_TYPE) - } - else -> showConfirmTransactionDialog() + if (!Utils.Biometric.displayBiometricPrompt(activity, authenticationCallback)) { + Utils.showMessage(context, getString(R.string.no_fingerprint_error), Toast.LENGTH_LONG) } } - @SuppressLint("NewApi") - private fun displayBiometricPrompt(keyName: String) { - try { - Utils.Biometric.generateKeyPair(keyName, true) - val signature = Utils.Biometric.initSignature(keyName) - - if (signature != null) { - - val biometricPrompt = BiometricPrompt.Builder(context) - .setTitle(getString(R.string.authentication_required)) - .setNegativeButton("Cancel", activity!!.mainExecutor, DialogInterface.OnClickListener { _, _ -> - - }) - .build() - - biometricPrompt.authenticate(BiometricPrompt.CryptoObject(signature), getBiometricCancellationSignal(), activity!!.mainExecutor, biometricAuthenticationCallback) + private val authenticationCallback = object : androidx.biometric.BiometricPrompt.AuthenticationCallback() { + override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { + super.onAuthenticationError(errorCode, errString) + if (context == null || activity == null) { + return } - } catch (e: Exception) { - e.printStackTrace() - } - } - private fun showFingerprintDialog(keyName: String) { - val fingerprintManager = FingerprintManagerCompat.from(context!!) - if (fingerprintManager.hasEnrolledFingerprints()) { + println("Biometric Error Code: $errorCode Error String: $errString") - Utils.Biometric.generateKeyPair(keyName, true) - val signature = Utils.Biometric.initSignature(keyName) - - fingerprintManager.authenticate(FingerprintManagerCompat.CryptoObject(signature!!), 0, - getFingerprintCancellationSignal(), fingerprintAuthCallback, null) + if (errorCode == BiometricConstants.ERROR_NEGATIVE_BUTTON) { + return + } activity!!.runOnUiThread { - - biometricDialogV23 = BiometricDialogV23(context!!) - biometricDialogV23!!.setTitle(R.string.authentication_required) - biometricDialogV23!!.show() + val message = Utils.Biometric.translateError(context, errorCode) + if (message != null) { + Toast.makeText(context, message, Toast.LENGTH_LONG).show() + } else { + Toast.makeText(context, errString, Toast.LENGTH_LONG).show() + } } - } else { - showConfirmTransactionDialog() - } - } - - @SuppressLint("NewApi") - private fun getBiometricCancellationSignal(): CancellationSignal { - // With this cancel signal, we can cancel biometric prompt operation - val cancellationSignal = CancellationSignal() - cancellationSignal.setOnCancelListener { - println("Cancel result, signal triggered") } - return cancellationSignal - } - - @SuppressLint("NewApi") - private fun getFingerprintCancellationSignal(): androidx.core.os.CancellationSignal { - // With this cancel signal, we can cancel biometric prompt operation - val cancellationSignal = androidx.core.os.CancellationSignal() - cancellationSignal.setOnCancelListener { - println("Cancel result, signal triggered") - } - - return cancellationSignal - } - - @SuppressLint("NewApi") - private val biometricAuthenticationCallback = object : BiometricPrompt.AuthenticationCallback() { - - override fun onAuthenticationError(errorCode: Int, errString: CharSequence?) { - super.onAuthenticationError(errorCode, errString) - Toast.makeText(context, errString, Toast.LENGTH_LONG).show() - } - - override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult?) { + override fun onAuthenticationSucceeded(result: androidx.biometric.BiometricPrompt.AuthenticationResult) { super.onAuthenticationSucceeded(result) - showConfirmTransactionDialog() - } - } - - private val fingerprintAuthCallback = object : FingerprintManagerCompat.AuthenticationCallback() { - - override fun onAuthenticationError(errMsgId: Int, errString: CharSequence?) { - super.onAuthenticationError(errMsgId, errString) - Toast.makeText(context, errString, Toast.LENGTH_LONG).show() - if (biometricDialogV23 != null) { - biometricDialogV23!!.dismiss() + if (context == null || activity == null) { + return } - } - override fun onAuthenticationHelp(helpMsgId: Int, helpString: CharSequence?) { - super.onAuthenticationHelp(helpMsgId, helpString) - Toast.makeText(context, helpString, Toast.LENGTH_SHORT).show() - } + val biometricOption = util!!.get(Constants.USE_BIOMETRIC) + if (biometricOption == Constants.FINGERPRINT) { + activity!!.runOnUiThread { + try { + val pass = Utils.Biometric.getPassFromKeystore(context, Constants.SPENDING_PASSPHRASE_TYPE) + showConfirmTransactionDialog(pass) + } catch (e: Exception) { + e.printStackTrace() + } + } - override fun onAuthenticationSucceeded(result: FingerprintManagerCompat.AuthenticationResult?) { - super.onAuthenticationSucceeded(result) - if (biometricDialogV23 != null) { - biometricDialogV23!!.dismiss() + return } - showConfirmTransactionDialog() - } - - override fun onAuthenticationFailed() { - super.onAuthenticationFailed() - Toast.makeText(context, R.string.biometric_auth_failed, Toast.LENGTH_SHORT).show() + activity!!.runOnUiThread { + showConfirmTransactionDialog(null) + } } } diff --git a/app/src/main/java/com/dcrandroid/util/Utils.java b/app/src/main/java/com/dcrandroid/util/Utils.java index 5c885e93e..7d766f9c6 100644 --- a/app/src/main/java/com/dcrandroid/util/Utils.java +++ b/app/src/main/java/com/dcrandroid/util/Utils.java @@ -16,23 +16,30 @@ import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; -import android.content.res.Resources; import android.os.Build; import android.preference.PreferenceManager; import android.security.keystore.KeyGenParameterSpec; import android.security.keystore.KeyProperties; -import android.util.TypedValue; import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; import android.widget.TextView; import android.widget.Toast; +import androidx.annotation.StringRes; +import androidx.biometric.BiometricConstants; +import androidx.biometric.BiometricPrompt; +import androidx.core.app.NotificationCompat; +import androidx.core.hardware.fingerprint.FingerprintManagerCompat; +import androidx.fragment.app.FragmentActivity; + import com.dcrandroid.MainActivity; import com.dcrandroid.R; import com.dcrandroid.data.Constants; import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; @@ -43,13 +50,8 @@ import java.math.BigDecimal; import java.math.MathContext; import java.math.RoundingMode; -import java.security.KeyPair; -import java.security.KeyPairGenerator; +import java.nio.charset.StandardCharsets; import java.security.KeyStore; -import java.security.PrivateKey; -import java.security.PublicKey; -import java.security.Signature; -import java.security.spec.ECGenParameterSpec; import java.text.DecimalFormat; import java.text.NumberFormat; import java.util.ArrayList; @@ -57,12 +59,15 @@ import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; -import androidx.annotation.StringRes; -import androidx.annotation.Nullable; -import androidx.core.app.NotificationCompat; -import androidx.core.hardware.fingerprint.FingerprintManagerCompat; +import javax.crypto.Cipher; +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; +import javax.crypto.spec.GCMParameterSpec; + import dcrlibwallet.Dcrlibwallet; public class Utils { @@ -180,7 +185,7 @@ public static String getNetworkAddress(Context context) { } public static String calculateDays(long seconds, Context context) { - if(context == null){ + if (context == null) { return ""; } @@ -368,7 +373,7 @@ public static void copyToClipboard(Context ctx, String copyText, String successM } - public static void copyToClipboard(Context ctx, String copyText, @StringRes int successMessage){ + public static void copyToClipboard(Context ctx, String copyText, @StringRes int successMessage) { copyToClipboard(ctx, copyText, ctx.getString(successMessage)); } @@ -386,7 +391,7 @@ public static String readFromClipboard(Context context) { return ""; } - public static void showMessage(Context ctx, String message, int duration){ + public static void showMessage(Context ctx, String message, int duration) { LayoutInflater inflater = (LayoutInflater) ctx.getSystemService(Context.LAYOUT_INFLATER_SERVICE); View vi = inflater.inflate(R.layout.toast, null); TextView tv = vi.findViewById(android.R.id.message); @@ -577,77 +582,138 @@ public static void sendTransactionNotification(Context context, NotificationMana manager.notify(Constants.TRANSACTION_SUMMARY_ID, groupSummary); } - public static class Biometric { + static byte[] readFileToBytes(String path) throws Exception { + FileInputStream fin = new FileInputStream(path); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + byte[] buff = new byte[8192]; + int len; + while ((len = fin.read(buff)) != -1) { + out.write(buff, 0, len); + } - public static boolean isSupportBiometricPrompt(Context context) { - PackageManager packageManager = context.getPackageManager(); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - return packageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT); - } + fin.close(); - return false; + return out.toByteArray(); + } + + static void writeBytesToFile(byte[] output, String path) throws Exception { + File file = new File(path); + + FileOutputStream fout = new FileOutputStream(file); + ByteArrayInputStream bin = new ByteArrayInputStream(output); + + int len; + byte[] buff = new byte[8192]; + + while ((len = bin.read(buff)) != -1) { + fout.write(buff, 0, len); } - public static boolean isSupportFingerprint(Context context) { - PackageManager packageManager = context.getPackageManager(); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - return packageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT); + fout.flush(); + fout.close(); + bin.close(); + } + + public static class Biometric { + + static String getFilePath(Context context, String fileName) { + File path = new File(context.getFilesDir() + "/auth/"); + if (!path.exists()) { + path.mkdir(); } + return path.getAbsolutePath() + fileName; + } - return false; + static byte[] readFromFile(Context context, String fileName) throws Exception { + String path = Utils.Biometric.getFilePath(context, fileName); + return Utils.readFileToBytes(path); } - public static boolean isFingerprintEnrolled(Context context) { - FingerprintManagerCompat fingerprintManager = FingerprintManagerCompat.from(context); - return fingerprintManager.hasEnrolledFingerprints(); + static void saveToFile(Context context, String fileName, byte[] output) throws Exception { + String path = Utils.Biometric.getFilePath(context, fileName); + Utils.writeBytesToFile(output, path); } - @TargetApi(Build.VERSION_CODES.N) - public static KeyPair generateKeyPair(String keyName, boolean invalidatedByBiometricEnrollment) throws Exception { - KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore"); - KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec.Builder(keyName, - KeyProperties.PURPOSE_SIGN) - .setAlgorithmParameterSpec(new ECGenParameterSpec("secp256r1")) - .setDigests(KeyProperties.DIGEST_SHA256, - KeyProperties.DIGEST_SHA384, - KeyProperties.DIGEST_SHA512) - // Require the user to authenticate with a biometric to authorize every use of the key - .setUserAuthenticationRequired(true) - // Generated keys will be invalidated if the biometric templates are added more to user device - .setInvalidatedByBiometricEnrollment(invalidatedByBiometricEnrollment); + @TargetApi(Build.VERSION_CODES.M) + public static void savePassToKeystore(Context context, String pass, String alias) throws Exception { + final KeyGenerator keyGenerator = KeyGenerator + .getInstance(KeyProperties.KEY_ALGORITHM_AES, Constants.ANDROID_KEY_STORE); + + final KeyGenParameterSpec keyGenParameterSpec = new KeyGenParameterSpec.Builder(alias, + KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) + .setBlockModes(KeyProperties.BLOCK_MODE_GCM) + .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) + .build(); - keyPairGenerator.initialize(builder.build()); + keyGenerator.init(keyGenParameterSpec); + final SecretKey secretKey = keyGenerator.generateKey(); - return keyPairGenerator.generateKeyPair(); + final Cipher cipher = Cipher.getInstance(Constants.TRANSFORMATION); + cipher.init(Cipher.ENCRYPT_MODE, secretKey); + + byte[] iv = cipher.getIV(); + saveToFile(context, Constants.ENCRYPTION_IV, iv); + + byte[] encryption = cipher.doFinal(pass.getBytes(StandardCharsets.UTF_8)); + saveToFile(context, Constants.ENCRYPTION_DATA, encryption); } - @Nullable - public static KeyPair getKeyPair(String keyName) throws Exception { - KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore"); + @TargetApi(Build.VERSION_CODES.KITKAT) + public static String getPassFromKeystore(Context context, String alias) throws Exception { + + byte[] encryptionIv = readFromFile(context, Constants.ENCRYPTION_IV); + byte[] encryptedData = readFromFile(context, Constants.ENCRYPTION_DATA); + + KeyStore keyStore = KeyStore.getInstance(Constants.ANDROID_KEY_STORE); keyStore.load(null); - if (keyStore.containsAlias(keyName)) { - // Get public key - PublicKey publicKey = keyStore.getCertificate(keyName).getPublicKey(); - // Get private key - PrivateKey privateKey = (PrivateKey) keyStore.getKey(keyName, null); - // Return a key pair - return new KeyPair(publicKey, privateKey); - } - return null; + final KeyStore.SecretKeyEntry secretKeyEntry = (KeyStore.SecretKeyEntry) keyStore + .getEntry(alias, null); + + final SecretKey secretKey = secretKeyEntry.getSecretKey(); + final Cipher cipher = Cipher.getInstance(Constants.TRANSFORMATION); + final GCMParameterSpec spec = new GCMParameterSpec(128, encryptionIv); + cipher.init(Cipher.DECRYPT_MODE, secretKey, spec); + + final byte[] decodedData = cipher.doFinal(encryptedData); + + return new String(decodedData, StandardCharsets.UTF_8); } - @Nullable - public static Signature initSignature(String keyName) throws Exception { - KeyPair keyPair = getKeyPair(keyName); + public static boolean isFingerprintEnrolled(Context context) { + FingerprintManagerCompat fingerprintManager = FingerprintManagerCompat.from(context); + return fingerprintManager.hasEnrolledFingerprints(); + } + + public static boolean displayBiometricPrompt(FragmentActivity activity, BiometricPrompt.AuthenticationCallback callback) { + + if (Utils.Biometric.isFingerprintEnrolled(activity)) { + Executor executor = Executors.newSingleThreadExecutor(); + BiometricPrompt biometricPrompt = new BiometricPrompt(activity, executor, callback); + BiometricPrompt.PromptInfo promptInfo = new BiometricPrompt.PromptInfo.Builder() + .setTitle(activity.getString(R.string.authentication_required)) + .setNegativeButtonText(activity.getString(R.string.cancel)) + .build(); - if (keyPair != null) { - Signature signature = Signature.getInstance("SHA256withECDSA"); - signature.initSign(keyPair.getPrivate()); - return signature; + biometricPrompt.authenticate(promptInfo); + return true; } - return null; + return false; + } + + public static String translateError(Context context, int errorCode) { + String message = null; + switch (errorCode) { + case BiometricConstants.ERROR_LOCKOUT: + message = context.getString(R.string.biometric_lockout_error); + break; + case BiometricConstants.ERROR_LOCKOUT_PERMANENT: + message = context.getString(R.string.biometric_permanent_lockout_error); + break; + } + + return message; } } } \ No newline at end of file diff --git a/app/src/main/res/layout/password_dialog.xml b/app/src/main/res/layout/password_dialog.xml new file mode 100644 index 000000000..33ba10a51 --- /dev/null +++ b/app/src/main/res/layout/password_dialog.xml @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9f6a0450e..e56588e30 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -410,6 +410,20 @@ + + + Off + Fingerprint + Fingerprint + Pass + + + + off + fingerprint + fingerprint_pass + + + Recent Activity Show All Transactions @@ -567,4 +581,8 @@ %1$s DCR (external) %1$s DCR (%2$s) + Too many attempts. Try again later. + Biometric disabled. Unlock your device with a strong authentication (PIN/Pattern/Password) + Signing… + Enroll at least one fingerprint \ No newline at end of file diff --git a/app/src/main/res/xml/pref_main.xml b/app/src/main/res/xml/pref_main.xml index 820c469bd..fe9cf0730 100644 --- a/app/src/main/res/xml/pref_main.xml +++ b/app/src/main/res/xml/pref_main.xml @@ -23,10 +23,13 @@ android:summary="@string/required_to_enter_app" android:title="@string/change_startup_pin_password" app:iconSpaceReserved="false" /> -