aboutsummaryrefslogtreecommitdiffstats
path: root/app
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-11-04 11:28:40 +0100
committerDanilo M. <danix@danix.xyz>2026-11-04 11:28:40 +0100
commit1d564b91ecb1b52c585e21f10596e80bc69292d4 (patch)
treee7cfa8bfe4ec23b11f81613d67125130c4932c1c /app
parente573a321d982341f2940868cb80da8d1eccc6981 (diff)
downloadHardenedPass-1d564b91ecb1b52c585e21f10596e80bc69292d4.tar.gz
HardenedPass-1d564b91ecb1b52c585e21f10596e80bc69292d4.zip
feat(app): move the autofill decryption prompts to Compose
AutofillDecryptActivity shows the no keys dialog and the entry view's passphrase dialog in Compose over its transparent FLAG_SECURE window. The flow starts once, on the first window focus, instead of on every onStart: Android 14 evicts a screen lock prompt shown before the activity is on top, and a restart showed a second passphrase dialog. After a key import it restarts on the next focus. Dismissing the no keys dialog now finishes instead of leaving an invisible window. Rotation and resizing no longer recreate the activity; other recreations cancel the fill. The passphrase is now cached only after a successful screen lock prompt, as the entry view does: a passphrase typed after a cancelled or unavailable prompt was cached whenever the cache feature was on. PasswordDialog, its layout, dimens.xml and requireKeysExist lose their last user and go. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'app')
-rw-r--r--app/src/main/AndroidManifest.xml3
-rw-r--r--app/src/main/java/app/passwordstore/ui/autofill/AutofillDecryptActivity.kt198
-rw-r--r--app/src/main/java/app/passwordstore/ui/autofill/AutofillModel.kt9
-rw-r--r--app/src/main/java/app/passwordstore/ui/crypto/BasePGPActivity.kt44
-rw-r--r--app/src/main/java/app/passwordstore/ui/crypto/PasswordDialog.kt105
-rw-r--r--app/src/main/java/app/passwordstore/ui/entry/EntryDestination.kt7
-rw-r--r--app/src/main/res/layout/dialog_password_entry.xml35
-rw-r--r--app/src/main/res/values/dimens.xml9
-rw-r--r--app/src/test/java/app/passwordstore/ui/autofill/AutofillModelTest.kt18
9 files changed, 166 insertions, 262 deletions
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index bcddf4c3..15d6447e 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -83,8 +83,11 @@
android:resource="@xml/oreo_autofill_service" />
</service>
+ <!-- ponytail: rotation, resizing and keyboard changes must not cancel a fill in progress;
+ other recreations (theme, locale) cancel it. -->
<activity
android:name=".ui.autofill.AutofillDecryptActivity"
+ android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|keyboard|keyboardHidden"
android:exported="false"
android:theme="@style/NoBackgroundThemeM3" />
<activity
diff --git a/app/src/main/java/app/passwordstore/ui/autofill/AutofillDecryptActivity.kt b/app/src/main/java/app/passwordstore/ui/autofill/AutofillDecryptActivity.kt
index 6539aa14..17859cad 100644
--- a/app/src/main/java/app/passwordstore/ui/autofill/AutofillDecryptActivity.kt
+++ b/app/src/main/java/app/passwordstore/ui/autofill/AutofillDecryptActivity.kt
@@ -11,8 +11,16 @@ import android.content.IntentSender
import android.os.Build
import android.os.Bundle
import android.view.autofill.AutofillManager
+import androidx.activity.compose.setContent
+import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.res.stringResource
import androidx.core.content.edit
-import androidx.fragment.app.setFragmentResultListener
import androidx.lifecycle.lifecycleScope
import app.passwordstore.Application.Companion.screenWasOff
import app.passwordstore.R
@@ -20,9 +28,12 @@ import app.passwordstore.crypto.PGPIdentifier
import app.passwordstore.data.crypto.PGPPassphraseCache
import app.passwordstore.data.passfile.PasswordEntry
import app.passwordstore.data.repo.PasswordRepository
+import app.passwordstore.ui.compose.theme.APSTheme
import app.passwordstore.ui.crypto.BasePGPActivity
-import app.passwordstore.ui.crypto.PasswordDialog
import app.passwordstore.ui.crypto.getPGPIdentifiers
+import app.passwordstore.ui.entry.PassphraseDialog
+import app.passwordstore.ui.main.MainActivity
+import app.passwordstore.ui.setup.SetupStart
import app.passwordstore.util.auth.BiometricAuthenticator
import app.passwordstore.util.auth.BiometricAuthenticator.Result as BiometricResult
import app.passwordstore.util.autofill.AutofillPreferences
@@ -48,6 +59,10 @@ import logcat.LogPriority.ERROR
import logcat.asLog
import logcat.logcat
+/**
+ * Decrypts one entry for the autofill service and returns the dataset to fill. Its transparent,
+ * FLAG_SECURE window shows only the "no keys" and passphrase dialogs.
+ */
@AndroidEntryPoint
class AutofillDecryptActivity : BasePGPActivity() {
@@ -56,50 +71,131 @@ class AutofillDecryptActivity : BasePGPActivity() {
@Inject lateinit var passphraseCache: PGPPassphraseCache
private lateinit var directoryStructure: DirectoryStructure
+ private lateinit var filePath: String
+ private lateinit var clientState: Bundle
+ private lateinit var action: AutofillAction
private var clearCache = true
- override fun onStart() {
- super.onStart()
- val filePath =
+ /** The screen lock prompt's outcome: the passphrase is cached only after a success. */
+ private var auth: BiometricResult = BiometricResult.CanceledByUser
+
+ /** Set once the flow started; reset after a key import, so the next window focus restarts it. */
+ private var started = false
+
+ /** Shows the "No keys imported" dialog. */
+ private var noKeys by mutableStateOf(false)
+
+ /** The recipients the passphrase dialog asks for; null while it is not shown. */
+ private var passphraseFor by mutableStateOf<List<PGPIdentifier>?>(null)
+
+ // The import's activity is still on top when its result arrives: the flow restarts on the next
+ // window focus, so the screen lock prompt is not shown (and evicted) behind it.
+ private val keyImport =
+ registerForActivityResult(StartActivityForResult()) { result ->
+ if (result.resultCode == RESULT_OK) started = false else finish()
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ // ponytail: a recreation (theme or locale change; rotation and resizing do not recreate, see
+ // the manifest) cancels the fill instead of resuming a half-done flow; the user taps the field
+ // again. Keep the flow's state in a ViewModel if this ever matters.
+ if (savedInstanceState != null) {
+ finish()
+ return
+ }
+ filePath =
intent?.getStringExtra(EXTRA_FILE_PATH)
?: run {
logcat(ERROR) { "AutofillDecryptActivity started without EXTRA_FILE_PATH" }
finish()
return
}
- val clientState =
+ clientState =
intent?.getBundleExtra(AutofillManager.EXTRA_CLIENT_STATE)
?: run {
logcat(ERROR) { "AutofillDecryptActivity started without EXTRA_CLIENT_STATE" }
finish()
return
}
- val isSearchAction = intent?.getBooleanExtra(EXTRA_SEARCH_ACTION, true)!!
- val action = if (isSearchAction) AutofillAction.Search else AutofillAction.Match
+ val isSearchAction = intent.getBooleanExtra(EXTRA_SEARCH_ACTION, true)
+ action = if (isSearchAction) AutofillAction.Search else AutofillAction.Match
directoryStructure = AutofillPreferences.directoryStructure(this)
logcat { action.toString() }
- requireKeysExist {
- if (
- features.isEnabled(EnablePGPPassphraseCache) && BiometricAuthenticator.canAuthenticate(this)
- ) {
- BiometricAuthenticator.authenticate(
- this,
- R.string.biometric_prompt_title_gpg_passphrase_cache,
- ) { authResult ->
- decrypt(filePath, clientState, action, authResult)
- }
- } else {
- decrypt(filePath, clientState, action, BiometricResult.CanceledByUser)
+ setContent {
+ APSTheme {
+ if (noKeys)
+ AlertDialog(
+ onDismissRequest = { finish() },
+ title = { Text(stringResource(R.string.no_keys_imported_dialog_title)) },
+ text = { Text(stringResource(R.string.no_keys_imported_dialog_message)) },
+ confirmButton = {
+ TextButton(
+ onClick = {
+ noKeys = false
+ keyImport.launch(
+ MainActivity.setupIntent(this@AutofillDecryptActivity, SetupStart.PgpImport)
+ )
+ }
+ ) {
+ Text(stringResource(R.string.button_label_import))
+ }
+ },
+ )
+ val identifiers = passphraseFor
+ if (identifiers != null)
+ PassphraseDialog(
+ wrong = false,
+ cacheEnabled = features.isEnabled(EnablePGPPassphraseCache),
+ clearCache = clearCache,
+ onSubmit = onSubmit@{ passphrase, clear ->
+ // Cleared before decrypting: a second submit in the same frame does nothing.
+ if (passphraseFor == null) return@onSubmit
+ passphraseFor = null
+ clearCache = clear
+ lifecycleScope.launch(dispatcherProvider.main()) {
+ decryptWithPassphrase(identifiers, passphrase)
+ }
+ },
+ onCancel = { finish() },
+ )
}
}
}
- private fun decrypt(
- filePath: String,
- clientState: Bundle,
- action: AutofillAction,
- authResult: BiometricResult,
- ) {
+ // The flow waits for window focus, as LaunchActivity's prompt does: Android 14's SystemUI
+ // dismisses a screen lock prompt shown before its activity is on top. It runs once (again only
+ // after a key import); the old onStart start ran it on every return to this activity.
+ override fun onWindowFocusChanged(hasFocus: Boolean) {
+ super.onWindowFocusChanged(hasFocus)
+ if (!hasFocus || started || isFinishing) return
+ started = true
+ start()
+ }
+
+ /** Asks for a key import first while no PGP key is imported. */
+ private fun start() {
+ if (isFinishing) return
+ lifecycleScope.launch { if (repository.hasKeys()) unlock() else noKeys = true }
+ }
+
+ private fun unlock() {
+ if (
+ features.isEnabled(EnablePGPPassphraseCache) && BiometricAuthenticator.canAuthenticate(this)
+ ) {
+ BiometricAuthenticator.authenticate(
+ this,
+ R.string.biometric_prompt_title_gpg_passphrase_cache,
+ ) { authResult ->
+ decrypt(authResult)
+ }
+ } else {
+ decrypt(BiometricResult.CanceledByUser)
+ }
+ }
+
+ private fun decrypt(authResult: BiometricResult) {
+ auth = authResult
val gpgIdentifiers =
getPGPIdentifiers(
getParentPath(filePath, PasswordRepository.getRepositoryDirectory().toString())
@@ -112,8 +208,7 @@ class AutofillDecryptActivity : BasePGPActivity() {
is BiometricResult.CanceledBySystem,
is BiometricResult.CanceledByUser,
is BiometricResult.Failure,
- is BiometricResult.HardwareUnavailableOrDisabled ->
- askPassphrase(filePath, gpgIdentifiers, clientState, action)
+ is BiometricResult.HardwareUnavailableOrDisabled -> askPassphrase(gpgIdentifiers)
is BiometricResult.Success -> {
/* clear passphrase cache on first use after application startup or if screen was off;
also make sure to purge a stale cache after caching has been disabled via PGP settings */
@@ -128,56 +223,25 @@ class AutofillDecryptActivity : BasePGPActivity() {
gpgIdentifiers.first(),
)
if (cachedPassphrase != null) {
- decryptWithPassphrase(
- File(filePath),
- gpgIdentifiers,
- clientState,
- action,
- cachedPassphrase,
- )
+ decryptWithPassphrase(gpgIdentifiers, cachedPassphrase)
} else {
- askPassphrase(filePath, gpgIdentifiers, clientState, action)
+ askPassphrase(gpgIdentifiers)
}
}
}
}
}
- private suspend fun askPassphrase(
- filePath: String,
- identifiers: List<PGPIdentifier>,
- clientState: Bundle,
- action: AutofillAction,
- ) {
+ private suspend fun askPassphrase(identifiers: List<PGPIdentifier>) {
if (!repository.isPasswordProtected(identifiers)) {
- decryptWithPassphrase(File(filePath), identifiers, clientState, action, password = "")
+ decryptWithPassphrase(identifiers, password = "")
return
}
- val dialog =
- PasswordDialog.newInstance(
- cacheEnabled = features.isEnabled(EnablePGPPassphraseCache),
- clearCache = clearCache,
- )
- dialog.show(supportFragmentManager, "PASSWORD_DIALOG")
- dialog.setFragmentResultListener(PasswordDialog.PASSWORD_RESULT_KEY) { key, bundle ->
- if (key == PasswordDialog.PASSWORD_RESULT_KEY) {
- val value = bundle.getString(PasswordDialog.PASSWORD_PHRASE_KEY)!!
- clearCache = bundle.getBoolean(PasswordDialog.PASSWORD_CLEAR_KEY)
- lifecycleScope.launch(dispatcherProvider.main()) {
- decryptWithPassphrase(File(filePath), identifiers, clientState, action, value)
- }
- }
- }
+ passphraseFor = identifiers
}
- private suspend fun decryptWithPassphrase(
- filePath: File,
- identifiers: List<PGPIdentifier>,
- clientState: Bundle,
- action: AutofillAction,
- password: String,
- ) {
- val credentials = decryptCredential(filePath, password, identifiers)
+ private suspend fun decryptWithPassphrase(identifiers: List<PGPIdentifier>, password: String) {
+ val credentials = decryptCredential(File(filePath), password, identifiers)
if (credentials == null) {
setResult(RESULT_CANCELED)
} else {
@@ -219,7 +283,7 @@ class AutofillDecryptActivity : BasePGPActivity() {
.onSuccess { result ->
return runCatching {
runCatching {
- if (features.isEnabled(EnablePGPPassphraseCache)) {
+ if (cachesPassphrase(features.isEnabled(EnablePGPPassphraseCache), auth)) {
passphraseCache.cachePassphrase(this, identifiers.first(), password)
settings.edit {
putBoolean(PreferenceKeys.CLEAR_PASSPHRASE_CACHE, clearCache)
diff --git a/app/src/main/java/app/passwordstore/ui/autofill/AutofillModel.kt b/app/src/main/java/app/passwordstore/ui/autofill/AutofillModel.kt
index ac2d0948..9f436ea2 100644
--- a/app/src/main/java/app/passwordstore/ui/autofill/AutofillModel.kt
+++ b/app/src/main/java/app/passwordstore/ui/autofill/AutofillModel.kt
@@ -6,6 +6,7 @@
package app.passwordstore.ui.autofill
import app.passwordstore.ui.list.ListRow
+import app.passwordstore.util.auth.BiometricAuthenticator.Result as BiometricResult
import app.passwordstore.util.settings.DirectoryStructure
import java.io.File
import kotlinx.collections.immutable.ImmutableList
@@ -36,3 +37,11 @@ fun autofillRows(
ListRow(file.absolutePath, identifier ?: account ?: file.nameWithoutExtension, subtitle, 0)
}
.toImmutableList()
+
+/**
+ * Whether autofill may cache the passphrase it just decrypted with: only with the cache feature on
+ * ([cacheEnabled]) and after a successful screen lock prompt ([auth]), as the entry view does. A
+ * passphrase typed after a cancelled, failed or unavailable prompt is never kept.
+ */
+fun cachesPassphrase(cacheEnabled: Boolean, auth: BiometricResult): Boolean =
+ cacheEnabled && auth is BiometricResult.Success
diff --git a/app/src/main/java/app/passwordstore/ui/crypto/BasePGPActivity.kt b/app/src/main/java/app/passwordstore/ui/crypto/BasePGPActivity.kt
index 6831761b..e40585d5 100644
--- a/app/src/main/java/app/passwordstore/ui/crypto/BasePGPActivity.kt
+++ b/app/src/main/java/app/passwordstore/ui/crypto/BasePGPActivity.kt
@@ -12,19 +12,15 @@ import android.os.Build
import android.os.Bundle
import android.os.PersistableBundle
import android.view.WindowManager
-import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
import androidx.annotation.CallSuper
import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.FragmentActivity
-import androidx.lifecycle.lifecycleScope
import app.passwordstore.R
import app.passwordstore.crypto.PGPIdentifier
import app.passwordstore.data.crypto.CryptoRepository
import app.passwordstore.data.repo.PasswordRepository
import app.passwordstore.injection.prefs.SettingsPreferences
-import app.passwordstore.ui.main.MainActivity
-import app.passwordstore.ui.setup.SetupStart
import app.passwordstore.util.coroutines.DispatcherProvider
import app.passwordstore.util.extensions.clipboard
import app.passwordstore.util.extensions.getString
@@ -33,29 +29,14 @@ import app.passwordstore.util.extensions.snackbar
import app.passwordstore.util.services.ClipboardService
import app.passwordstore.util.settings.Constants
import app.passwordstore.util.settings.PreferenceKeys
-import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint
import java.io.File
import javax.inject.Inject
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
@Suppress("Registered")
@AndroidEntryPoint
open class BasePGPActivity : AppCompatActivity() {
- /** Action to invoke if [keyImportAction] succeeds. */
- private var onKeyImport: (() -> Unit)? = null
- private val keyImportAction =
- registerForActivityResult(StartActivityForResult()) {
- if (it.resultCode == RESULT_OK) {
- onKeyImport?.invoke()
- onKeyImport = null
- } else {
- finish()
- }
- }
-
/** [SharedPreferences] instance used by subclasses to persist settings */
@SettingsPreferences @Inject lateinit var settings: SharedPreferences
@Inject lateinit var repository: CryptoRepository
@@ -71,31 +52,6 @@ open class BasePGPActivity : AppCompatActivity() {
window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE)
}
- /**
- * Function to execute [onKeysExist] only if there are PGP keys imported in the app's key manager.
- */
- fun requireKeysExist(onKeysExist: () -> Unit) {
- lifecycleScope.launch {
- val hasKeys = repository.hasKeys()
- if (!hasKeys) {
- withContext(dispatcherProvider.main()) {
- MaterialAlertDialogBuilder(this@BasePGPActivity)
- .setTitle(resources.getString(R.string.no_keys_imported_dialog_title))
- .setMessage(resources.getString(R.string.no_keys_imported_dialog_message))
- .setPositiveButton(resources.getString(R.string.button_label_import)) { _, _ ->
- onKeyImport = onKeysExist
- keyImportAction.launch(
- MainActivity.setupIntent(this@BasePGPActivity, SetupStart.PgpImport)
- )
- }
- .show()
- }
- } else {
- onKeysExist()
- }
- }
- }
-
companion object {
const val EXTRA_FILE_PATH = "FILE_PATH"
diff --git a/app/src/main/java/app/passwordstore/ui/crypto/PasswordDialog.kt b/app/src/main/java/app/passwordstore/ui/crypto/PasswordDialog.kt
deleted file mode 100644
index d845d689..00000000
--- a/app/src/main/java/app/passwordstore/ui/crypto/PasswordDialog.kt
+++ /dev/null
@@ -1,105 +0,0 @@
-/*
- * Copyright © 2014-2024 The Android Password Store Authors. All Rights Reserved.
- * SPDX-License-Identifier: GPL-3.0-only
- */
-
-package app.passwordstore.ui.crypto
-
-import android.app.Dialog
-import android.content.DialogInterface
-import android.os.Bundle
-import android.view.KeyEvent
-import android.view.WindowManager
-import androidx.core.os.bundleOf
-import androidx.core.view.isVisible
-import androidx.core.widget.doOnTextChanged
-import androidx.fragment.app.DialogFragment
-import androidx.fragment.app.setFragmentResult
-import app.passwordstore.R
-import app.passwordstore.databinding.DialogPasswordEntryBinding
-import app.passwordstore.util.extensions.finish
-import app.passwordstore.util.extensions.unsafeLazy
-import com.google.android.material.dialog.MaterialAlertDialogBuilder
-
-/** [DialogFragment] to request a password from the user and forward it along. */
-class PasswordDialog : DialogFragment() {
-
- private val binding by unsafeLazy { DialogPasswordEntryBinding.inflate(layoutInflater) }
- private var isError: Boolean = false
- private var clearCacheChecked: Boolean = true
-
- override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
- val builder = MaterialAlertDialogBuilder(requireContext())
- builder.setView(binding.root)
- builder.setTitle(R.string.password)
-
- val cacheEnabled = requireArguments().getBoolean(CACHE_ENABLED_EXTRA, false)
- binding.autoClearCache.isVisible = cacheEnabled
-
- if (cacheEnabled) {
- clearCacheChecked = requireArguments().getBoolean(AUTO_CLEAR_CACHE_EXTRA)
- binding.autoClearCache.isChecked = clearCacheChecked
- binding.autoClearCache.setOnCheckedChangeListener { _, isChecked ->
- clearCacheChecked = isChecked
- }
- }
-
- builder.setPositiveButton(android.R.string.ok) { _, _ -> setPasswordAndDismiss() }
- val dialog = builder.create()
- dialog.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE)
- dialog.setOnShowListener {
- if (isError) {
- binding.passwordField.error = getString(R.string.git_operation_wrong_password)
- }
- binding.passwordEditText.doOnTextChanged { _, _, _, _ -> binding.passwordField.error = null }
- binding.passwordEditText.setOnKeyListener { _, keyCode, _ ->
- if (keyCode == KeyEvent.KEYCODE_ENTER) {
- setPasswordAndDismiss()
- return@setOnKeyListener true
- }
- false
- }
- }
- dialog.window?.setFlags(
- WindowManager.LayoutParams.FLAG_SECURE,
- WindowManager.LayoutParams.FLAG_SECURE,
- )
- return dialog
- }
-
- fun setError() {
- isError = true
- }
-
- override fun onCancel(dialog: DialogInterface) {
- super.onCancel(dialog)
- finish()
- }
-
- private fun setPasswordAndDismiss() {
- val password = binding.passwordEditText.text.toString()
- setFragmentResult(
- PASSWORD_RESULT_KEY,
- bundleOf(PASSWORD_PHRASE_KEY to password, PASSWORD_CLEAR_KEY to clearCacheChecked),
- )
- dismissAllowingStateLoss()
- }
-
- companion object {
-
- private const val CACHE_ENABLED_EXTRA = "CACHE_ENABLED"
- private const val AUTO_CLEAR_CACHE_EXTRA = "AUTO_CLEAR_CACHE"
-
- const val PASSWORD_RESULT_KEY = "password_result"
- const val PASSWORD_PHRASE_KEY = "password_phrase"
- const val PASSWORD_CLEAR_KEY = "password_clear"
-
- fun newInstance(cacheEnabled: Boolean, clearCache: Boolean): PasswordDialog {
- val extras =
- bundleOf(CACHE_ENABLED_EXTRA to cacheEnabled, AUTO_CLEAR_CACHE_EXTRA to clearCache)
- val fragment = PasswordDialog()
- fragment.arguments = extras
- return fragment
- }
- }
-}
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 637c9b56..e5341320 100644
--- a/app/src/main/java/app/passwordstore/ui/entry/EntryDestination.kt
+++ b/app/src/main/java/app/passwordstore/ui/entry/EntryDestination.kt
@@ -173,9 +173,12 @@ fun EntryDestination(
}
}
-/** Asks for the PGP passphrase, as the old `PasswordDialog` did. */
+/**
+ * Asks for the PGP passphrase (entry view and autofill). The passphrase lives only in `remember`
+ * and the dialog's window is FLAG_SECURE.
+ */
@Composable
-private fun PassphraseDialog(
+internal fun PassphraseDialog(
wrong: Boolean,
cacheEnabled: Boolean,
clearCache: Boolean,
diff --git a/app/src/main/res/layout/dialog_password_entry.xml b/app/src/main/res/layout/dialog_password_entry.xml
deleted file mode 100644
index bbfbc506..00000000
--- a/app/src/main/res/layout/dialog_password_entry.xml
+++ /dev/null
@@ -1,35 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?><!--
- ~ Copyright © 2014-2024 The Android Password Store Authors. All Rights Reserved.
- ~ SPDX-License-Identifier: GPL-3.0-only
- -->
-
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:orientation="vertical"
- android:padding="@dimen/activity_horizontal_margin">
-
- <com.google.android.material.textfield.TextInputLayout
- android:id="@+id/password_field"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:hint="@string/password"
- app:endIconMode="password_toggle"
- app:hintAnimationEnabled="true"
- app:hintEnabled="true">
-
- <com.google.android.material.textfield.TextInputEditText
- android:id="@+id/password_edit_text"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:inputType="textPassword" />
- <requestFocus />
- </com.google.android.material.textfield.TextInputLayout>
-
- <com.google.android.material.checkbox.MaterialCheckBox
- android:id="@+id/auto_clear_cache"
- android:layout_width="match_parent"
- android:text="@string/clear_cached_password_on_screen_off"
- android:layout_height="wrap_content" />
-</LinearLayout>
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
deleted file mode 100644
index de2ac854..00000000
--- a/app/src/main/res/values/dimens.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-<!--
- ~ Copyright © 2014-2024 The Android Password Store Authors. All Rights Reserved.
- ~ SPDX-License-Identifier: GPL-3.0-only
- -->
-
-<resources>
- <!-- Default screen margins, per the Android Design guidelines. -->
- <dimen name="activity_horizontal_margin">16dp</dimen>
-</resources>
diff --git a/app/src/test/java/app/passwordstore/ui/autofill/AutofillModelTest.kt b/app/src/test/java/app/passwordstore/ui/autofill/AutofillModelTest.kt
index 8907aa54..39f7f358 100644
--- a/app/src/test/java/app/passwordstore/ui/autofill/AutofillModelTest.kt
+++ b/app/src/test/java/app/passwordstore/ui/autofill/AutofillModelTest.kt
@@ -5,10 +5,13 @@
package app.passwordstore.ui.autofill
+import app.passwordstore.util.auth.BiometricAuthenticator.Result as BiometricResult
import app.passwordstore.util.settings.DirectoryStructure
import java.io.File
import kotlin.test.Test
import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
class AutofillModelTest {
@@ -70,4 +73,19 @@ class AutofillModelTest {
rows(DirectoryStructure.FileBased, "b.org.gpg", "a.org.gpg").map { it.second },
)
}
+
+ @Test
+ fun passphraseIsCachedOnlyAfterTheScreenLock() {
+ assertTrue(cachesPassphrase(true, BiometricResult.Success(null)))
+ val notConfirmed =
+ listOf(
+ BiometricResult.CanceledByUser,
+ BiometricResult.CanceledBySystem,
+ BiometricResult.HardwareUnavailableOrDisabled,
+ BiometricResult.Failure(null, "failed"),
+ BiometricResult.Retry,
+ )
+ for (auth in notConfirmed) assertFalse(cachesPassphrase(true, auth), auth.toString())
+ assertFalse(cachesPassphrase(false, BiometricResult.Success(null)))
+ }
}