fix: Unprotect with User Verification #2283

This commit is contained in:
J-Jamet
2025-12-02 20:25:44 +01:00
parent 2bbb40e513
commit b394a99e40
10 changed files with 94 additions and 24 deletions

View File

@@ -323,6 +323,27 @@ class EntryActivity : DatabaseLockActivity() {
}
}
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.RESUMED) {
mEntryViewModel.entryState.collect { entryState ->
when (entryState) {
is EntryViewModel.EntryState.Loading -> {}
is EntryViewModel.EntryState.RequestUnprotectField -> {
val fieldView = entryState.protectedFieldView
if (fieldView.isCurrentlyProtected()) {
checkUserVerification(
userVerificationViewModel = mUserVerificationViewModel,
dataToVerify = UserVerificationData(protectedFieldView = fieldView)
)
} else {
fieldView.protect()
}
mEntryViewModel.actionPerformed()
}
}
}
}
}
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.RESUMED) {
mUserVerificationViewModel.userVerificationState.collect { uVState ->
@@ -333,7 +354,10 @@ class EntryActivity : DatabaseLockActivity() {
mUserVerificationViewModel.onUserVerificationReceived()
}
is UserVerificationViewModel.UIState.OnUserVerificationSucceeded -> {
// Edit Entry if corresponding data
editEntry(uVState.dataToVerify.database, uVState.dataToVerify.entryId)
// Unprotect field if corresponding data
uVState.dataToVerify.protectedFieldView?.unprotect()
mUserVerificationViewModel.onUserVerificationReceived()
}
}

View File

@@ -477,7 +477,7 @@ class EntryEditActivity : DatabaseLockActivity(),
searchAction = {
// Nothing when search retrieved
},
selectionAction = { intentSender, typeMode, searchInfo ->
selectionAction = { _, typeMode, _ ->
when(typeMode) {
TypeMode.DEFAULT -> {}
TypeMode.MAGIKEYBOARD ->

View File

@@ -152,10 +152,12 @@ class EntryFragment: DatabaseFragment() {
private fun assignEntryInfo(entryInfo: EntryInfo?) {
// Set copy buttons
templateView.apply {
setOnUnprotectClickListener { _, textEditFieldView ->
mEntryViewModel.requestUnprotectField(textEditFieldView)
}
setOnAskCopySafeClickListener {
showClipboardDialog()
}
setOnCopyActionClickListener { field ->
mClipboardHelper?.timeoutCopyToClipboard(
TemplateField.getLocalizedName(context, field.name),
@@ -242,7 +244,7 @@ class EntryFragment: DatabaseFragment() {
fun firstEntryFieldCopyView(): View? {
return try {
templateView.getActionImageView()
} catch (e: Exception) {
} catch (_: Exception) {
null
}
}

View File

@@ -90,7 +90,9 @@ class UserVerificationHelper {
* Check if the User needs to be verified for this entry
*/
fun EntryInfo.isUserVerificationNeeded(): Boolean {
return this.passkey != null
// Apply to any entry with protected content
// Not only this.passkey != null
return true
}
fun Fragment.checkUserVerification(

View File

@@ -3,7 +3,7 @@ package com.kunzisoft.keepass.view
import android.view.View.OnClickListener
interface ProtectedFieldView {
fun setOnUnprotectClickListener(onUnprotectClickListener: OnClickListener?)
fun setProtection(protection: Boolean, onUnprotectClickListener: OnClickListener?)
fun protect()
fun unprotect()
fun isCurrentlyProtected(): Boolean

View File

@@ -127,10 +127,8 @@ class TemplateEditView @JvmOverloads constructor(context: Context,
PasswordTextEditFieldView(it)
else TextEditFieldView(it)).apply {
// hiddenProtectedValue (mHideProtectedValue) don't work with TextInputLayout
if (field.protectedValue.isProtected) {
setOnUnprotectClickListener {
mOnUnprotectClickListener?.invoke(field, this)
}
setProtection(field.protectedValue.isProtected) {
mOnUnprotectClickListener?.invoke(field, this)
}
default = templateAttribute.default
setMaxChars(templateAttribute.options.getNumberChars())
@@ -198,7 +196,7 @@ class TemplateEditView @JvmOverloads constructor(context: Context,
val value = field.protectedValue.toString().trim()
type = dateInstantType
activation = value.isNotEmpty()
} catch (e: Exception) {
} catch (_: Exception) {
type = dateInstantType
activation = false
}

View File

@@ -25,6 +25,11 @@ class TemplateView @JvmOverloads constructor(context: Context,
: TemplateAbstractView<TextFieldView, TextFieldView, DateTimeFieldView>
(context, attrs, defStyle) {
private var mOnUnprotectClickListener: ((Field, ProtectedFieldView) -> Unit)? = null
fun setOnUnprotectClickListener(listener: ((Field, ProtectedFieldView) -> Unit)?) {
this.mOnUnprotectClickListener = listener
}
private var mOnAskCopySafeClickListener: (() -> Unit)? = null
fun setOnAskCopySafeClickListener(listener: (() -> Unit)? = null) {
this.mOnAskCopySafeClickListener = listener
@@ -58,7 +63,9 @@ class TemplateView @JvmOverloads constructor(context: Context,
PasskeyTextFieldView(it)
else TextFieldView(it)).apply {
applyFontVisibility(mFontInVisibility)
setProtection(field.protectedValue.isProtected)
setProtection(field.protectedValue.isProtected) {
mOnUnprotectClickListener?.invoke(field, this)
}
label = templateAttribute.alias
?: TemplateField.getLocalizedName(context, field.name)
setMaxChars(templateAttribute.options.getNumberChars())
@@ -114,7 +121,7 @@ class TemplateView @JvmOverloads constructor(context: Context,
try {
val value = field.protectedValue.toString().trim()
activation = value.isNotEmpty()
} catch (e: Exception) {
} catch (_: Exception) {
activation = false
}
}

View File

@@ -171,14 +171,16 @@ open class TextEditFieldView @JvmOverloads constructor(context: Context,
}
}
override fun setOnUnprotectClickListener(onUnprotectClickListener: OnClickListener?) {
labelView.endIconMode = TextInputLayout.END_ICON_PASSWORD_TOGGLE
valueView.inputType = valueView.inputType or InputType.TYPE_TEXT_VARIATION_PASSWORD
/*
// FIXME Called by itself during orientation change
labelView.setEndIconOnClickListener {
onUnprotectClickListener?.onClick(this@TextEditFieldView)
}*/
override fun setProtection(protection: Boolean, onUnprotectClickListener: OnClickListener?) {
if (protection) {
labelView.endIconMode = TextInputLayout.END_ICON_PASSWORD_TOGGLE
valueView.inputType = valueView.inputType or InputType.TYPE_TEXT_VARIATION_PASSWORD
/*
// FIXME Called by itself during orientation change
labelView.setEndIconOnClickListener {
onUnprotectClickListener?.onClick(this@TextEditFieldView)
}*/
}
}
override fun isCurrentlyProtected(): Boolean {

View File

@@ -42,7 +42,8 @@ import com.kunzisoft.keepass.utils.AppUtil.openExternalApp
open class TextFieldView @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0)
: RelativeLayout(context, attrs, defStyle), GenericTextFieldView {
: RelativeLayout(context, attrs, defStyle),
GenericTextFieldView, ProtectedFieldView {
protected var labelViewId = ViewCompat.generateViewId()
private var valueViewId = ViewCompat.generateViewId()
@@ -204,17 +205,30 @@ open class TextFieldView @JvmOverloads constructor(context: Context,
}
}
fun setProtection(protection: Boolean) {
override fun setProtection(protection: Boolean, onUnprotectClickListener: OnClickListener?) {
showButton.isVisible = protection
showButton.isSelected = true
showButton.setOnClickListener {
showButton.isSelected = !showButton.isSelected
changeProtectedValueParameters()
onUnprotectClickListener?.onClick(this@TextFieldView)
}
changeProtectedValueParameters()
invalidate()
}
override fun isCurrentlyProtected(): Boolean {
return showButton.isSelected
}
override fun protect() {
showButton.isSelected = !showButton.isSelected
changeProtectedValueParameters()
}
override fun unprotect() {
showButton.isSelected = !showButton.isSelected
changeProtectedValueParameters()
}
protected fun changeProtectedValueParameters() {
valueView.apply {
if (showButton.isVisible) {

View File

@@ -31,6 +31,9 @@ import com.kunzisoft.keepass.model.EntryAttachmentState
import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.otp.OtpElement
import com.kunzisoft.keepass.utils.IOActionTask
import com.kunzisoft.keepass.view.ProtectedFieldView
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import java.util.UUID
@@ -67,6 +70,9 @@ class EntryViewModel: ViewModel() {
val historySelected : LiveData<EntryHistory> get() = _historySelected
private val _historySelected = SingleLiveEvent<EntryHistory>()
private val mEntryState = MutableStateFlow<EntryState>(EntryState.Loading)
val entryState: StateFlow<EntryState> = mEntryState
fun loadDatabase(database: ContextualDatabase?) {
loadEntry(database, mainEntryId, historyPosition)
}
@@ -126,6 +132,10 @@ class EntryViewModel: ViewModel() {
}
}
fun requestUnprotectField(fieldView: ProtectedFieldView) {
mEntryState.value = EntryState.RequestUnprotectField(fieldView)
}
fun onOtpElementUpdated(optElement: OtpElement?) {
_onOtpElementUpdated.value = optElement
}
@@ -146,6 +156,10 @@ class EntryViewModel: ViewModel() {
_sectionSelected.value = section
}
fun actionPerformed() {
mEntryState.value = EntryState.Loading
}
data class EntryInfoHistory(var mainEntryId: NodeId<UUID>,
var historyPosition: Int,
val template: Template,
@@ -167,6 +181,13 @@ class EntryViewModel: ViewModel() {
}
}
sealed class EntryState {
object Loading: EntryState()
data class RequestUnprotectField(
val protectedFieldView: ProtectedFieldView
): EntryState()
}
companion object {
private val TAG = EntryViewModel::class.java.name
}