Merge branch 'feature/BIOMETRIC_SECURITY' into develop

This commit is contained in:
J-Jamet
2020-10-07 13:38:51 +02:00
6 changed files with 117 additions and 61 deletions

View File

@@ -101,7 +101,7 @@ dependencies {
// WARNING: Bug with extra field
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.documentfile:documentfile:1.0.1'
implementation 'androidx.biometric:biometric:1.0.1'
implementation 'androidx.biometric:biometric:1.1.0-alpha02'
// Lifecycle - LiveData - ViewModel - Coroutines
implementation "androidx.core:core-ktx:1.3.1"
implementation 'androidx.fragment:fragment-ktx:1.2.5'

View File

@@ -52,6 +52,7 @@ import com.kunzisoft.keepass.activities.selection.SpecialModeActivity
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
import com.kunzisoft.keepass.autofill.AutofillHelper
import com.kunzisoft.keepass.biometric.AdvancedUnlockedManager
import com.kunzisoft.keepass.biometric.BiometricUnlockDatabaseHelper
import com.kunzisoft.keepass.database.action.ProgressDatabaseTaskProvider
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
@@ -68,7 +69,6 @@ import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.BACK_PREVIOUS_KEYBOARD_ACTION
import com.kunzisoft.keepass.utils.MenuUtil
import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.utils.closeDatabase
import com.kunzisoft.keepass.view.AdvancedUnlockInfoView
import com.kunzisoft.keepass.view.KeyFileSelectionView
import com.kunzisoft.keepass.view.asError
@@ -698,7 +698,7 @@ open class PasswordActivity : SpecialModeActivity() {
})
if (!readOnlyEducationPerformed) {
val biometricCanAuthenticate = BiometricManager.from(this).canAuthenticate()
val biometricCanAuthenticate = BiometricUnlockDatabaseHelper.canAuthenticate(this)
Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
&& PreferencesUtil.isBiometricUnlockEnable(applicationContext)
&& (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED || biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS)

View File

@@ -53,6 +53,10 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
private var biometricUnlockDatabaseHelper: BiometricUnlockDatabaseHelper? = null
private var biometricMode: Mode = Mode.UNAVAILABLE
// Only to fix multiple fingerprint menu #332
private var mAllowAdvancedUnlockMenu = false
private var mAddBiometricMenuInProgress = false
/**
* Manage setting to auto open biometric prompt
*/
@@ -84,13 +88,15 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
// biometric not supported (by API level or hardware) so keep option hidden
// or manually disable
val biometricCanAuthenticate = BiometricManager.from(context).canAuthenticate()
val biometricCanAuthenticate = BiometricUnlockDatabaseHelper.canAuthenticate(context)
allowOpenBiometricPrompt = true
if (!PreferencesUtil.isBiometricUnlockEnable(context)
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE) {
toggleMode(Mode.UNAVAILABLE)
} else if (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED){
toggleMode(Mode.BIOMETRIC_SECURITY_UPDATE_REQUIRED)
} else {
// biometric is available but not configured, show icon but in disabled state with some information
if (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED) {
@@ -114,7 +120,7 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
} else {
cipherDatabaseAction.containsCipherDatabase(databaseFileUri) { containsCipher ->
// biometric available but no stored password found yet for this DB so show info don't listen
toggleMode( if (containsCipher) {
toggleMode(if (containsCipher) {
// listen for decryption
Mode.EXTRACT_CREDENTIAL
} else {
@@ -156,10 +162,16 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
context.runOnUiThread {
when (biometricMode) {
Mode.UNAVAILABLE -> {}
Mode.BIOMETRIC_NOT_CONFIGURED -> {}
Mode.KEY_MANAGER_UNAVAILABLE -> {}
Mode.WAIT_CREDENTIAL -> {}
Mode.UNAVAILABLE -> {
}
Mode.BIOMETRIC_SECURITY_UPDATE_REQUIRED -> {
}
Mode.BIOMETRIC_NOT_CONFIGURED -> {
}
Mode.KEY_MANAGER_UNAVAILABLE -> {
}
Mode.WAIT_CREDENTIAL -> {
}
Mode.STORE_CREDENTIAL -> {
// newly store the entered password in encrypted way
biometricUnlockDatabaseHelper?.encryptData(passwordView?.text.toString())
@@ -183,6 +195,15 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
advancedUnlockInfoView?.setIconViewClickListener(false, null)
}
private fun initSecurityUpdateRequired() {
showFingerPrintViews(true)
setAdvancedUnlockedTitleView(R.string.biometric_security_update_required)
advancedUnlockInfoView?.setIconViewClickListener(false) {
context.startActivity(Intent(Settings.ACTION_SECURITY_SETTINGS))
}
}
private fun initNotConfigured() {
showFingerPrintViews(true)
setAdvancedUnlockedTitleView(R.string.configure_biometric)
@@ -196,7 +217,6 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
private fun initKeyManagerNotAvailable() {
showFingerPrintViews(true)
setAdvancedUnlockedTitleView(R.string.keystore_not_accessible)
setAdvancedUnlockedMessageView("")
advancedUnlockInfoView?.setIconViewClickListener(false) {
context.startActivity(Intent(Settings.ACTION_SECURITY_SETTINGS))
@@ -210,17 +230,25 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
advancedUnlockInfoView?.setIconViewClickListener(false) {
biometricAuthenticationCallback.onAuthenticationError(
BiometricConstants.ERROR_UNABLE_TO_PROCESS
, context.getString(R.string.credential_before_click_biometric_button))
BiometricConstants.ERROR_UNABLE_TO_PROCESS, context.getString(R.string.credential_before_click_biometric_button))
}
}
private fun openBiometricPrompt(biometricPrompt: BiometricPrompt?,
cryptoObject: BiometricPrompt.CryptoObject,
cryptoObject: BiometricPrompt.CryptoObject?,
promptInfo: BiometricPrompt.PromptInfo) {
context.runOnUiThread {
if (allowOpenBiometricPrompt)
biometricPrompt?.authenticate(promptInfo, cryptoObject)
if (allowOpenBiometricPrompt) {
if (biometricPrompt != null) {
if (cryptoObject != null) {
biometricPrompt.authenticate(promptInfo, cryptoObject)
} else {
setAdvancedUnlockedTitleView(R.string.crypto_object_not_initialized)
}
} else {
setAdvancedUnlockedTitleView(R.string.biometric_prompt_not_initialized)
}
}
}
}
@@ -230,14 +258,10 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
setAdvancedUnlockedMessageView("")
biometricUnlockDatabaseHelper?.initEncryptData { biometricPrompt, cryptoObject, promptInfo ->
cryptoObject?.let { crypto ->
// Set listener to open the biometric dialog and save credential
advancedUnlockInfoView?.setIconViewClickListener { _ ->
openBiometricPrompt(biometricPrompt, crypto, promptInfo)
}
// Set listener to open the biometric dialog and save credential
advancedUnlockInfoView?.setIconViewClickListener { _ ->
openBiometricPrompt(biometricPrompt, cryptoObject, promptInfo)
}
}
}
@@ -252,19 +276,16 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
it?.specParameters?.let { specs ->
biometricUnlockDatabaseHelper?.initDecryptData(specs) { biometricPrompt, cryptoObject, promptInfo ->
cryptoObject?.let { crypto ->
// Set listener to open the biometric dialog and check credential
advancedUnlockInfoView?.setIconViewClickListener { _ ->
openBiometricPrompt(biometricPrompt, crypto, promptInfo)
}
// Auto open the biometric prompt
if (isBiometricPromptAutoOpenEnable) {
isBiometricPromptAutoOpenEnable = false
openBiometricPrompt(biometricPrompt, crypto, promptInfo)
}
// Set listener to open the biometric dialog and check credential
advancedUnlockInfoView?.setIconViewClickListener { _ ->
openBiometricPrompt(biometricPrompt, cryptoObject, promptInfo)
}
// Auto open the biometric prompt
if (isBiometricPromptAutoOpenEnable) {
isBiometricPromptAutoOpenEnable = false
openBiometricPrompt(biometricPrompt, cryptoObject, promptInfo)
}
}
}
}
@@ -273,16 +294,32 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
@Synchronized
fun initBiometricMode() {
mAllowAdvancedUnlockMenu = false
when (biometricMode) {
Mode.UNAVAILABLE -> initNotAvailable()
Mode.BIOMETRIC_SECURITY_UPDATE_REQUIRED -> initSecurityUpdateRequired()
Mode.BIOMETRIC_NOT_CONFIGURED -> initNotConfigured()
Mode.KEY_MANAGER_UNAVAILABLE -> initKeyManagerNotAvailable()
Mode.WAIT_CREDENTIAL -> initWaitData()
Mode.STORE_CREDENTIAL -> initEncryptData()
Mode.EXTRACT_CREDENTIAL -> initDecryptData()
}
invalidateBiometricMenu()
}
private fun invalidateBiometricMenu() {
// Show fingerprint key deletion
context.invalidateOptionsMenu()
if (!mAddBiometricMenuInProgress) {
mAddBiometricMenuInProgress = true
cipherDatabaseAction.containsCipherDatabase(databaseFileUri) { containsCipher ->
mAllowAdvancedUnlockMenu = containsCipher
&& (biometricMode != Mode.UNAVAILABLE
&& biometricMode != Mode.KEY_MANAGER_UNAVAILABLE)
mAddBiometricMenuInProgress = false
context.invalidateOptionsMenu()
}
}
}
fun destroy() {
@@ -293,26 +330,19 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
checkboxPasswordView?.setOnCheckedChangeListener(onCheckedPasswordChangeListener)
}
// Only to fix multiple fingerprint menu #332
private var addBiometricMenuInProgress = false
fun inflateOptionsMenu(menuInflater: MenuInflater, menu: Menu) {
if (!addBiometricMenuInProgress) {
addBiometricMenuInProgress = true
cipherDatabaseAction.containsCipherDatabase(databaseFileUri) {
if ((biometricMode != Mode.UNAVAILABLE && biometricMode != Mode.BIOMETRIC_NOT_CONFIGURED)
&& it) {
menuInflater.inflate(R.menu.advanced_unlock, menu)
addBiometricMenuInProgress = false
}
}
}
if (mAllowAdvancedUnlockMenu)
menuInflater.inflate(R.menu.advanced_unlock, menu)
}
fun deleteEntryKey() {
allowOpenBiometricPrompt = false
advancedUnlockInfoView?.setIconViewClickListener(false, null)
biometricUnlockDatabaseHelper?.closeBiometricPrompt()
biometricUnlockDatabaseHelper?.deleteEntryKey()
cipherDatabaseAction.deleteByDatabaseUri(databaseFileUri)
biometricMode = Mode.BIOMETRIC_NOT_CONFIGURED
checkBiometricAvailability()
cipherDatabaseAction.deleteByDatabaseUri(databaseFileUri) {
checkBiometricAvailability()
}
}
override fun handleEncryptedResult(encryptedValue: String, ivSpec: String) {
@@ -359,7 +389,13 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
}
enum class Mode {
UNAVAILABLE, BIOMETRIC_NOT_CONFIGURED, KEY_MANAGER_UNAVAILABLE, WAIT_CREDENTIAL, STORE_CREDENTIAL, EXTRACT_CREDENTIAL
UNAVAILABLE,
BIOMETRIC_SECURITY_UPDATE_REQUIRED,
BIOMETRIC_NOT_CONFIGURED,
KEY_MANAGER_UNAVAILABLE,
WAIT_CREDENTIAL,
STORE_CREDENTIAL,
EXTRACT_CREDENTIAL
}
companion object {

View File

@@ -29,6 +29,9 @@ import android.util.Base64
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG
import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_WEAK
import androidx.biometric.BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE
import androidx.biometric.BiometricPrompt
import androidx.fragment.app.FragmentActivity
import com.kunzisoft.keepass.R
@@ -66,7 +69,7 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity) {
setDeviceCredentialAllowed(true)
else
*/
setNegativeButtonText(context.getString(android.R.string.cancel))
setNegativeButtonText(context.getString(android.R.string.cancel))
}.build()
private val promptInfoExtractCredential = BiometricPrompt.PromptInfo.Builder().apply {
@@ -78,8 +81,8 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity) {
if (keyguardManager?.isDeviceSecure == true)
setDeviceCredentialAllowed(true)
else
*/
setNegativeButtonText(context.getString(android.R.string.cancel))
*/
setNegativeButtonText(context.getString(android.R.string.cancel))
}.build()
val isKeyManagerInitialized: Boolean
@@ -91,7 +94,7 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity) {
}
init {
if (BiometricManager.from(context).canAuthenticate() != BiometricManager.BIOMETRIC_SUCCESS) {
if (canAuthenticate(context) != BiometricManager.BIOMETRIC_SUCCESS) {
// really not much to do when no fingerprint support found
isKeyManagerInit = false
} else {
@@ -158,7 +161,7 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity) {
fun initEncryptData(actionIfCypherInit
: (biometricPrompt: BiometricPrompt?,
cryptoObject: BiometricPrompt.CryptoObject?,
promptInfo: BiometricPrompt.PromptInfo)->Unit) {
promptInfo: BiometricPrompt.PromptInfo) -> Unit) {
if (!isKeyManagerInitialized) {
return
}
@@ -203,9 +206,9 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity) {
}
fun initDecryptData(ivSpecValue: String, actionIfCypherInit
: (biometricPrompt: BiometricPrompt?,
cryptoObject: BiometricPrompt.CryptoObject?,
promptInfo: BiometricPrompt.PromptInfo)->Unit) {
: (biometricPrompt: BiometricPrompt?,
cryptoObject: BiometricPrompt.CryptoObject?,
promptInfo: BiometricPrompt.PromptInfo) -> Unit) {
if (!isKeyManagerInitialized) {
return
}
@@ -295,6 +298,20 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity) {
private const val BIOMETRIC_BLOCKS_MODES = KeyProperties.BLOCK_MODE_CBC
private const val BIOMETRIC_ENCRYPTION_PADDING = KeyProperties.ENCRYPTION_PADDING_PKCS7
fun canAuthenticate(context: Context): Int {
return try {
BiometricManager.from(context).canAuthenticate(BIOMETRIC_STRONG)
} catch (e: Exception) {
Log.e(TAG, "Unable to authenticate with strong biometric.", e)
try {
BiometricManager.from(context).canAuthenticate(BIOMETRIC_WEAK)
} catch (e: Exception) {
Log.e(TAG, "Unable to authenticate with weak biometric.", e)
BIOMETRIC_ERROR_HW_UNAVAILABLE
}
}
}
/**
* Remove entry key in keystore
*/

View File

@@ -47,6 +47,7 @@ import com.kunzisoft.keepass.icons.IconPackChooser
import com.kunzisoft.keepass.settings.preference.IconPackListPreference
import com.kunzisoft.keepass.utils.UriUtil
class NestedAppSettingsFragment : NestedSettingsFragment() {
override fun onCreateScreenPreference(screen: Screen, savedInstanceState: Bundle?, rootKey: String?) {
@@ -211,7 +212,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
// < M solve verifyError exception
var biometricUnlockSupported = false
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val biometricCanAuthenticate = BiometricManager.from(activity).canAuthenticate()
val biometricCanAuthenticate = BiometricUnlockDatabaseHelper.canAuthenticate(activity)
biometricUnlockSupported = biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS
}
@@ -336,8 +337,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
activity?.let { activity ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
findPreference<SwitchPreference?>(getString(R.string.settings_autofill_enable_key))?.let {
autoFillEnablePreference ->
findPreference<SwitchPreference?>(getString(R.string.settings_autofill_enable_key))?.let { autoFillEnablePreference ->
val autofillManager = activity.getSystemService(AutofillManager::class.java)
autoFillEnablePreference.isChecked = autofillManager != null
&& autofillManager.hasEnabledAutofillServices()

View File

@@ -268,6 +268,7 @@
<string name="version_label">Version %1$s</string>
<string name="build_label">Build %1$s</string>
<string name="configure_biometric">Biometric prompt is supported, but not set up.</string>
<string name="biometric_security_update_required">Biometric security update required.</string>
<string name="keystore_not_accessible">The keystore is not properly initialized.</string>
<string name="open_biometric_prompt_unlock_database">Open the biometric prompt to unlock the database</string>
<string name="open_biometric_prompt_store_credential">Open the biometric prompt to store credentials</string>
@@ -280,6 +281,8 @@
<string name="biometric_not_recognized">Could not recognize biometric</string>
<string name="biometric_scanning_error">Biometric error: %1$s</string>
<string name="no_credentials_stored">This database does not have stored credential yet.</string>
<string name="biometric_prompt_not_initialized">Unable to initialize biometric prompt.</string>
<string name="crypto_object_not_initialized">Unable to retrieve crypto object.</string>
<string name="credential_before_click_biometric_button">Type in the password, and then click the \"Biometric\" button.</string>
<string name="database_history">History</string>
<string name="menu_appearance_settings">Appearance</string>