feat: Warning when overwriting existing passkey #2124

This commit is contained in:
J-Jamet
2025-10-07 13:27:46 +02:00
parent 02c514272e
commit 90b3b56893
9 changed files with 129 additions and 38 deletions

View File

@@ -1,6 +1,6 @@
KeePassDX(4.2.0)
* Passkeys management #1421 #2097 (Thx @cali-95)
* Confirm usage of passkey #2165
* Confirm usage of passkey #2165 #2124
* Dialog to manage missing signature #2152 #2155 #2161 #2160
* Capture error #2159
* Change Passkey Backup Eligibility & Backup State #2135 #2150

View File

@@ -41,6 +41,9 @@ import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.isVisible
import androidx.core.widget.NestedScrollView
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.google.android.material.datepicker.MaterialDatePicker
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.timepicker.MaterialTimePicker
@@ -99,6 +102,7 @@ import com.kunzisoft.keepass.view.showActionErrorIfNeeded
import com.kunzisoft.keepass.view.updateLockPaddingStart
import com.kunzisoft.keepass.viewmodels.ColorPickerViewModel
import com.kunzisoft.keepass.viewmodels.EntryEditViewModel
import kotlinx.coroutines.launch
import java.util.EnumSet
import java.util.UUID
@@ -154,9 +158,6 @@ class EntryEditActivity : DatabaseLockActivity(),
}
}
// To ask data lost only one time
private var backPressedAlreadyApproved = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_entry_edit)
@@ -408,6 +409,31 @@ class EntryEditActivity : DatabaseLockActivity(),
)
}
}
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
mEntryEditViewModel.uiState.collect { uiState ->
when (uiState) {
EntryEditViewModel.UIState.Loading -> {}
EntryEditViewModel.UIState.ShowOverwriteMessage -> {
if (mEntryEditViewModel.warningOverwriteDataAlreadyApproved.not()) {
AlertDialog.Builder(this@EntryEditActivity)
.setTitle(R.string.warning_overwrite_data_title)
.setMessage(R.string.warning_overwrite_data_description)
.setNegativeButton(android.R.string.cancel) { _, _ ->
mEntryEditViewModel.backPressedAlreadyApproved = true
onCancelSpecialMode()
}
.setPositiveButton(android.R.string.ok) { _, _ ->
mEntryEditViewModel.warningOverwriteDataAlreadyApproved = true
}
.create().show()
}
}
}
}
}
}
}
override fun viewToInvalidateTimeout(): View? {
@@ -422,7 +448,7 @@ class EntryEditActivity : DatabaseLockActivity(),
super.onDatabaseRetrieved(database)
mAllowCustomFields = database?.allowEntryCustomFields() == true
mAllowOTP = database?.allowOTP == true
mEntryEditViewModel.loadDatabase(database)
mEntryEditViewModel.loadTemplateEntry(database)
mTemplatesSelectorAdapter?.apply {
iconDrawableFactory = mDatabase?.iconDrawableFactory
notifyDataSetChanged()
@@ -751,13 +777,13 @@ class EntryEditActivity : DatabaseLockActivity(),
}
private fun onApprovedBackPressed(approved: () -> Unit) {
if (!backPressedAlreadyApproved) {
if (mEntryEditViewModel.backPressedAlreadyApproved.not()) {
AlertDialog.Builder(this)
.setMessage(R.string.discard_changes)
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(R.string.discard) { _, _ ->
mAttachmentFileBinderManager?.stopUploadAllAttachments()
backPressedAlreadyApproved = true
mEntryEditViewModel.backPressedAlreadyApproved = true
approved.invoke()
}.create().show()
} else {

View File

@@ -19,30 +19,39 @@
*/
package com.kunzisoft.keepass.utils
import kotlinx.coroutines.*
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlin.coroutines.EmptyCoroutineContext
/**
* Class to invoke action in a separate IO thread
*/
class IOActionTask<T>(
private val action: () -> T ,
private val afterActionListener: ((T?) -> Unit)? = null) {
private val mainScope = CoroutineScope(Dispatchers.Main)
private val action: () -> T,
private val onActionComplete: ((T?) -> Unit)? = null,
private val scope: CoroutineScope = CoroutineScope(Dispatchers.Main),
private val exceptionHandler: CoroutineExceptionHandler? = null
) {
fun execute() {
mainScope.launch {
scope.launch(exceptionHandler ?: EmptyCoroutineContext) {
withContext(Dispatchers.IO) {
val asyncResult: Deferred<T?> = async {
try {
action.invoke()
} catch (e: Exception) {
e.printStackTrace()
null
}
exceptionHandler?.let {
action.invoke()
} ?: try {
action.invoke()
} catch (e: Exception) {
e.printStackTrace()
null
}
}
withContext(Dispatchers.Main) {
afterActionListener?.invoke(asyncResult.await())
onActionComplete?.invoke(asyncResult.await())
}
}
}

View File

@@ -3,6 +3,7 @@ package com.kunzisoft.keepass.viewmodels
import android.net.Uri
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.Entry
@@ -19,6 +20,8 @@ import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.StreamDirection
import com.kunzisoft.keepass.otp.OtpElement
import com.kunzisoft.keepass.utils.IOActionTask
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import java.util.UUID
@@ -32,6 +35,10 @@ class EntryEditViewModel: NodeEditViewModel() {
private var mIsTemplate: Boolean = false
private val mTempAttachments = mutableListOf<EntryAttachmentState>()
// To show dialog only one time
var backPressedAlreadyApproved = false
var warningOverwriteDataAlreadyApproved = false
val templatesEntry : LiveData<TemplatesEntry?> get() = _templatesEntry
private val _templatesEntry = MutableLiveData<TemplatesEntry?>()
@@ -71,7 +78,10 @@ class EntryEditViewModel: NodeEditViewModel() {
val onBinaryPreviewLoaded : LiveData<AttachmentPosition> get() = _onBinaryPreviewLoaded
private val _onBinaryPreviewLoaded = SingleLiveEvent<AttachmentPosition>()
fun loadDatabase(database: ContextualDatabase?) {
private val mUiState = MutableStateFlow<UIState>(UIState.Loading)
val uiState: StateFlow<UIState> = mUiState
fun loadTemplateEntry(database: ContextualDatabase?) {
loadTemplateEntry(database, mEntryId, mParentId, mRegisterInfo)
}
@@ -88,7 +98,8 @@ class EntryEditViewModel: NodeEditViewModel() {
database?.let {
mEntryId?.let {
IOActionTask(
{
scope = viewModelScope,
action = {
// Create an Entry copy to modify from the database entry
mEntry = database.getEntryById(it)
// Retrieve the parent
@@ -107,16 +118,20 @@ class EntryEditViewModel: NodeEditViewModel() {
)
}
},
{ templatesEntry ->
onActionComplete = { templatesEntry ->
mEntryId = null
_templatesEntry.value = templatesEntry
if (templatesEntry?.overwrittenData == true) {
mUiState.value = UIState.ShowOverwriteMessage
}
}
).execute()
}
mParentId?.let {
IOActionTask(
{
scope = viewModelScope,
action = {
mParent = database.getGroupById(it)
mParent?.let { parentGroup ->
mEntry = database.createEntry()?.apply {
@@ -146,7 +161,7 @@ class EntryEditViewModel: NodeEditViewModel() {
)
}
},
{ templatesEntry ->
onActionComplete = { templatesEntry ->
mParentId = null
_templatesEntry.value = templatesEntry
}
@@ -165,6 +180,7 @@ class EntryEditViewModel: NodeEditViewModel() {
val entryTemplate = entry?.let { database.getTemplate(it) }
?: Template.STANDARD
var entryInfo: EntryInfo? = null
var overwrittenData = false
// Decode the entry / load entry info
entry?.let {
database.decodeEntryWithTemplateConfiguration(it).let { entry ->
@@ -172,13 +188,19 @@ class EntryEditViewModel: NodeEditViewModel() {
entry.getEntryInfo(database, true).let { tempEntryInfo ->
// Retrieve data from registration
registerInfo?.let { regInfo ->
tempEntryInfo.saveRegisterInfo(database, regInfo)
overwrittenData = tempEntryInfo.saveRegisterInfo(database, regInfo)
}
entryInfo = tempEntryInfo
}
}
}
return TemplatesEntry(isTemplate, templates, entryTemplate, entryInfo)
return TemplatesEntry(
isTemplate,
templates,
entryTemplate,
entryInfo,
overwrittenData
)
}
fun changeTemplate(template: Template) {
@@ -193,7 +215,8 @@ class EntryEditViewModel: NodeEditViewModel() {
fun saveEntryInfo(database: ContextualDatabase?, entry: Entry?, parent: Group?, entryInfo: EntryInfo) {
IOActionTask(
{
scope = viewModelScope,
action = {
removeTempAttachmentsNotCompleted(entryInfo)
entry?.let { oldEntry ->
// Create a clone
@@ -223,7 +246,7 @@ class EntryEditViewModel: NodeEditViewModel() {
EntrySave(oldEntry, newEntry, parent)
}
},
{ entrySave ->
onActionComplete = { entrySave ->
entrySave?.let {
_onEntrySaved.value = it
}
@@ -315,10 +338,13 @@ class EntryEditViewModel: NodeEditViewModel() {
_onBinaryPreviewLoaded.value = AttachmentPosition(entryAttachmentState, viewPosition)
}
data class TemplatesEntry(val isTemplate: Boolean,
val templates: List<Template>,
val defaultTemplate: Template,
val entryInfo: EntryInfo?)
data class TemplatesEntry(
val isTemplate: Boolean,
val templates: List<Template>,
val defaultTemplate: Template,
val entryInfo: EntryInfo?,
val overwrittenData: Boolean = false
)
data class EntryUpdate(val database: ContextualDatabase?, val entry: Entry?, val parent: Group?)
data class EntrySave(val oldEntry: Entry, val newEntry: Entry, val parent: Group?)
data class FieldEdition(val oldField: Field?, val newField: Field?)
@@ -326,6 +352,11 @@ class EntryEditViewModel: NodeEditViewModel() {
data class AttachmentUpload(val attachmentToUploadUri: Uri, val attachment: Attachment)
data class AttachmentPosition(val entryAttachmentState: EntryAttachmentState, val viewPosition: Float)
sealed class UIState {
object Loading: UIState()
object ShowOverwriteMessage: UIState()
}
companion object {
private val TAG = EntryEditViewModel::class.java.name
}

View File

@@ -389,6 +389,8 @@
<string name="warning_keyfile_integrity">The hash of the file is not guaranteed because Android can change its data on the fly. Change the file extension to .bin for correct integrity.</string>
<string name="warning_database_notification_permission">The notification permission allows you to display the status of the database and lock it with an easily accessible button.\n\nIf you do not activate this permission, the database open in the background will not be visible if another application is in the foreground.</string>
<string name="warning_copy_permission">The notification permission is needed to use the clipboard notification feature.</string>
<string name="warning_overwrite_data_title">Overwrite existing data?</string>
<string name="warning_overwrite_data_description">This action will replace the existing data in the entry, you can retrieve the old data if the history is enabled.</string>
<string name="later">Later</string>
<string name="ask">Ask</string>
<string name="merge_success">Merge successfully completed</string>

View File

@@ -204,18 +204,20 @@ class EntryInfo : NodeInfo {
}
/**
* Add registerInfo to current EntryInfo
* Add registerInfo to current EntryInfo,
* return true if data has been overwrite
*/
fun saveRegisterInfo(database: Database?, registerInfo: RegisterInfo) {
fun saveRegisterInfo(database: Database?, registerInfo: RegisterInfo): Boolean {
saveSearchInfo(database, registerInfo.searchInfo)
registerInfo.username?.let { username = it }
registerInfo.password?.let { password = it }
setCreditCard(registerInfo.creditCard)
setPasskey(registerInfo.passkey)
val dataOverwrite: Boolean = setPasskey(registerInfo.passkey)
saveAppOrigin(database, registerInfo.appOrigin)
if (title.isEmpty()) {
title = registerInfo.toString().toTitle()
}
return dataOverwrite
}
/**

View File

@@ -40,8 +40,24 @@ object PasskeyEntryFields {
)
}
fun EntryInfo.setPasskey(passkey: Passkey?) {
fun EntryInfo.containsPasskey(): Boolean {
return this.tags.contains(PASSKEY_TAG)
|| this.containsCustomField(FIELD_USERNAME)
|| this.containsCustomField(FIELD_PRIVATE_KEY)
|| this.containsCustomField(FIELD_CREDENTIAL_ID)
|| this.containsCustomField(FIELD_USER_HANDLE)
|| this.containsCustomField(FIELD_RELYING_PARTY)
}
/**
* Set a passkey in an entry,
* return true if data has been overwritten
*/
fun EntryInfo.setPasskey(passkey: Passkey?): Boolean {
var overwrite = false
if (passkey != null) {
if (containsPasskey())
overwrite = true
tags.put(PASSKEY_TAG)
if (this.username.isEmpty())
this.username = passkey.username
@@ -76,6 +92,7 @@ object PasskeyEntryFields {
)
)
}
return overwrite
}
/**
@@ -132,4 +149,6 @@ object PasskeyEntryFields {
fun Field.isRelyingParty(): Boolean {
return name == FIELD_RELYING_PARTY
}
class AlreadyExistPasskey(): SecurityException("Entry already contains a passkey")
}

View File

@@ -1,5 +1,6 @@
* Passkeys management #1421 (Thx @cali-95)
* Search settings #2112 #2181
* Setting to close database after a Passkey selection #2187
* Warning when overwriting existing Passkey #2124
* Autofill refactoring #765
* Small fixes #2171 #2150 #2159 #2122

View File

@@ -1,5 +1,6 @@
* Gestion de Passkeys #1421 (Thx @cali-95)
* Paramètres de recherche #2112 #2181
* Paramètre de fermeture de la base après une sélection de Passkey #2187
* Mise en garde lors de l'écrasement d'un Passkey existant #21124
* Refonte du remplissage automatique #765
* Petites corrections #2171 #2150 #2159 #2122