fix: Add User Verification to database settings #2283

This commit is contained in:
J-Jamet
2025-12-01 21:01:08 +01:00
parent c754b6a049
commit 17d4c363ac
9 changed files with 142 additions and 20 deletions

View File

@@ -325,15 +325,15 @@ class EntryActivity : DatabaseLockActivity() {
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.RESUMED) {
mUserVerificationViewModel.uiState.collect { uIState ->
when (uIState) {
mUserVerificationViewModel.userVerificationState.collect { uVState ->
when (uVState) {
is UserVerificationViewModel.UIState.Loading -> {}
is UserVerificationViewModel.UIState.OnUserVerificationCanceled -> {
coordinatorLayout?.showError(uIState.error)
coordinatorLayout?.showError(uVState.error)
mUserVerificationViewModel.onUserVerificationReceived()
}
is UserVerificationViewModel.UIState.OnUserVerificationSucceeded -> {
editEntry(uIState.dataToVerify.database, uIState.dataToVerify.entryId)
editEntry(uVState.dataToVerify.database, uVState.dataToVerify.entryId)
mUserVerificationViewModel.onUserVerificationReceived()
}
}

View File

@@ -578,15 +578,15 @@ class GroupActivity : DatabaseLockActivity(),
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.RESUMED) {
mUserVerificationViewModel.uiState.collect { uIState ->
when (uIState) {
mUserVerificationViewModel.userVerificationState.collect { uVState ->
when (uVState) {
is UserVerificationViewModel.UIState.Loading -> {}
is UserVerificationViewModel.UIState.OnUserVerificationCanceled -> {
coordinatorLayout?.showError(uIState.error)
coordinatorLayout?.showError(uVState.error)
mUserVerificationViewModel.onUserVerificationReceived()
}
is UserVerificationViewModel.UIState.OnUserVerificationSucceeded -> {
editEntry(uIState.dataToVerify.database, uIState.dataToVerify.entryId)
editEntry(uVState.dataToVerify.database, uVState.dataToVerify.entryId)
mUserVerificationViewModel.onUserVerificationReceived()
}
}

View File

@@ -5,5 +5,6 @@ import com.kunzisoft.keepass.database.element.node.NodeId
data class UserVerificationData(
val database: ContextualDatabase? = null,
val entryId: NodeId<*>? = null
val entryId: NodeId<*>? = null,
val preferenceKey: String? = null
)

View File

@@ -9,6 +9,7 @@ import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL
import androidx.biometric.BiometricManager.BIOMETRIC_SUCCESS
import androidx.biometric.BiometricPrompt
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.CheckDatabaseCredentialDialogFragment
@@ -91,6 +92,17 @@ class UserVerificationHelper {
return this.passkey != null
}
fun Fragment.checkUserVerification(
userVerificationViewModel: UserVerificationViewModel,
dataToVerify: UserVerificationData
) {
if (context?.isAuthenticatorsAllowed() == true) {
activity?.showUserVerificationDeviceCredential(userVerificationViewModel, dataToVerify)
} else {
activity?.showUserVerificationDatabaseCredential(userVerificationViewModel, dataToVerify)
}
}
fun FragmentActivity.checkUserVerification(
userVerificationViewModel: UserVerificationViewModel,
dataToVerify: UserVerificationData

View File

@@ -175,7 +175,7 @@ class PasskeyLauncherActivity : DatabaseLockActivity() {
}
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.RESUMED) {
userVerificationViewModel.uiState.collect { uiState ->
userVerificationViewModel.userVerificationState.collect { uiState ->
when (uiState) {
is UserVerificationViewModel.UIState.Loading -> {}
is UserVerificationViewModel.UIState.OnUserVerificationSucceeded -> {

View File

@@ -42,6 +42,8 @@ import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.SetMainCredentialDialogFragment
import com.kunzisoft.keepass.activities.legacy.DatabaseRetrieval
import com.kunzisoft.keepass.activities.legacy.resetAppTimeoutWhenViewTouchedOrFocused
import com.kunzisoft.keepass.credentialprovider.UserVerificationData
import com.kunzisoft.keepass.credentialprovider.UserVerificationHelper.Companion.checkUserVerification
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
@@ -74,11 +76,16 @@ import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.utils.getParcelableCompat
import com.kunzisoft.keepass.utils.getSerializableCompat
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
import com.kunzisoft.keepass.viewmodels.SettingsViewModel
import com.kunzisoft.keepass.viewmodels.UserVerificationViewModel
import kotlinx.coroutines.launch
class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetrieval {
private val mSettingsViewModel: SettingsViewModel by activityViewModels()
private val mDatabaseViewModel: DatabaseViewModel by activityViewModels()
private val mUserVerificationViewModel: UserVerificationViewModel by activityViewModels()
private val mDatabase: ContextualDatabase?
get() = mDatabaseViewModel.database
private var mDatabaseReadOnly: Boolean = false
@@ -171,6 +178,45 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
}
}
}
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.RESUMED) {
mUserVerificationViewModel.userVerificationState.collect { state ->
when (state) {
is UserVerificationViewModel.UIState.Loading -> {}
is UserVerificationViewModel.UIState.OnUserVerificationCanceled -> {
mSettingsViewModel.showError(state.error)
mUserVerificationViewModel.onUserVerificationReceived()
}
is UserVerificationViewModel.UIState.OnUserVerificationSucceeded -> {
state.dataToVerify.database?.let { database ->
state.dataToVerify.preferenceKey?.let { preferenceKey ->
// Main Preferences
when (preferenceKey) {
// Master Key
getString(R.string.settings_database_change_credentials_key) -> {
SetMainCredentialDialogFragment
.getInstance(database.allowNoMasterKey)
.show(parentFragmentManager, "passwordDialog")
}
else -> {}
}
// TODO Settings in compose
@Suppress("DEPRECATION")
mSettingsViewModel.dialogFragment?.let { dialogFragment ->
dialogFragment.setTargetFragment(
this@NestedDatabaseSettingsFragment, 0
)
dialogFragment.show(parentFragmentManager, TAG_PREF_FRAGMENT)
}
mSettingsViewModel.dialogFragment = null
}
}
mUserVerificationViewModel.onUserVerificationReceived()
}
}
}
}
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@@ -325,7 +371,6 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
}
// Change the recycle bin group
recycleBinGroupPref?.setOnPreferenceClickListener {
true
}
// Recycle Bin group
@@ -431,11 +476,17 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
}
private fun onCreateDatabaseMasterKeyPreference(database: ContextualDatabase) {
findPreference<Preference>(getString(R.string.settings_database_change_credentials_key))?.apply {
val changeCredentialKey = getString(R.string.settings_database_change_credentials_key)
findPreference<Preference>(changeCredentialKey)?.apply {
isEnabled = if (!mDatabaseReadOnly) {
onPreferenceClickListener = Preference.OnPreferenceClickListener {
SetMainCredentialDialogFragment.getInstance(database.allowNoMasterKey)
.show(parentFragmentManager, "passwordDialog")
checkUserVerification(
mUserVerificationViewModel,
UserVerificationData(
database = database,
preferenceKey = changeCredentialKey
)
)
false
}
true
@@ -730,9 +781,14 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
}
if (dialogFragment != null && !mDatabaseReadOnly) {
@Suppress("DEPRECATION")
dialogFragment.setTargetFragment(this, 0)
dialogFragment.show(parentFragmentManager, TAG_PREF_FRAGMENT)
mSettingsViewModel.dialogFragment = dialogFragment
checkUserVerification(
mUserVerificationViewModel,
UserVerificationData(
database = mDatabase,
preferenceKey = preference.key
)
)
}
// Could not be handled here. Try with the super method.
else if (otherDialogFragment) {
@@ -742,7 +798,6 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
override fun onResume() {
super.onResume()
context?.let { context ->
mDatabaseAutoSaveEnabled = PreferencesUtil.isAutoSaveDatabaseEnabled(context)
}

View File

@@ -28,9 +28,13 @@ import android.view.MenuItem
import android.view.View
import android.widget.TextView
import android.widget.Toast
import androidx.activity.viewModels
import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.SetMainCredentialDialogFragment
@@ -41,6 +45,9 @@ import com.kunzisoft.keepass.database.MainCredential
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
import com.kunzisoft.keepass.view.showError
import com.kunzisoft.keepass.viewmodels.SettingsViewModel
import kotlinx.coroutines.launch
import org.joda.time.DateTime
import java.util.Properties
@@ -49,6 +56,8 @@ open class SettingsActivity
MainPreferenceFragment.Callback,
SetMainCredentialDialogFragment.AssignMainCredentialDialogListener {
private val mSettingsViewModel: SettingsViewModel by viewModels()
private var backupManager: BackupManager? = null
private var mExternalFileHelper: ExternalFileHelper? = null
@@ -118,7 +127,7 @@ open class SettingsActivity
if (savedInstanceState?.getString(TITLE_KEY).isNullOrEmpty())
toolbar?.setTitle(R.string.settings)
else
toolbar?.title = savedInstanceState?.getString(TITLE_KEY)
toolbar?.title = savedInstanceState.getString(TITLE_KEY)
setSupportActionBar(toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
@@ -145,6 +154,20 @@ open class SettingsActivity
// Eat state
intent.removeExtra(FRAGMENT_ARG)
}
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.RESUMED) {
mSettingsViewModel.settingsState.collect { settingsState ->
when (settingsState) {
is SettingsViewModel.SettingsState.Wait -> {}
is SettingsViewModel.SettingsState.ShowError -> {
coordinatorLayout?.showError(settingsState.error)
mSettingsViewModel.errorShown()
}
}
}
}
}
}
/**

View File

@@ -0,0 +1,31 @@
package com.kunzisoft.keepass.viewmodels
import android.app.Application
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.AndroidViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
class SettingsViewModel(application: Application): AndroidViewModel(application) {
private val mSettingsState = MutableStateFlow<SettingsState>(SettingsState.Wait)
val settingsState: StateFlow<SettingsState> = mSettingsState
var dialogFragment: DialogFragment? = null
fun showError(error: Throwable?) {
mSettingsState.value = SettingsState.ShowError(error)
}
fun errorShown() {
mSettingsState.value = SettingsState.Wait
}
sealed class SettingsState {
object Wait: SettingsState()
data class ShowError(
val error: Throwable? = null
): SettingsState()
}
}

View File

@@ -13,7 +13,7 @@ import kotlinx.coroutines.flow.StateFlow
class UserVerificationViewModel: ViewModel() {
private val mUiState = MutableStateFlow<UIState>(UIState.Loading)
val uiState: StateFlow<UIState> = mUiState
val userVerificationState: StateFlow<UIState> = mUiState
var dataToVerify: UserVerificationData = UserVerificationData()