fix: Add User Verification for Entry Edition #2283

This commit is contained in:
J-Jamet
2025-11-29 13:04:01 +01:00
parent 7ed8a44168
commit d251788b1a
7 changed files with 164 additions and 78 deletions

View File

@@ -41,6 +41,9 @@ import androidx.core.graphics.BlendModeColorFilterCompat
import androidx.core.graphics.BlendModeCompat
import androidx.core.graphics.ColorUtils
import androidx.core.view.ViewCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.appbar.AppBarLayout
@@ -53,6 +56,8 @@ import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
import com.kunzisoft.keepass.adapters.TagsAdapter
import com.kunzisoft.keepass.credentialprovider.SpecialMode
import com.kunzisoft.keepass.credentialprovider.UserVerificationData
import com.kunzisoft.keepass.credentialprovider.UserVerificationHelper.Companion.askUserVerification
import com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.element.Attachment
@@ -79,6 +84,8 @@ import com.kunzisoft.keepass.view.hideByFading
import com.kunzisoft.keepass.view.setTransparentNavigationBar
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
import com.kunzisoft.keepass.viewmodels.EntryViewModel
import com.kunzisoft.keepass.viewmodels.UserVerificationViewModel
import kotlinx.coroutines.launch
import java.util.EnumSet
import java.util.UUID
@@ -100,14 +107,10 @@ class EntryActivity : DatabaseLockActivity() {
private var loadingView: ProgressBar? = null
private val mEntryViewModel: EntryViewModel by viewModels()
private val mUserVerificationViewModel: UserVerificationViewModel by viewModels()
private val mEntryActivityEducation = EntryActivityEducation(this)
private var mMainEntryId: NodeId<UUID>? = null
private var mHistoryPosition: Int = -1
private var mEntryIsHistory: Boolean = false
private var mEntryLoaded = false
private var mAttachmentFileBinderManager: AttachmentFileBinderManager? = null
private var mExternalFileHelper: ExternalFileHelper? = null
private var mAttachmentSelected: Attachment? = null
@@ -238,13 +241,9 @@ class EntryActivity : DatabaseLockActivity() {
mEntryViewModel.entryInfoHistory.observe(this) { entryInfoHistory ->
if (entryInfoHistory != null) {
this.mMainEntryId = entryInfoHistory.mainEntryId
// Manage history position
val historyPosition = entryInfoHistory.historyPosition
this.mHistoryPosition = historyPosition
val entryIsHistory = historyPosition > -1
this.mEntryIsHistory = entryIsHistory
// Assign history dedicated view
historyView?.visibility = if (entryIsHistory) View.VISIBLE else View.GONE
// TODO History badge
@@ -279,7 +278,6 @@ class EntryActivity : DatabaseLockActivity() {
mForegroundColor = if (showEntryColors) entryInfo.foregroundColor else null
loadingView?.hideByFading()
mEntryLoaded = true
} else {
finish()
}
@@ -322,6 +320,33 @@ class EntryActivity : DatabaseLockActivity() {
)
}
}
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.RESUMED) {
mUserVerificationViewModel.uiState.collect { uIState ->
when (uIState) {
is UserVerificationViewModel.UIState.Loading -> {}
is UserVerificationViewModel.UIState.OnUserVerificationCanceled -> {
mUserVerificationViewModel.onUserVerificationReceived()
}
is UserVerificationViewModel.UIState.OnUserVerificationSucceeded -> {
uIState.dataToVerify.database?.let { database ->
uIState.dataToVerify.entryId?.let { entryId ->
EntryEditActivity.launch(
activity = this@EntryActivity,
database = database,
registrationType = EntryEditActivity.RegistrationType.UPDATE,
nodeId = entryId,
activityResultLauncher = mEntryActivityResultLauncher
)
}
}
mUserVerificationViewModel.onUserVerificationReceived()
}
}
}
}
}
}
override fun finishActivityIfReloadRequested(): Boolean {
@@ -410,13 +435,13 @@ class EntryActivity : DatabaseLockActivity() {
override fun onCreateOptionsMenu(menu: Menu): Boolean {
super.onCreateOptionsMenu(menu)
if (mEntryLoaded) {
if (mEntryViewModel.entryLoaded) {
val inflater = menuInflater
inflater.inflate(R.menu.entry, menu)
inflater.inflate(R.menu.database, menu)
if (mEntryIsHistory && !mDatabaseReadOnly) {
if (mEntryViewModel.entryIsHistory && !mDatabaseReadOnly) {
inflater.inflate(R.menu.entry_history, menu)
}
@@ -429,7 +454,7 @@ class EntryActivity : DatabaseLockActivity() {
}
override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
if (mEntryIsHistory || mDatabaseReadOnly) {
if (mEntryViewModel.entryIsHistory || mDatabaseReadOnly) {
menu?.findItem(R.id.menu_save_database)?.isVisible = false
menu?.findItem(R.id.menu_merge_database)?.isVisible = false
menu?.findItem(R.id.menu_edit)?.isVisible = false
@@ -477,31 +502,27 @@ class EntryActivity : DatabaseLockActivity() {
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.menu_edit -> {
mDatabase?.let { database ->
mMainEntryId?.let { entryId ->
EntryEditActivity.launch(
activity = this,
database = database,
registrationType = EntryEditActivity.RegistrationType.UPDATE,
nodeId = entryId,
activityResultLauncher = mEntryActivityResultLauncher
)
}
}
askUserVerification(
userVerificationViewModel = mUserVerificationViewModel,
userVerificationCondition = true,
dataToVerify = UserVerificationData(mDatabase, mEntryViewModel.mainEntryId)
)
return true
}
R.id.menu_restore_entry_history -> {
mMainEntryId?.let { mainEntryId ->
mEntryViewModel.mainEntryId?.let { mainEntryId ->
restoreEntryHistory(
mainEntryId,
mHistoryPosition)
mEntryViewModel.historyPosition
)
}
}
R.id.menu_delete_entry_history -> {
mMainEntryId?.let { mainEntryId ->
mEntryViewModel.mainEntryId?.let { mainEntryId ->
deleteEntryHistory(
mainEntryId,
mHistoryPosition)
mEntryViewModel.historyPosition
)
}
}
R.id.menu_save_database -> {
@@ -521,7 +542,7 @@ class EntryActivity : DatabaseLockActivity() {
override fun finish() {
// Transit data in previous Activity after an update
Intent().apply {
putExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY, mMainEntryId)
putExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY, mEntryViewModel.mainEntryId)
setResult(RESULT_OK, this)
}
super.finish()

View File

@@ -52,7 +52,9 @@ import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import androidx.core.view.isVisible
import androidx.drawerlayout.widget.DrawerLayout
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.datepicker.MaterialDatePicker
import com.google.android.material.timepicker.MaterialTimePicker
@@ -73,6 +75,8 @@ import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.removeModes
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveSearchInfo
import com.kunzisoft.keepass.credentialprovider.SpecialMode
import com.kunzisoft.keepass.credentialprovider.TypeMode
import com.kunzisoft.keepass.credentialprovider.UserVerificationData
import com.kunzisoft.keepass.credentialprovider.UserVerificationHelper.Companion.askUserVerification
import com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.buildPasskeyResponseAndSetResult
import com.kunzisoft.keepass.database.ContextualDatabase
@@ -122,6 +126,7 @@ import com.kunzisoft.keepass.view.updateLockPaddingStart
import com.kunzisoft.keepass.viewmodels.GroupEditViewModel
import com.kunzisoft.keepass.viewmodels.GroupViewModel
import com.kunzisoft.keepass.viewmodels.MainCredentialViewModel
import com.kunzisoft.keepass.viewmodels.UserVerificationViewModel
import kotlinx.coroutines.launch
import org.joda.time.LocalDateTime
import java.util.EnumSet
@@ -158,6 +163,7 @@ class GroupActivity : DatabaseLockActivity(),
private val mGroupViewModel: GroupViewModel by viewModels()
private val mGroupEditViewModel: GroupEditViewModel by viewModels()
private val mMainCredentialViewModel: MainCredentialViewModel by viewModels()
private val mUserVerificationViewModel: UserVerificationViewModel by viewModels()
private val mGroupActivityEducation = GroupActivityEducation(this)
@@ -565,6 +571,33 @@ class GroupActivity : DatabaseLockActivity(),
}
}
}
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.RESUMED) {
mUserVerificationViewModel.uiState.collect { uIState ->
when (uIState) {
is UserVerificationViewModel.UIState.Loading -> {}
is UserVerificationViewModel.UIState.OnUserVerificationCanceled -> {
mUserVerificationViewModel.onUserVerificationReceived()
}
is UserVerificationViewModel.UIState.OnUserVerificationSucceeded -> {
uIState.dataToVerify.database?.let { database ->
uIState.dataToVerify.entryId?.let { entryId ->
EntryEditActivity.launch(
activity = this@GroupActivity,
database = database,
registrationType = EntryEditActivity.RegistrationType.UPDATE,
nodeId = entryId,
activityResultLauncher = mEntryActivityResultLauncher
)
}
}
mUserVerificationViewModel.onUserVerificationReceived()
}
}
}
}
}
}
override fun viewToInvalidateTimeout(): View? {
@@ -1060,12 +1093,10 @@ class GroupActivity : DatabaseLockActivity(),
launchDialogForGroupUpdate(node as Group)
}
Type.ENTRY -> {
EntryEditActivity.launch(
activity = this@GroupActivity,
database = database,
registrationType = EntryEditActivity.RegistrationType.UPDATE,
nodeId = (node as Entry).nodeId,
activityResultLauncher = mEntryActivityResultLauncher
askUserVerification(
userVerificationViewModel = mUserVerificationViewModel,
userVerificationCondition = true,
dataToVerify = UserVerificationData(database,node.nodeId)
)
}
}

View File

@@ -0,0 +1,9 @@
package com.kunzisoft.keepass.credentialprovider
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.element.node.NodeId
data class UserVerificationData(
val database: ContextualDatabase? = null,
val entryId: NodeId<*>? = null
)

View File

@@ -14,7 +14,6 @@ import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.MainCredentialDialogFragment
import com.kunzisoft.keepass.activities.dialogs.MainCredentialDialogFragment.Companion.TAG_ASK_MAIN_CREDENTIAL
import com.kunzisoft.keepass.credentialprovider.passkey.data.UserVerificationRequirement
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.utils.getEnumExtra
import com.kunzisoft.keepass.utils.putEnumExtra
import com.kunzisoft.keepass.view.toastError
@@ -86,12 +85,13 @@ class UserVerificationHelper {
* Ask for the database credential otherwise
*/
fun FragmentActivity.askUserVerification(
database: ContextualDatabase?,
userVerificationViewModel: UserVerificationViewModel
userVerificationViewModel: UserVerificationViewModel,
userVerificationCondition: Boolean,
dataToVerify: UserVerificationData
) {
if (this.intent.getUserVerificationCondition()) {
if (userVerificationCondition) {
// Important to check the nullable database here
database?.let {
dataToVerify.database?.let {
if (isAuthenticatorsAllowed()) {
BiometricPrompt(
this, ContextCompat.getMainExecutor(this),
@@ -113,20 +113,20 @@ class UserVerificationHelper {
toastError(SecurityException("Authentication error: $errString"))
}
}
userVerificationViewModel.onUserVerificationFailed(database)
userVerificationViewModel.onUserVerificationFailed(dataToVerify)
}
override fun onAuthenticationSucceeded(
result: BiometricPrompt.AuthenticationResult
) {
super.onAuthenticationSucceeded(result)
userVerificationViewModel.onUserVerificationSucceeded(database)
userVerificationViewModel.onUserVerificationSucceeded(dataToVerify)
}
override fun onAuthenticationFailed() {
super.onAuthenticationFailed()
toastError(SecurityException(getString(R.string.device_unlock_not_recognized)))
userVerificationViewModel.onUserVerificationFailed(database)
userVerificationViewModel.onUserVerificationFailed(dataToVerify)
}
}).authenticate(
BiometricPrompt.PromptInfo.Builder()
@@ -141,7 +141,7 @@ class UserVerificationHelper {
.findFragmentByTag(TAG_ASK_MAIN_CREDENTIAL) as? MainCredentialDialogFragment?
if (mainCredentialDialogFragment == null) {
mainCredentialDialogFragment = MainCredentialDialogFragment
.getInstance(database.fileUri)
.getInstance(dataToVerify.database.fileUri)
mainCredentialDialogFragment.show(
supportFragmentManager,
TAG_ASK_MAIN_CREDENTIAL
@@ -150,7 +150,7 @@ class UserVerificationHelper {
}
}
} else {
userVerificationViewModel.onUserVerificationSucceeded(database)
userVerificationViewModel.onUserVerificationSucceeded(dataToVerify)
}
}
}

View File

@@ -31,7 +31,9 @@ import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AlertDialog
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.FileDatabaseSelectActivity
import com.kunzisoft.keepass.activities.GroupActivity
@@ -43,6 +45,7 @@ import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addTypeMode
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.setActivityResult
import com.kunzisoft.keepass.credentialprovider.SpecialMode
import com.kunzisoft.keepass.credentialprovider.TypeMode
import com.kunzisoft.keepass.credentialprovider.UserVerificationData
import com.kunzisoft.keepass.credentialprovider.UserVerificationHelper.Companion.addUserVerification
import com.kunzisoft.keepass.credentialprovider.UserVerificationHelper.Companion.askUserVerification
import com.kunzisoft.keepass.credentialprovider.UserVerificationHelper.Companion.getUserVerificationCondition
@@ -186,19 +189,23 @@ class PasskeyLauncherActivity : DatabaseLockActivity() {
}
}
lifecycleScope.launch {
userVerificationViewModel.uiState.collect { uiState ->
when (uiState) {
is UserVerificationViewModel.UIState.Loading -> {}
is UserVerificationViewModel.UIState.OnUserVerificationSucceeded -> {
passkeyLauncherViewModel.launchActionIfNeeded(
userVerified = true,
intent = intent,
specialMode = mSpecialMode,
database = uiState.database
)
}
is UserVerificationViewModel.UIState.OnUserVerificationCanceled -> {
passkeyLauncherViewModel.cancelResult()
repeatOnLifecycle(Lifecycle.State.RESUMED) {
userVerificationViewModel.uiState.collect { uiState ->
when (uiState) {
is UserVerificationViewModel.UIState.Loading -> {}
is UserVerificationViewModel.UIState.OnUserVerificationSucceeded -> {
passkeyLauncherViewModel.launchActionIfNeeded(
userVerified = true,
intent = intent,
specialMode = mSpecialMode,
database = uiState.dataToVerify.database
)
userVerificationViewModel.onUserVerificationReceived()
}
is UserVerificationViewModel.UIState.OnUserVerificationCanceled -> {
passkeyLauncherViewModel.cancelResult()
userVerificationViewModel.onUserVerificationReceived()
}
}
}
}
@@ -208,9 +215,11 @@ class PasskeyLauncherActivity : DatabaseLockActivity() {
override fun onUnknownDatabaseRetrieved(database: ContextualDatabase?) {
super.onUnknownDatabaseRetrieved(database)
// To manage https://github.com/Kunzisoft/KeePassDX/issues/2283
// When a database is opened
askUserVerification(
database = database,
userVerificationViewModel = userVerificationViewModel
userVerificationViewModel = userVerificationViewModel,
userVerificationCondition = intent.getUserVerificationCondition(),
dataToVerify = UserVerificationData(database)
)
}
@@ -227,9 +236,13 @@ class PasskeyLauncherActivity : DatabaseLockActivity() {
}
ACTION_DATABASE_CHECK_CREDENTIAL_TASK -> {
if (result.isSuccess) {
userVerificationViewModel.onUserVerificationSucceeded(database)
userVerificationViewModel.onUserVerificationSucceeded(
UserVerificationData(database)
)
} else {
userVerificationViewModel.onUserVerificationFailed(database)
userVerificationViewModel.onUserVerificationFailed(
UserVerificationData(database)
)
}
}
}

View File

@@ -36,8 +36,14 @@ import java.util.UUID
class EntryViewModel: ViewModel() {
private var mMainEntryId: NodeId<UUID>? = null
private var mHistoryPosition: Int = -1
var mainEntryId: NodeId<UUID>? = null
private set
var historyPosition: Int = -1
private set
var entryIsHistory: Boolean = false
private set
var entryLoaded = false
private set
val entryInfoHistory : LiveData<EntryInfoHistory?> get() = _entryInfoHistory
private val _entryInfoHistory = MutableLiveData<EntryInfoHistory?>()
@@ -60,12 +66,12 @@ class EntryViewModel: ViewModel() {
private val _historySelected = SingleLiveEvent<EntryHistory>()
fun loadDatabase(database: ContextualDatabase?) {
loadEntry(database, mMainEntryId, mHistoryPosition)
loadEntry(database, mainEntryId, historyPosition)
}
fun loadEntry(database: ContextualDatabase?, mainEntryId: NodeId<UUID>?, historyPosition: Int = -1) {
this.mMainEntryId = mainEntryId
this.mHistoryPosition = historyPosition
this.mainEntryId = mainEntryId
this.historyPosition = historyPosition
if (database != null && mainEntryId != null) {
IOActionTask(
@@ -104,6 +110,12 @@ class EntryViewModel: ViewModel() {
}
},
{ entryInfoHistory ->
if (entryInfoHistory != null) {
this.mainEntryId = entryInfoHistory.mainEntryId
this.historyPosition = historyPosition
this.entryIsHistory = historyPosition > -1
this.entryLoaded = true
}
_entryInfoHistory.value = entryInfoHistory
_entryHistory.value = entryInfoHistory?.entryHistory
}

View File

@@ -1,7 +1,7 @@
package com.kunzisoft.keepass.viewmodels
import androidx.lifecycle.ViewModel
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.credentialprovider.UserVerificationData
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@@ -13,22 +13,22 @@ class UserVerificationViewModel: ViewModel() {
private val mUiState = MutableStateFlow<UIState>(UIState.Loading)
val uiState: StateFlow<UIState> = mUiState
fun onUserVerificationSucceeded(database: ContextualDatabase?) {
mUiState.value = UIState.OnUserVerificationSucceeded(database)
fun onUserVerificationSucceeded(dataToVerify: UserVerificationData) {
mUiState.value = UIState.OnUserVerificationSucceeded(dataToVerify)
}
fun onUserVerificationFailed(database: ContextualDatabase? = null) {
mUiState.value = UIState.OnUserVerificationCanceled(database)
fun onUserVerificationFailed(dataToVerify: UserVerificationData = UserVerificationData()) {
mUiState.value = UIState.OnUserVerificationCanceled(dataToVerify)
}
fun onUserVerificationReceived() {
mUiState.value = UIState.Loading
}
sealed class UIState {
object Loading: UIState()
data class OnUserVerificationSucceeded(
val database: ContextualDatabase?
): UIState()
data class OnUserVerificationCanceled(
val database: ContextualDatabase?
): UIState()
data class OnUserVerificationSucceeded(val dataToVerify: UserVerificationData): UIState()
data class OnUserVerificationCanceled(val dataToVerify: UserVerificationData): UIState()
}
}