mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
fix: Unprotect with User Verification #2283
This commit is contained in:
@@ -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 {
|
lifecycleScope.launch {
|
||||||
repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||||
mUserVerificationViewModel.userVerificationState.collect { uVState ->
|
mUserVerificationViewModel.userVerificationState.collect { uVState ->
|
||||||
@@ -333,7 +354,10 @@ class EntryActivity : DatabaseLockActivity() {
|
|||||||
mUserVerificationViewModel.onUserVerificationReceived()
|
mUserVerificationViewModel.onUserVerificationReceived()
|
||||||
}
|
}
|
||||||
is UserVerificationViewModel.UIState.OnUserVerificationSucceeded -> {
|
is UserVerificationViewModel.UIState.OnUserVerificationSucceeded -> {
|
||||||
|
// Edit Entry if corresponding data
|
||||||
editEntry(uVState.dataToVerify.database, uVState.dataToVerify.entryId)
|
editEntry(uVState.dataToVerify.database, uVState.dataToVerify.entryId)
|
||||||
|
// Unprotect field if corresponding data
|
||||||
|
uVState.dataToVerify.protectedFieldView?.unprotect()
|
||||||
mUserVerificationViewModel.onUserVerificationReceived()
|
mUserVerificationViewModel.onUserVerificationReceived()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -477,7 +477,7 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
searchAction = {
|
searchAction = {
|
||||||
// Nothing when search retrieved
|
// Nothing when search retrieved
|
||||||
},
|
},
|
||||||
selectionAction = { intentSender, typeMode, searchInfo ->
|
selectionAction = { _, typeMode, _ ->
|
||||||
when(typeMode) {
|
when(typeMode) {
|
||||||
TypeMode.DEFAULT -> {}
|
TypeMode.DEFAULT -> {}
|
||||||
TypeMode.MAGIKEYBOARD ->
|
TypeMode.MAGIKEYBOARD ->
|
||||||
|
|||||||
@@ -152,10 +152,12 @@ class EntryFragment: DatabaseFragment() {
|
|||||||
private fun assignEntryInfo(entryInfo: EntryInfo?) {
|
private fun assignEntryInfo(entryInfo: EntryInfo?) {
|
||||||
// Set copy buttons
|
// Set copy buttons
|
||||||
templateView.apply {
|
templateView.apply {
|
||||||
|
setOnUnprotectClickListener { _, textEditFieldView ->
|
||||||
|
mEntryViewModel.requestUnprotectField(textEditFieldView)
|
||||||
|
}
|
||||||
setOnAskCopySafeClickListener {
|
setOnAskCopySafeClickListener {
|
||||||
showClipboardDialog()
|
showClipboardDialog()
|
||||||
}
|
}
|
||||||
|
|
||||||
setOnCopyActionClickListener { field ->
|
setOnCopyActionClickListener { field ->
|
||||||
mClipboardHelper?.timeoutCopyToClipboard(
|
mClipboardHelper?.timeoutCopyToClipboard(
|
||||||
TemplateField.getLocalizedName(context, field.name),
|
TemplateField.getLocalizedName(context, field.name),
|
||||||
@@ -242,7 +244,7 @@ class EntryFragment: DatabaseFragment() {
|
|||||||
fun firstEntryFieldCopyView(): View? {
|
fun firstEntryFieldCopyView(): View? {
|
||||||
return try {
|
return try {
|
||||||
templateView.getActionImageView()
|
templateView.getActionImageView()
|
||||||
} catch (e: Exception) {
|
} catch (_: Exception) {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,7 +90,9 @@ class UserVerificationHelper {
|
|||||||
* Check if the User needs to be verified for this entry
|
* Check if the User needs to be verified for this entry
|
||||||
*/
|
*/
|
||||||
fun EntryInfo.isUserVerificationNeeded(): Boolean {
|
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(
|
fun Fragment.checkUserVerification(
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package com.kunzisoft.keepass.view
|
|||||||
import android.view.View.OnClickListener
|
import android.view.View.OnClickListener
|
||||||
|
|
||||||
interface ProtectedFieldView {
|
interface ProtectedFieldView {
|
||||||
fun setOnUnprotectClickListener(onUnprotectClickListener: OnClickListener?)
|
fun setProtection(protection: Boolean, onUnprotectClickListener: OnClickListener?)
|
||||||
fun protect()
|
fun protect()
|
||||||
fun unprotect()
|
fun unprotect()
|
||||||
fun isCurrentlyProtected(): Boolean
|
fun isCurrentlyProtected(): Boolean
|
||||||
|
|||||||
@@ -127,10 +127,8 @@ class TemplateEditView @JvmOverloads constructor(context: Context,
|
|||||||
PasswordTextEditFieldView(it)
|
PasswordTextEditFieldView(it)
|
||||||
else TextEditFieldView(it)).apply {
|
else TextEditFieldView(it)).apply {
|
||||||
// hiddenProtectedValue (mHideProtectedValue) don't work with TextInputLayout
|
// hiddenProtectedValue (mHideProtectedValue) don't work with TextInputLayout
|
||||||
if (field.protectedValue.isProtected) {
|
setProtection(field.protectedValue.isProtected) {
|
||||||
setOnUnprotectClickListener {
|
mOnUnprotectClickListener?.invoke(field, this)
|
||||||
mOnUnprotectClickListener?.invoke(field, this)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
default = templateAttribute.default
|
default = templateAttribute.default
|
||||||
setMaxChars(templateAttribute.options.getNumberChars())
|
setMaxChars(templateAttribute.options.getNumberChars())
|
||||||
@@ -198,7 +196,7 @@ class TemplateEditView @JvmOverloads constructor(context: Context,
|
|||||||
val value = field.protectedValue.toString().trim()
|
val value = field.protectedValue.toString().trim()
|
||||||
type = dateInstantType
|
type = dateInstantType
|
||||||
activation = value.isNotEmpty()
|
activation = value.isNotEmpty()
|
||||||
} catch (e: Exception) {
|
} catch (_: Exception) {
|
||||||
type = dateInstantType
|
type = dateInstantType
|
||||||
activation = false
|
activation = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,11 @@ class TemplateView @JvmOverloads constructor(context: Context,
|
|||||||
: TemplateAbstractView<TextFieldView, TextFieldView, DateTimeFieldView>
|
: TemplateAbstractView<TextFieldView, TextFieldView, DateTimeFieldView>
|
||||||
(context, attrs, defStyle) {
|
(context, attrs, defStyle) {
|
||||||
|
|
||||||
|
private var mOnUnprotectClickListener: ((Field, ProtectedFieldView) -> Unit)? = null
|
||||||
|
fun setOnUnprotectClickListener(listener: ((Field, ProtectedFieldView) -> Unit)?) {
|
||||||
|
this.mOnUnprotectClickListener = listener
|
||||||
|
}
|
||||||
|
|
||||||
private var mOnAskCopySafeClickListener: (() -> Unit)? = null
|
private var mOnAskCopySafeClickListener: (() -> Unit)? = null
|
||||||
fun setOnAskCopySafeClickListener(listener: (() -> Unit)? = null) {
|
fun setOnAskCopySafeClickListener(listener: (() -> Unit)? = null) {
|
||||||
this.mOnAskCopySafeClickListener = listener
|
this.mOnAskCopySafeClickListener = listener
|
||||||
@@ -58,7 +63,9 @@ class TemplateView @JvmOverloads constructor(context: Context,
|
|||||||
PasskeyTextFieldView(it)
|
PasskeyTextFieldView(it)
|
||||||
else TextFieldView(it)).apply {
|
else TextFieldView(it)).apply {
|
||||||
applyFontVisibility(mFontInVisibility)
|
applyFontVisibility(mFontInVisibility)
|
||||||
setProtection(field.protectedValue.isProtected)
|
setProtection(field.protectedValue.isProtected) {
|
||||||
|
mOnUnprotectClickListener?.invoke(field, this)
|
||||||
|
}
|
||||||
label = templateAttribute.alias
|
label = templateAttribute.alias
|
||||||
?: TemplateField.getLocalizedName(context, field.name)
|
?: TemplateField.getLocalizedName(context, field.name)
|
||||||
setMaxChars(templateAttribute.options.getNumberChars())
|
setMaxChars(templateAttribute.options.getNumberChars())
|
||||||
@@ -114,7 +121,7 @@ class TemplateView @JvmOverloads constructor(context: Context,
|
|||||||
try {
|
try {
|
||||||
val value = field.protectedValue.toString().trim()
|
val value = field.protectedValue.toString().trim()
|
||||||
activation = value.isNotEmpty()
|
activation = value.isNotEmpty()
|
||||||
} catch (e: Exception) {
|
} catch (_: Exception) {
|
||||||
activation = false
|
activation = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -171,14 +171,16 @@ open class TextEditFieldView @JvmOverloads constructor(context: Context,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setOnUnprotectClickListener(onUnprotectClickListener: OnClickListener?) {
|
override fun setProtection(protection: Boolean, onUnprotectClickListener: OnClickListener?) {
|
||||||
labelView.endIconMode = TextInputLayout.END_ICON_PASSWORD_TOGGLE
|
if (protection) {
|
||||||
valueView.inputType = valueView.inputType or InputType.TYPE_TEXT_VARIATION_PASSWORD
|
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 {
|
// FIXME Called by itself during orientation change
|
||||||
onUnprotectClickListener?.onClick(this@TextEditFieldView)
|
labelView.setEndIconOnClickListener {
|
||||||
}*/
|
onUnprotectClickListener?.onClick(this@TextEditFieldView)
|
||||||
|
}*/
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun isCurrentlyProtected(): Boolean {
|
override fun isCurrentlyProtected(): Boolean {
|
||||||
|
|||||||
@@ -42,7 +42,8 @@ import com.kunzisoft.keepass.utils.AppUtil.openExternalApp
|
|||||||
open class TextFieldView @JvmOverloads constructor(context: Context,
|
open class TextFieldView @JvmOverloads constructor(context: Context,
|
||||||
attrs: AttributeSet? = null,
|
attrs: AttributeSet? = null,
|
||||||
defStyle: Int = 0)
|
defStyle: Int = 0)
|
||||||
: RelativeLayout(context, attrs, defStyle), GenericTextFieldView {
|
: RelativeLayout(context, attrs, defStyle),
|
||||||
|
GenericTextFieldView, ProtectedFieldView {
|
||||||
|
|
||||||
protected var labelViewId = ViewCompat.generateViewId()
|
protected var labelViewId = ViewCompat.generateViewId()
|
||||||
private var valueViewId = 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.isVisible = protection
|
||||||
showButton.isSelected = true
|
showButton.isSelected = true
|
||||||
showButton.setOnClickListener {
|
showButton.setOnClickListener {
|
||||||
showButton.isSelected = !showButton.isSelected
|
onUnprotectClickListener?.onClick(this@TextFieldView)
|
||||||
changeProtectedValueParameters()
|
|
||||||
}
|
}
|
||||||
changeProtectedValueParameters()
|
changeProtectedValueParameters()
|
||||||
invalidate()
|
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() {
|
protected fun changeProtectedValueParameters() {
|
||||||
valueView.apply {
|
valueView.apply {
|
||||||
if (showButton.isVisible) {
|
if (showButton.isVisible) {
|
||||||
|
|||||||
@@ -31,6 +31,9 @@ import com.kunzisoft.keepass.model.EntryAttachmentState
|
|||||||
import com.kunzisoft.keepass.model.EntryInfo
|
import com.kunzisoft.keepass.model.EntryInfo
|
||||||
import com.kunzisoft.keepass.otp.OtpElement
|
import com.kunzisoft.keepass.otp.OtpElement
|
||||||
import com.kunzisoft.keepass.utils.IOActionTask
|
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
|
import java.util.UUID
|
||||||
|
|
||||||
|
|
||||||
@@ -67,6 +70,9 @@ class EntryViewModel: ViewModel() {
|
|||||||
val historySelected : LiveData<EntryHistory> get() = _historySelected
|
val historySelected : LiveData<EntryHistory> get() = _historySelected
|
||||||
private val _historySelected = SingleLiveEvent<EntryHistory>()
|
private val _historySelected = SingleLiveEvent<EntryHistory>()
|
||||||
|
|
||||||
|
private val mEntryState = MutableStateFlow<EntryState>(EntryState.Loading)
|
||||||
|
val entryState: StateFlow<EntryState> = mEntryState
|
||||||
|
|
||||||
fun loadDatabase(database: ContextualDatabase?) {
|
fun loadDatabase(database: ContextualDatabase?) {
|
||||||
loadEntry(database, mainEntryId, historyPosition)
|
loadEntry(database, mainEntryId, historyPosition)
|
||||||
}
|
}
|
||||||
@@ -126,6 +132,10 @@ class EntryViewModel: ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun requestUnprotectField(fieldView: ProtectedFieldView) {
|
||||||
|
mEntryState.value = EntryState.RequestUnprotectField(fieldView)
|
||||||
|
}
|
||||||
|
|
||||||
fun onOtpElementUpdated(optElement: OtpElement?) {
|
fun onOtpElementUpdated(optElement: OtpElement?) {
|
||||||
_onOtpElementUpdated.value = optElement
|
_onOtpElementUpdated.value = optElement
|
||||||
}
|
}
|
||||||
@@ -146,6 +156,10 @@ class EntryViewModel: ViewModel() {
|
|||||||
_sectionSelected.value = section
|
_sectionSelected.value = section
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun actionPerformed() {
|
||||||
|
mEntryState.value = EntryState.Loading
|
||||||
|
}
|
||||||
|
|
||||||
data class EntryInfoHistory(var mainEntryId: NodeId<UUID>,
|
data class EntryInfoHistory(var mainEntryId: NodeId<UUID>,
|
||||||
var historyPosition: Int,
|
var historyPosition: Int,
|
||||||
val template: Template,
|
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 {
|
companion object {
|
||||||
private val TAG = EntryViewModel::class.java.name
|
private val TAG = EntryViewModel::class.java.name
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user