From c0c98afc8abbcbac28ace330a5c8659aba0cd572 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Mon, 2 Nov 2026 23:38:48 +0100 Subject: fix(app): harden the entry passphrase prompt Ignore a submit while no prompt is showing, so OK plus IME Done cannot start two decrypts. Key the passphrase dialog on the retry count so each attempt gets a fresh field and shows the wrong password error. Note the process death gap around key import and the editor. Record the TotpModule, text selection and file IO decisions in the plan. Co-Authored-By: Claude Opus 5.5 --- .../app/passwordstore/ui/entry/EntryDestination.kt | 20 +++++++++++++------- .../app/passwordstore/ui/entry/EntryViewModel.kt | 6 +++++- 2 files changed, 18 insertions(+), 8 deletions(-) (limited to 'app/src') diff --git a/app/src/main/java/app/passwordstore/ui/entry/EntryDestination.kt b/app/src/main/java/app/passwordstore/ui/entry/EntryDestination.kt index a54b517d..5c16b5a3 100644 --- a/app/src/main/java/app/passwordstore/ui/entry/EntryDestination.kt +++ b/app/src/main/java/app/passwordstore/ui/entry/EntryDestination.kt @@ -25,6 +25,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.key import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.produceState import androidx.compose.runtime.remember @@ -104,6 +105,9 @@ fun EntryDestination( } } + // ponytail: after process death during key import or the editor, the unlock flow can start + // twice or a prompt can be left over the list; rare, no secret exposed. Fix by tracking + // whether the flow already started if it shows up. val keyImport = rememberLauncherForActivityResult(StartActivityForResult()) { result -> if (result.resultCode == Activity.RESULT_OK) unlock() else onClose() @@ -173,13 +177,15 @@ fun EntryDestination( ) EntryPrompt.Passphrase, EntryPrompt.WrongPassphrase -> - PassphraseDialog( - wrong = viewModel.prompt == EntryPrompt.WrongPassphrase, - cacheEnabled = viewModel.cacheEnabled, - clearCache = viewModel.clearCache, - onSubmit = { passphrase, clear -> viewModel.submit(passphrase, clear, activity) }, - onCancel = viewModel::cancel, - ) + key(viewModel.retries) { + PassphraseDialog( + wrong = viewModel.prompt == EntryPrompt.WrongPassphrase, + cacheEnabled = viewModel.cacheEnabled, + clearCache = viewModel.clearCache, + onSubmit = { passphrase, clear -> viewModel.submit(passphrase, clear, activity) }, + onCancel = viewModel::cancel, + ) + } } } } diff --git a/app/src/main/java/app/passwordstore/ui/entry/EntryViewModel.kt b/app/src/main/java/app/passwordstore/ui/entry/EntryViewModel.kt index 38d3b9d9..5714f2a6 100644 --- a/app/src/main/java/app/passwordstore/ui/entry/EntryViewModel.kt +++ b/app/src/main/java/app/passwordstore/ui/entry/EntryViewModel.kt @@ -86,7 +86,10 @@ constructor( private var started = false private var copyOnDecrypt = false - private var retries = 0 + /** Failed-or-asked passphrase prompts so far; keys the dialog so each attempt starts fresh. */ + var retries = 0 + private set + private var auth: BiometricResult = BiometricResult.CanceledByUser private var ids: List = emptyList() private var file: File? = null @@ -115,6 +118,7 @@ constructor( } fun submit(passphrase: String, clearCache: Boolean, context: Context) { + if (prompt == EntryPrompt.None) return prompt = EntryPrompt.None this.clearCache = clearCache val app = context.applicationContext -- cgit v1.2.3