mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
fix: Biometric error prompts #2081
This commit is contained in:
@@ -32,7 +32,6 @@ import android.view.MenuItem
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.Button
|
import android.widget.Button
|
||||||
import android.widget.CompoundButton
|
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.result.ActivityResultLauncher
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
@@ -43,6 +42,9 @@ import androidx.appcompat.widget.Toolbar
|
|||||||
import androidx.biometric.BiometricManager
|
import androidx.biometric.BiometricManager
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
import androidx.fragment.app.commit
|
import androidx.fragment.app.commit
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.dialogs.DuplicateUuidDialog
|
import com.kunzisoft.keepass.activities.dialogs.DuplicateUuidDialog
|
||||||
@@ -81,10 +83,11 @@ import com.kunzisoft.keepass.view.asError
|
|||||||
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
|
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
|
||||||
import com.kunzisoft.keepass.viewmodels.AdvancedUnlockViewModel
|
import com.kunzisoft.keepass.viewmodels.AdvancedUnlockViewModel
|
||||||
import com.kunzisoft.keepass.viewmodels.DatabaseFileViewModel
|
import com.kunzisoft.keepass.viewmodels.DatabaseFileViewModel
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
|
|
||||||
|
|
||||||
class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderListener {
|
class MainCredentialActivity : DatabaseModeActivity() {
|
||||||
|
|
||||||
// Views
|
// Views
|
||||||
private var toolbar: Toolbar? = null
|
private var toolbar: Toolbar? = null
|
||||||
@@ -166,21 +169,13 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Listen password checkbox to init advanced unlock and confirmation button
|
// Listen password checkbox to init advanced unlock and confirmation button
|
||||||
mainCredentialView?.onPasswordChecked =
|
mainCredentialView?.onConditionToStoreCredentialChanged = { credentialStorage, verified ->
|
||||||
CompoundButton.OnCheckedChangeListener { _, _ ->
|
mAdvancedUnlockViewModel.checkUnlockAvailability(
|
||||||
mAdvancedUnlockViewModel.checkUnlockAvailability()
|
conditionToStoreCredentialVerified = verified
|
||||||
enableConfirmationButton()
|
)
|
||||||
}
|
// TODO Async by ViewModel
|
||||||
mainCredentialView?.onKeyFileChecked =
|
enableConfirmationButton()
|
||||||
CompoundButton.OnCheckedChangeListener { _, _ ->
|
}
|
||||||
// TODO mAdvancedUnlockViewModel.checkUnlockAvailability()
|
|
||||||
enableConfirmationButton()
|
|
||||||
}
|
|
||||||
mainCredentialView?.onHardwareKeyChecked =
|
|
||||||
CompoundButton.OnCheckedChangeListener { _, _ ->
|
|
||||||
// TODO mAdvancedUnlockViewModel.checkUnlockAvailability()
|
|
||||||
enableConfirmationButton()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Observe if default database
|
// Observe if default database
|
||||||
mDatabaseFileViewModel.isDefaultDatabase.observe(this) { isDefaultDatabase ->
|
mDatabaseFileViewModel.isDefaultDatabase.observe(this) { isDefaultDatabase ->
|
||||||
@@ -228,6 +223,27 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
|||||||
|
|
||||||
onDatabaseFileLoaded(databaseFile?.databaseUri, keyFileUri, hardwareKey)
|
onDatabaseFileLoaded(databaseFile?.databaseUri, keyFileUri, hardwareKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
|
mAdvancedUnlockViewModel.uiState.collect { uiState ->
|
||||||
|
// New value received
|
||||||
|
if (uiState.isCredentialRequired) {
|
||||||
|
mAdvancedUnlockViewModel.provideCredentialForEncryption(
|
||||||
|
getCredentialForEncryption()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
uiState.cipherEncryptDatabase?.let { cipherEncryptDatabase ->
|
||||||
|
onCredentialEncrypted(cipherEncryptDatabase)
|
||||||
|
mAdvancedUnlockViewModel.consumeCredentialEncrypted()
|
||||||
|
}
|
||||||
|
uiState.cipherDecryptDatabase?.let { cipherDecryptDatabase ->
|
||||||
|
onCredentialDecrypted(cipherDecryptDatabase)
|
||||||
|
mAdvancedUnlockViewModel.consumeCredentialDecrypted()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
@@ -400,23 +416,6 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun retrieveCredentialForEncryption(): ByteArray {
|
|
||||||
return mainCredentialView?.retrieveCredentialForStorage(credentialStorageListener)
|
|
||||||
?: byteArrayOf()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun conditionToStoreCredential(): Boolean {
|
|
||||||
return mainCredentialView?.conditionToStoreCredential() == true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCredentialEncrypted(cipherEncryptDatabase: CipherEncryptDatabase) {
|
|
||||||
// Load the database if password is registered with biometric
|
|
||||||
loadDatabase(mDatabaseFileUri,
|
|
||||||
mainCredentialView?.getMainCredential(),
|
|
||||||
cipherEncryptDatabase
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val credentialStorageListener = object: MainCredentialView.CredentialStorageListener {
|
private val credentialStorageListener = object: MainCredentialView.CredentialStorageListener {
|
||||||
override fun passwordToStore(password: String?): ByteArray? {
|
override fun passwordToStore(password: String?): ByteArray? {
|
||||||
return password?.toByteArray()
|
return password?.toByteArray()
|
||||||
@@ -433,7 +432,20 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCredentialDecrypted(cipherDecryptDatabase: CipherDecryptDatabase) {
|
private fun getCredentialForEncryption(): ByteArray {
|
||||||
|
return mainCredentialView?.retrieveCredentialForStorage(credentialStorageListener)
|
||||||
|
?: byteArrayOf()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onCredentialEncrypted(cipherEncryptDatabase: CipherEncryptDatabase) {
|
||||||
|
// Load the database if password is registered with biometric
|
||||||
|
loadDatabase(mDatabaseFileUri,
|
||||||
|
mainCredentialView?.getMainCredential(),
|
||||||
|
cipherEncryptDatabase
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onCredentialDecrypted(cipherDecryptDatabase: CipherDecryptDatabase) {
|
||||||
// Load the database if password is retrieve from biometric
|
// Load the database if password is retrieve from biometric
|
||||||
// Retrieve from biometric
|
// Retrieve from biometric
|
||||||
val mainCredential = mainCredentialView?.getMainCredential() ?: MainCredential()
|
val mainCredential = mainCredentialView?.getMainCredential() ?: MainCredential()
|
||||||
|
|||||||
@@ -35,7 +35,9 @@ import androidx.biometric.BiometricPrompt
|
|||||||
import androidx.core.view.MenuProvider
|
import androidx.core.view.MenuProvider
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
|
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
|
||||||
import com.kunzisoft.keepass.database.exception.UnknownDatabaseLocationException
|
import com.kunzisoft.keepass.database.exception.UnknownDatabaseLocationException
|
||||||
@@ -52,8 +54,6 @@ import kotlinx.coroutines.launch
|
|||||||
|
|
||||||
class AdvancedUnlockFragment: Fragment(), AdvancedUnlockManager.AdvancedUnlockCallback {
|
class AdvancedUnlockFragment: Fragment(), AdvancedUnlockManager.AdvancedUnlockCallback {
|
||||||
|
|
||||||
private var mBuilderListener: BuilderListener? = null
|
|
||||||
|
|
||||||
private var mAdvancedUnlockEnabled = false
|
private var mAdvancedUnlockEnabled = false
|
||||||
private var mAutoOpenPromptEnabled = false
|
private var mAutoOpenPromptEnabled = false
|
||||||
|
|
||||||
@@ -84,6 +84,8 @@ class AdvancedUnlockFragment: Fragment(), AdvancedUnlockManager.AdvancedUnlockCa
|
|||||||
// Only keep connection when we request a device credential activity
|
// Only keep connection when we request a device credential activity
|
||||||
private var keepConnection = false
|
private var keepConnection = false
|
||||||
|
|
||||||
|
private var isConditionToStoreCredentialVerified = false
|
||||||
|
|
||||||
private var mDeviceCredentialResultLauncher = registerForActivityResult(
|
private var mDeviceCredentialResultLauncher = registerForActivityResult(
|
||||||
ActivityResultContracts.StartActivityForResult()
|
ActivityResultContracts.StartActivityForResult()
|
||||||
) { result ->
|
) { result ->
|
||||||
@@ -120,14 +122,6 @@ class AdvancedUnlockFragment: Fragment(), AdvancedUnlockManager.AdvancedUnlockCa
|
|||||||
|
|
||||||
mAdvancedUnlockEnabled = PreferencesUtil.isAdvancedUnlockEnable(context)
|
mAdvancedUnlockEnabled = PreferencesUtil.isAdvancedUnlockEnable(context)
|
||||||
mAutoOpenPromptEnabled = PreferencesUtil.isAdvancedUnlockPromptAutoOpenEnable(context)
|
mAutoOpenPromptEnabled = PreferencesUtil.isAdvancedUnlockPromptAutoOpenEnable(context)
|
||||||
try {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
||||||
mBuilderListener = context as BuilderListener
|
|
||||||
}
|
|
||||||
} catch (e: ClassCastException) {
|
|
||||||
throw ClassCastException(context.toString()
|
|
||||||
+ " must implement " + BuilderListener::class.java.name)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
@@ -138,11 +132,6 @@ class AdvancedUnlockFragment: Fragment(), AdvancedUnlockManager.AdvancedUnlockCa
|
|||||||
mAdvancedUnlockViewModel.onInitAdvancedUnlockModeRequested.observe(this) {
|
mAdvancedUnlockViewModel.onInitAdvancedUnlockModeRequested.observe(this) {
|
||||||
initAdvancedUnlockMode()
|
initAdvancedUnlockMode()
|
||||||
}
|
}
|
||||||
|
|
||||||
mAdvancedUnlockViewModel.onUnlockAvailabilityCheckRequested.observe(this) {
|
|
||||||
checkUnlockAvailability()
|
|
||||||
}
|
|
||||||
|
|
||||||
mAdvancedUnlockViewModel.onDatabaseFileLoaded.observe(this) {
|
mAdvancedUnlockViewModel.onDatabaseFileLoaded.observe(this) {
|
||||||
onDatabaseLoaded(it)
|
onDatabaseLoaded(it)
|
||||||
}
|
}
|
||||||
@@ -162,6 +151,27 @@ class AdvancedUnlockFragment: Fragment(), AdvancedUnlockManager.AdvancedUnlockCa
|
|||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
activity?.addMenuProvider(menuProvider, viewLifecycleOwner)
|
activity?.addMenuProvider(menuProvider, viewLifecycleOwner)
|
||||||
|
|
||||||
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
|
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
|
mAdvancedUnlockViewModel.uiState.collect { uiState ->
|
||||||
|
// New credential value received
|
||||||
|
uiState.credential?.let {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
advancedUnlockManager?.encryptData(uiState.credential)
|
||||||
|
}
|
||||||
|
mAdvancedUnlockViewModel.consumeCredentialForEncryption()
|
||||||
|
}
|
||||||
|
// Condition to store credential verified
|
||||||
|
isConditionToStoreCredentialVerified = uiState.isConditionToStoreCredentialVerified
|
||||||
|
// Check unlock availability
|
||||||
|
if (uiState.onUnlockAvailabilityCheckRequested) {
|
||||||
|
checkUnlockAvailability()
|
||||||
|
mAdvancedUnlockViewModel.consumeCheckUnlockAvailability()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
@@ -250,7 +260,7 @@ class AdvancedUnlockFragment: Fragment(), AdvancedUnlockManager.AdvancedUnlockCa
|
|||||||
if (advancedUnlockManager?.isKeyManagerInitialized != true) {
|
if (advancedUnlockManager?.isKeyManagerInitialized != true) {
|
||||||
toggleMode(Mode.KEY_MANAGER_UNAVAILABLE)
|
toggleMode(Mode.KEY_MANAGER_UNAVAILABLE)
|
||||||
} else {
|
} else {
|
||||||
if (mBuilderListener?.conditionToStoreCredential() == true) {
|
if (isConditionToStoreCredentialVerified) {
|
||||||
// listen for encryption
|
// listen for encryption
|
||||||
toggleMode(Mode.STORE_CREDENTIAL)
|
toggleMode(Mode.STORE_CREDENTIAL)
|
||||||
} else {
|
} else {
|
||||||
@@ -261,8 +271,13 @@ class AdvancedUnlockFragment: Fragment(), AdvancedUnlockManager.AdvancedUnlockCa
|
|||||||
// listen for decryption
|
// listen for decryption
|
||||||
Mode.EXTRACT_CREDENTIAL
|
Mode.EXTRACT_CREDENTIAL
|
||||||
} else {
|
} else {
|
||||||
// wait for typing
|
if (isConditionToStoreCredentialVerified) {
|
||||||
Mode.WAIT_CREDENTIAL
|
// if condition OK, key manager in error
|
||||||
|
Mode.KEY_MANAGER_UNAVAILABLE
|
||||||
|
} else {
|
||||||
|
// wait for typing
|
||||||
|
Mode.WAIT_CREDENTIAL
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -523,9 +538,7 @@ class AdvancedUnlockFragment: Fragment(), AdvancedUnlockManager.AdvancedUnlockCa
|
|||||||
}
|
}
|
||||||
Mode.STORE_CREDENTIAL -> {
|
Mode.STORE_CREDENTIAL -> {
|
||||||
// newly store the entered password in encrypted way
|
// newly store the entered password in encrypted way
|
||||||
mBuilderListener?.retrieveCredentialForEncryption()?.let { credential ->
|
mAdvancedUnlockViewModel.retrieveCredentialForEncryption()
|
||||||
advancedUnlockManager?.encryptData(credential)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Mode.EXTRACT_CREDENTIAL -> {
|
Mode.EXTRACT_CREDENTIAL -> {
|
||||||
// retrieve the encrypted value from preferences
|
// retrieve the encrypted value from preferences
|
||||||
@@ -545,7 +558,7 @@ class AdvancedUnlockFragment: Fragment(), AdvancedUnlockManager.AdvancedUnlockCa
|
|||||||
|
|
||||||
override fun handleEncryptedResult(encryptedValue: ByteArray, ivSpec: ByteArray) {
|
override fun handleEncryptedResult(encryptedValue: ByteArray, ivSpec: ByteArray) {
|
||||||
databaseFileUri?.let { databaseUri ->
|
databaseFileUri?.let { databaseUri ->
|
||||||
mBuilderListener?.onCredentialEncrypted(
|
mAdvancedUnlockViewModel.onCredentialEncrypted(
|
||||||
CipherEncryptDatabase().apply {
|
CipherEncryptDatabase().apply {
|
||||||
this.databaseUri = databaseUri
|
this.databaseUri = databaseUri
|
||||||
this.credentialStorage = credentialDatabaseStorage
|
this.credentialStorage = credentialDatabaseStorage
|
||||||
@@ -559,7 +572,7 @@ class AdvancedUnlockFragment: Fragment(), AdvancedUnlockManager.AdvancedUnlockCa
|
|||||||
override fun handleDecryptedResult(decryptedValue: ByteArray) {
|
override fun handleDecryptedResult(decryptedValue: ByteArray) {
|
||||||
// Load database directly with password retrieve
|
// Load database directly with password retrieve
|
||||||
databaseFileUri?.let { databaseUri ->
|
databaseFileUri?.let { databaseUri ->
|
||||||
mBuilderListener?.onCredentialDecrypted(
|
mAdvancedUnlockViewModel.onCredentialDecrypted(
|
||||||
CipherDecryptDatabase().apply {
|
CipherDecryptDatabase().apply {
|
||||||
this.databaseUri = databaseUri
|
this.databaseUri = databaseUri
|
||||||
this.credentialStorage = credentialDatabaseStorage
|
this.credentialStorage = credentialDatabaseStorage
|
||||||
@@ -630,13 +643,6 @@ class AdvancedUnlockFragment: Fragment(), AdvancedUnlockManager.AdvancedUnlockCa
|
|||||||
EXTRACT_CREDENTIAL
|
EXTRACT_CREDENTIAL
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BuilderListener {
|
|
||||||
fun retrieveCredentialForEncryption(): ByteArray
|
|
||||||
fun conditionToStoreCredential(): Boolean
|
|
||||||
fun onCredentialEncrypted(cipherEncryptDatabase: CipherEncryptDatabase)
|
|
||||||
fun onCredentialDecrypted(cipherDecryptDatabase: CipherDecryptDatabase)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
if (!keepConnection) {
|
if (!keepConnection) {
|
||||||
@@ -645,13 +651,11 @@ class AdvancedUnlockFragment: Fragment(), AdvancedUnlockManager.AdvancedUnlockCa
|
|||||||
advancedUnlockManager = null
|
advancedUnlockManager = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
super.onPause()
|
super.onPause()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
mAdvancedUnlockInfoView = null
|
mAdvancedUnlockInfoView = null
|
||||||
|
|
||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -659,20 +663,12 @@ class AdvancedUnlockFragment: Fragment(), AdvancedUnlockManager.AdvancedUnlockCa
|
|||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
disconnect()
|
disconnect()
|
||||||
advancedUnlockManager = null
|
advancedUnlockManager = null
|
||||||
mBuilderListener = null
|
|
||||||
}
|
}
|
||||||
|
mAdvancedUnlockViewModel.deleteData()
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDetach() {
|
|
||||||
mBuilderListener = null
|
|
||||||
|
|
||||||
super.onDetach()
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
private val TAG = AdvancedUnlockFragment::class.java.name
|
private val TAG = AdvancedUnlockFragment::class.java.name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -53,9 +53,7 @@ class MainCredentialView @JvmOverloads constructor(context: Context,
|
|||||||
private var checkboxHardwareView: CompoundButton
|
private var checkboxHardwareView: CompoundButton
|
||||||
private var hardwareKeySelectionView: HardwareKeySelectionView
|
private var hardwareKeySelectionView: HardwareKeySelectionView
|
||||||
|
|
||||||
var onPasswordChecked: (CompoundButton.OnCheckedChangeListener)? = null
|
var onConditionToStoreCredentialChanged: ((CredentialStorage, verified: Boolean) -> Unit)? = null
|
||||||
var onKeyFileChecked: (CompoundButton.OnCheckedChangeListener)? = null
|
|
||||||
var onHardwareKeyChecked: (CompoundButton.OnCheckedChangeListener)? = null
|
|
||||||
var onValidateListener: (() -> Unit)? = null
|
var onValidateListener: (() -> Unit)? = null
|
||||||
|
|
||||||
private var mCredentialStorage: CredentialStorage = CredentialStorage.PASSWORD
|
private var mCredentialStorage: CredentialStorage = CredentialStorage.PASSWORD
|
||||||
@@ -104,7 +102,10 @@ class MainCredentialView @JvmOverloads constructor(context: Context,
|
|||||||
}
|
}
|
||||||
|
|
||||||
checkboxPasswordView.setOnCheckedChangeListener { view, checked ->
|
checkboxPasswordView.setOnCheckedChangeListener { view, checked ->
|
||||||
onPasswordChecked?.onCheckedChanged(view, checked)
|
onConditionToStoreCredentialChanged?.invoke(
|
||||||
|
mCredentialStorage,
|
||||||
|
conditionToStoreCredential()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
checkboxKeyFileView.setOnCheckedChangeListener { view, checked ->
|
checkboxKeyFileView.setOnCheckedChangeListener { view, checked ->
|
||||||
if (checked) {
|
if (checked) {
|
||||||
@@ -112,7 +113,10 @@ class MainCredentialView @JvmOverloads constructor(context: Context,
|
|||||||
checkboxKeyFileView.isChecked = false
|
checkboxKeyFileView.isChecked = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
onKeyFileChecked?.onCheckedChanged(view, checked)
|
onConditionToStoreCredentialChanged?.invoke(
|
||||||
|
mCredentialStorage,
|
||||||
|
conditionToStoreCredential()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
checkboxHardwareView.setOnCheckedChangeListener { view, checked ->
|
checkboxHardwareView.setOnCheckedChangeListener { view, checked ->
|
||||||
if (checked) {
|
if (checked) {
|
||||||
@@ -120,7 +124,10 @@ class MainCredentialView @JvmOverloads constructor(context: Context,
|
|||||||
checkboxHardwareView.isChecked = false
|
checkboxHardwareView.isChecked = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
onHardwareKeyChecked?.onCheckedChanged(view, checked)
|
onConditionToStoreCredentialChanged?.invoke(
|
||||||
|
mCredentialStorage,
|
||||||
|
conditionToStoreCredential()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
hardwareKeySelectionView.selectionListener = { _ ->
|
hardwareKeySelectionView.selectionListener = { _ ->
|
||||||
|
|||||||
@@ -3,18 +3,23 @@ package com.kunzisoft.keepass.viewmodels
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
import com.kunzisoft.keepass.model.CipherDecryptDatabase
|
||||||
|
import com.kunzisoft.keepass.model.CipherEncryptDatabase
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
|
||||||
class AdvancedUnlockViewModel : ViewModel() {
|
class AdvancedUnlockViewModel : ViewModel() {
|
||||||
|
|
||||||
var allowAutoOpenBiometricPrompt : Boolean = true
|
var allowAutoOpenBiometricPrompt : Boolean = true
|
||||||
var deviceCredentialAuthSucceeded: Boolean? = null
|
var deviceCredentialAuthSucceeded: Boolean? = null
|
||||||
|
|
||||||
|
private val _uiState = MutableStateFlow(DeviceUnlockUiStates())
|
||||||
|
val uiState: StateFlow<DeviceUnlockUiStates> = _uiState
|
||||||
|
|
||||||
val onInitAdvancedUnlockModeRequested : LiveData<Void?> get() = _onInitAdvancedUnlockModeRequested
|
val onInitAdvancedUnlockModeRequested : LiveData<Void?> get() = _onInitAdvancedUnlockModeRequested
|
||||||
private val _onInitAdvancedUnlockModeRequested = SingleLiveEvent<Void?>()
|
private val _onInitAdvancedUnlockModeRequested = SingleLiveEvent<Void?>()
|
||||||
|
|
||||||
val onUnlockAvailabilityCheckRequested : LiveData<Void?> get() = _onUnlockAvailabilityCheckRequested
|
|
||||||
private val _onUnlockAvailabilityCheckRequested = SingleLiveEvent<Void?>()
|
|
||||||
|
|
||||||
val onDatabaseFileLoaded : LiveData<Uri?> get() = _onDatabaseFileLoaded
|
val onDatabaseFileLoaded : LiveData<Uri?> get() = _onDatabaseFileLoaded
|
||||||
private val _onDatabaseFileLoaded = SingleLiveEvent<Uri?>()
|
private val _onDatabaseFileLoaded = SingleLiveEvent<Uri?>()
|
||||||
|
|
||||||
@@ -22,11 +27,135 @@ class AdvancedUnlockViewModel : ViewModel() {
|
|||||||
_onInitAdvancedUnlockModeRequested.call()
|
_onInitAdvancedUnlockModeRequested.call()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun checkUnlockAvailability() {
|
fun checkUnlockAvailability(conditionToStoreCredentialVerified: Boolean) {
|
||||||
_onUnlockAvailabilityCheckRequested.call()
|
_uiState.update { currentState ->
|
||||||
|
currentState.copy(
|
||||||
|
onUnlockAvailabilityCheckRequested = true,
|
||||||
|
isConditionToStoreCredentialVerified = conditionToStoreCredentialVerified
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun consumeCheckUnlockAvailability() {
|
||||||
|
_uiState.update { currentState ->
|
||||||
|
currentState.copy(
|
||||||
|
onUnlockAvailabilityCheckRequested = false
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun databaseFileLoaded(databaseUri: Uri?) {
|
fun databaseFileLoaded(databaseUri: Uri?) {
|
||||||
_onDatabaseFileLoaded.value = databaseUri
|
_onDatabaseFileLoaded.value = databaseUri
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun retrieveCredentialForEncryption() {
|
||||||
|
_uiState.update { currentState ->
|
||||||
|
currentState.copy(
|
||||||
|
isCredentialRequired = true,
|
||||||
|
credential = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun provideCredentialForEncryption(credential: ByteArray) {
|
||||||
|
_uiState.update { currentState ->
|
||||||
|
currentState.copy(
|
||||||
|
isCredentialRequired = false,
|
||||||
|
credential = credential
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun consumeCredentialForEncryption() {
|
||||||
|
_uiState.update { currentState ->
|
||||||
|
currentState.copy(
|
||||||
|
isCredentialRequired = false,
|
||||||
|
credential = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onCredentialEncrypted(cipherEncryptDatabase: CipherEncryptDatabase) {
|
||||||
|
_uiState.update { currentState ->
|
||||||
|
currentState.copy(
|
||||||
|
cipherEncryptDatabase = cipherEncryptDatabase
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun consumeCredentialEncrypted() {
|
||||||
|
_uiState.update { currentState ->
|
||||||
|
currentState.copy(
|
||||||
|
cipherEncryptDatabase = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onCredentialDecrypted(cipherDecryptDatabase: CipherDecryptDatabase) {
|
||||||
|
_uiState.update { currentState ->
|
||||||
|
currentState.copy(
|
||||||
|
cipherDecryptDatabase = cipherDecryptDatabase
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun consumeCredentialDecrypted() {
|
||||||
|
_uiState.update { currentState ->
|
||||||
|
currentState.copy(
|
||||||
|
cipherDecryptDatabase = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteData() {
|
||||||
|
_uiState.update { currentState ->
|
||||||
|
currentState.copy(
|
||||||
|
initAdvancedUnlockMode = false,
|
||||||
|
databaseFileUri = null,
|
||||||
|
isCredentialRequired = false,
|
||||||
|
credential = null,
|
||||||
|
isConditionToStoreCredentialVerified = false,
|
||||||
|
onUnlockAvailabilityCheckRequested = false,
|
||||||
|
cipherEncryptDatabase = null,
|
||||||
|
cipherDecryptDatabase = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class DeviceUnlockUiStates(
|
||||||
|
val initAdvancedUnlockMode: Boolean = false,
|
||||||
|
val databaseFileUri: Uri? = null,
|
||||||
|
val isCredentialRequired: Boolean = false,
|
||||||
|
val credential: ByteArray? = null,
|
||||||
|
val isConditionToStoreCredentialVerified: Boolean = false,
|
||||||
|
val onUnlockAvailabilityCheckRequested: Boolean = false,
|
||||||
|
val cipherEncryptDatabase: CipherEncryptDatabase? = null,
|
||||||
|
val cipherDecryptDatabase: CipherDecryptDatabase? = null
|
||||||
|
) {
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
|
||||||
|
other as DeviceUnlockUiStates
|
||||||
|
|
||||||
|
if (isCredentialRequired != other.isCredentialRequired) return false
|
||||||
|
if (isConditionToStoreCredentialVerified != other.isConditionToStoreCredentialVerified) return false
|
||||||
|
if (onUnlockAvailabilityCheckRequested != other.onUnlockAvailabilityCheckRequested) return false
|
||||||
|
if (!credential.contentEquals(other.credential)) return false
|
||||||
|
if (cipherEncryptDatabase != other.cipherEncryptDatabase) return false
|
||||||
|
if (cipherDecryptDatabase != other.cipherDecryptDatabase) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = isCredentialRequired.hashCode()
|
||||||
|
result = 31 * result + isConditionToStoreCredentialVerified.hashCode()
|
||||||
|
result = 31 * result + onUnlockAvailabilityCheckRequested.hashCode()
|
||||||
|
result = 31 * result + (credential?.contentHashCode() ?: 0)
|
||||||
|
result = 31 * result + (cipherEncryptDatabase?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + (cipherDecryptDatabase?.hashCode() ?: 0)
|
||||||
|
return result
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user