aboutsummaryrefslogtreecommitdiffstats
path: root/app/src/main
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-11-02 23:38:48 +0100
committerDanilo M. <danix@danix.xyz>2026-11-02 23:38:48 +0100
commitc0c98afc8abbcbac28ace330a5c8659aba0cd572 (patch)
treebe4e0d33e3ffd0b541434bcf8690a21f7a88bae4 /app/src/main
parent660afb674b1580a1fc54f09550d9279cf96ec71c (diff)
downloadHardenedPass-c0c98afc8abbcbac28ace330a5c8659aba0cd572.tar.gz
HardenedPass-c0c98afc8abbcbac28ace330a5c8659aba0cd572.zip
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 <noreply@anthropic.com>
Diffstat (limited to 'app/src/main')
-rw-r--r--app/src/main/java/app/passwordstore/ui/entry/EntryDestination.kt20
-rw-r--r--app/src/main/java/app/passwordstore/ui/entry/EntryViewModel.kt6
2 files changed, 18 insertions, 8 deletions
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<PGPIdentifier> = 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