aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-11-04 17:37:46 +0100
committerDanilo M. <danix@danix.xyz>2026-11-04 17:37:46 +0100
commit104c07f0bcc37d87ddc371d06827ff93ee3fe504 (patch)
treeba24087c7e778872ba403a3e1744e2ce6e44195e
parent35b2c6ab50a7b8e141f88f9ae88570ec43e6fa7b (diff)
downloadHardenedPass-104c07f0bcc37d87ddc371d06827ff93ee3fe504.tar.gz
HardenedPass-104c07f0bcc37d87ddc371d06827ff93ee3fe504.zip
refactor(app): fold BasePGPActivity into the autofill decryption
AutofillDecryptActivity was its last subclass: it now sets FLAG_SECURE and injects the settings, crypto repository and dispatchers itself. The path and clipboard helpers move to CryptoExtensions.kt as top-level functions (a test pins the path helpers), and the FILE_PATH extra key moves, unchanged, to LaunchActivity. An invalid or short .gpg-id ID is reported with a toast: the snackbar never showed in the autofill decryption, which closes its window at once. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
-rw-r--r--app/src/main/java/app/passwordstore/data/password/PasswordItem.kt6
-rw-r--r--app/src/main/java/app/passwordstore/ui/autofill/AutofillDecryptActivity.kt16
-rw-r--r--app/src/main/java/app/passwordstore/ui/crypto/CryptoExtensions.kt (renamed from app/src/main/java/app/passwordstore/ui/crypto/BasePGPActivity.kt)100
-rw-r--r--app/src/main/java/app/passwordstore/ui/entry/EntryDestination.kt2
-rw-r--r--app/src/main/java/app/passwordstore/ui/list/StoreActions.kt2
-rw-r--r--app/src/main/java/app/passwordstore/ui/main/LaunchActivity.kt8
-rw-r--r--app/src/main/java/app/passwordstore/ui/main/MainActivity.kt3
-rw-r--r--app/src/test/java/app/passwordstore/ui/crypto/CryptoExtensionsTest.kt27
8 files changed, 86 insertions, 78 deletions
diff --git a/app/src/main/java/app/passwordstore/data/password/PasswordItem.kt b/app/src/main/java/app/passwordstore/data/password/PasswordItem.kt
index 999ad050..5d32108f 100644
--- a/app/src/main/java/app/passwordstore/data/password/PasswordItem.kt
+++ b/app/src/main/java/app/passwordstore/data/password/PasswordItem.kt
@@ -6,7 +6,7 @@ package app.passwordstore.data.password
import android.content.Context
import android.content.Intent
-import app.passwordstore.ui.crypto.BasePGPActivity
+import app.passwordstore.ui.crypto.getLongName
import app.passwordstore.ui.main.LaunchActivity
import java.io.File
@@ -15,7 +15,7 @@ data class PasswordItem(val name: String, val type: Char, val file: File, val ro
val fullPathToParent = file.absolutePath.replace(rootDir.absolutePath, "").replace(file.name, "")
- val longName = BasePGPActivity.getLongName(fullPathToParent, rootDir.absolutePath, toString())
+ val longName = getLongName(fullPathToParent, rootDir.absolutePath, toString())
override fun equals(other: Any?): Boolean {
return (other is PasswordItem) && (other.file == file)
@@ -36,7 +36,7 @@ data class PasswordItem(val name: String, val type: Char, val file: File, val ro
/** Creates an [Intent] to launch this [PasswordItem] through the authentication process. */
fun createAuthEnabledIntent(context: Context): Intent {
val intent = Intent(context, LaunchActivity::class.java)
- intent.putExtra("FILE_PATH", file.absolutePath)
+ intent.putExtra(LaunchActivity.EXTRA_FILE_PATH, file.absolutePath)
intent.action = LaunchActivity.ACTION_DECRYPT_PASS
return intent
}
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 39bf8cff..3f0fb5ef 100644
--- a/app/src/main/java/app/passwordstore/ui/autofill/AutofillDecryptActivity.kt
+++ b/app/src/main/java/app/passwordstore/ui/autofill/AutofillDecryptActivity.kt
@@ -8,11 +8,14 @@ import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.IntentSender
+import android.content.SharedPreferences
import android.os.Build
import android.os.Bundle
+import android.view.WindowManager
import android.view.autofill.AutofillManager
import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
+import androidx.appcompat.app.AppCompatActivity
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
@@ -25,12 +28,14 @@ import androidx.lifecycle.lifecycleScope
import app.passwordstore.Application.Companion.screenWasOff
import app.passwordstore.R
import app.passwordstore.crypto.PGPIdentifier
+import app.passwordstore.data.crypto.CryptoRepository
import app.passwordstore.data.crypto.PGPPassphraseCache
import app.passwordstore.data.passfile.PasswordEntry
import app.passwordstore.data.repo.PasswordRepository
+import app.passwordstore.injection.prefs.SettingsPreferences
import app.passwordstore.ui.compose.theme.APSTheme
-import app.passwordstore.ui.crypto.BasePGPActivity
import app.passwordstore.ui.crypto.getPGPIdentifiers
+import app.passwordstore.ui.crypto.getParentPath
import app.passwordstore.ui.entry.PassphraseDialog
import app.passwordstore.ui.main.MainActivity
import app.passwordstore.ui.setup.SetupStart
@@ -38,6 +43,7 @@ import app.passwordstore.util.auth.BiometricAuthenticator
import app.passwordstore.util.auth.BiometricAuthenticator.Result as BiometricResult
import app.passwordstore.util.autofill.AutofillPreferences
import app.passwordstore.util.autofill.AutofillResponseBuilder
+import app.passwordstore.util.coroutines.DispatcherProvider
import app.passwordstore.util.extensions.asLog
import app.passwordstore.util.features.Feature.EnablePGPPassphraseCache
import app.passwordstore.util.features.Features
@@ -64,8 +70,11 @@ import logcat.logcat
* FLAG_SECURE window shows only the "no keys" and passphrase dialogs.
*/
@AndroidEntryPoint
-class AutofillDecryptActivity : BasePGPActivity() {
+class AutofillDecryptActivity : AppCompatActivity() {
+ @SettingsPreferences @Inject lateinit var settings: SharedPreferences
+ @Inject lateinit var repository: CryptoRepository
+ @Inject lateinit var dispatcherProvider: DispatcherProvider
@Inject lateinit var passwordEntryFactory: PasswordEntry.Factory
@Inject lateinit var features: Features
@Inject lateinit var passphraseCache: PGPPassphraseCache
@@ -97,6 +106,9 @@ class AutofillDecryptActivity : BasePGPActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
+ // Before anything is drawn: the passphrase dialog's parent stays out of screenshots and
+ // recents.
+ window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE)
// 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.
diff --git a/app/src/main/java/app/passwordstore/ui/crypto/BasePGPActivity.kt b/app/src/main/java/app/passwordstore/ui/crypto/CryptoExtensions.kt
index e40585d5..fbaa534f 100644
--- a/app/src/main/java/app/passwordstore/ui/crypto/BasePGPActivity.kt
+++ b/app/src/main/java/app/passwordstore/ui/crypto/CryptoExtensions.kt
@@ -6,22 +6,16 @@
package app.passwordstore.ui.crypto
import android.content.ClipData
+import android.content.Context
import android.content.Intent
-import android.content.SharedPreferences
import android.os.Build
-import android.os.Bundle
import android.os.PersistableBundle
-import android.view.WindowManager
-import androidx.annotation.CallSuper
+import android.widget.Toast
import androidx.annotation.StringRes
-import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.FragmentActivity
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.util.coroutines.DispatcherProvider
import app.passwordstore.util.extensions.clipboard
import app.passwordstore.util.extensions.getString
import app.passwordstore.util.extensions.sharedPrefs
@@ -29,61 +23,33 @@ 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 dagger.hilt.android.AndroidEntryPoint
import java.io.File
-import javax.inject.Inject
-@Suppress("Registered")
-@AndroidEntryPoint
-open class BasePGPActivity : AppCompatActivity() {
+/** Gets the relative path to the repository */
+private fun getRelativePath(fullPath: String, repositoryPath: String): String =
+ fullPath.replace(repositoryPath, "").replace("/+".toRegex(), "/")
- /** [SharedPreferences] instance used by subclasses to persist settings */
- @SettingsPreferences @Inject lateinit var settings: SharedPreferences
- @Inject lateinit var repository: CryptoRepository
- @Inject lateinit var dispatcherProvider: DispatcherProvider
-
- /**
- * [onCreate] sets the window up with the right flags to prevent auth leaks through screenshots or
- * recent apps screen.
- */
- @CallSuper
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE)
- }
-
- companion object {
-
- const val EXTRA_FILE_PATH = "FILE_PATH"
-
- /** Gets the relative path to the repository */
- fun getRelativePath(fullPath: String, repositoryPath: String): String =
- fullPath.replace(repositoryPath, "").replace("/+".toRegex(), "/")
+/** Gets the Parent path, relative to the repository */
+fun getParentPath(fullPath: String, repositoryPath: String): String {
+ val relativePath = getRelativePath(fullPath, repositoryPath)
+ val index = relativePath.lastIndexOf("/")
+ return "/${relativePath.substring(startIndex = 0, endIndex = index + 1)}/"
+ .replace("/+".toRegex(), "/")
+}
- /** Gets the Parent path, relative to the repository */
- fun getParentPath(fullPath: String, repositoryPath: String): String {
- val relativePath = getRelativePath(fullPath, repositoryPath)
- val index = relativePath.lastIndexOf("/")
- return "/${relativePath.substring(startIndex = 0, endIndex = index + 1)}/"
- .replace("/+".toRegex(), "/")
- }
-
- /** /path/to/store/social/facebook.gpg -> social/facebook */
- @JvmStatic
- fun getLongName(fullPath: String, repositoryPath: String, basename: String): String {
- var relativePath = getRelativePath(fullPath, repositoryPath)
- return if (relativePath.isNotEmpty() && relativePath != "/") {
- // remove preceding '/'
- relativePath = relativePath.substring(1)
- if (relativePath.endsWith('/')) {
- relativePath + basename
- } else {
- "$relativePath/$basename"
- }
- } else {
- basename
- }
+/** /path/to/store/social/facebook.gpg -> social/facebook */
+fun getLongName(fullPath: String, repositoryPath: String, basename: String): String {
+ var relativePath = getRelativePath(fullPath, repositoryPath)
+ return if (relativePath.isNotEmpty() && relativePath != "/") {
+ // remove preceding '/'
+ relativePath = relativePath.substring(1)
+ if (relativePath.endsWith('/')) {
+ relativePath + basename
+ } else {
+ "$relativePath/$basename"
}
+ } else {
+ basename
}
}
@@ -128,10 +94,11 @@ fun FragmentActivity.copyPasswordToClipboard(password: String?) {
/**
* Get a list of available [PGPIdentifier]s for the current password repository. This method throws
- * when no identifiers were able to be parsed. If this method returns null, it means that an invalid
- * identifier was encountered and further execution must stop to let the user correct the problem.
+ * when no identifiers were able to be parsed. If this method returns null, an invalid identifier
+ * was encountered and reported with a toast: further execution must stop to let the user correct
+ * the problem.
*/
-fun FragmentActivity.getPGPIdentifiers(subDir: String): List<PGPIdentifier>? {
+fun Context.getPGPIdentifiers(subDir: String): List<PGPIdentifier>? {
val repoRoot = PasswordRepository.getRepositoryDirectory()
val gpgIdentifierFile =
File(repoRoot, subDir).findTillRoot(".gpg-id", repoRoot)
@@ -149,11 +116,12 @@ fun FragmentActivity.getPGPIdentifiers(subDir: String): List<PGPIdentifier>? {
if (line.isEmpty()) return@run
// Apparently `gpg-id` being the first line is also acceptable?
if (line == "gpg-id") return@run
- if (line.removePrefix("0x").matches("[a-fA-F0-9]{8}".toRegex())) {
- snackbar(message = resources.getString(R.string.short_gpg_id))
- } else {
- snackbar(message = resources.getString(R.string.invalid_gpg_id))
- }
+ val message =
+ if (line.removePrefix("0x").matches("[a-fA-F0-9]{8}".toRegex())) R.string.short_gpg_id
+ else R.string.invalid_gpg_id
+ // A toast on the application context, not a snackbar: the autofill decryption
+ // finishes its window right after, and the toast must outlive it.
+ Toast.makeText(applicationContext, message, Toast.LENGTH_LONG).show()
return null
}
}
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 e5341320..02b112f4 100644
--- a/app/src/main/java/app/passwordstore/ui/entry/EntryDestination.kt
+++ b/app/src/main/java/app/passwordstore/ui/entry/EntryDestination.kt
@@ -43,10 +43,10 @@ import androidx.hilt.navigation.compose.hiltViewModel
import app.passwordstore.R
import app.passwordstore.data.passfile.Totp
import app.passwordstore.data.repo.PasswordRepository
-import app.passwordstore.ui.crypto.BasePGPActivity.Companion.getParentPath
import app.passwordstore.ui.crypto.copyPasswordToClipboard
import app.passwordstore.ui.crypto.copyTextToClipboard
import app.passwordstore.ui.crypto.getPGPIdentifiers
+import app.passwordstore.ui.crypto.getParentPath
import app.passwordstore.ui.main.LaunchActivity
import app.passwordstore.ui.main.MainActivity
import app.passwordstore.ui.setup.SetupStart
diff --git a/app/src/main/java/app/passwordstore/ui/list/StoreActions.kt b/app/src/main/java/app/passwordstore/ui/list/StoreActions.kt
index 2e148401..3bdede73 100644
--- a/app/src/main/java/app/passwordstore/ui/list/StoreActions.kt
+++ b/app/src/main/java/app/passwordstore/ui/list/StoreActions.kt
@@ -9,7 +9,7 @@ import android.content.Context
import androidx.core.content.edit
import app.passwordstore.R
import app.passwordstore.data.repo.PasswordRepository
-import app.passwordstore.ui.crypto.BasePGPActivity.Companion.getLongName
+import app.passwordstore.ui.crypto.getLongName
import app.passwordstore.ui.git.base.BaseGitActivity
import app.passwordstore.util.autofill.AutofillMatcher
import app.passwordstore.util.extensions.base64
diff --git a/app/src/main/java/app/passwordstore/ui/main/LaunchActivity.kt b/app/src/main/java/app/passwordstore/ui/main/LaunchActivity.kt
index 6f07a51a..c0af5742 100644
--- a/app/src/main/java/app/passwordstore/ui/main/LaunchActivity.kt
+++ b/app/src/main/java/app/passwordstore/ui/main/LaunchActivity.kt
@@ -12,7 +12,6 @@ import android.os.Looper
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.edit
import app.passwordstore.data.repo.PasswordRepository
-import app.passwordstore.ui.crypto.BasePGPActivity
import app.passwordstore.util.auth.BiometricAuthenticator
import app.passwordstore.util.auth.BiometricAuthenticator.Result
import app.passwordstore.util.extensions.sharedPrefs
@@ -61,10 +60,10 @@ class LaunchActivity : AppCompatActivity() {
// This activity is exported, so the file path is untrusted input: only forward it when it
// points at a file inside the password store. MainActivity is not exported and trusts it.
val repoDir = PasswordRepository.getRepositoryDirectory()
- val filePath = intent.getStringExtra(BasePGPActivity.EXTRA_FILE_PATH)
+ val filePath = intent.getStringExtra(EXTRA_FILE_PATH)
val intentToStart = Intent(this, MainActivity::class.java)
if (intent.action == ACTION_DECRYPT_PASS && isFileInRepo(filePath, repoDir))
- intentToStart.putExtra(BasePGPActivity.EXTRA_FILE_PATH, filePath)
+ intentToStart.putExtra(EXTRA_FILE_PATH, filePath)
// A plain start replaces the task, so an instance already open (one whose store vanished, say)
// is not left below the new one. An entry opens on top, back returns to where the user was.
else intentToStart.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
@@ -77,6 +76,9 @@ class LaunchActivity : AppCompatActivity() {
const val ACTION_DECRYPT_PASS = "DECRYPT_PASS"
+ /** The entry to open; home screen shortcuts carry this exact key, never change it. */
+ const val EXTRA_FILE_PATH = "FILE_PATH"
+
/** True when [path] resolves, after following `..` and symlinks, to a file under [repoDir]. */
fun isFileInRepo(path: String?, repoDir: File): Boolean {
if (path.isNullOrEmpty()) return false
diff --git a/app/src/main/java/app/passwordstore/ui/main/MainActivity.kt b/app/src/main/java/app/passwordstore/ui/main/MainActivity.kt
index a381ca84..ce5fb1df 100644
--- a/app/src/main/java/app/passwordstore/ui/main/MainActivity.kt
+++ b/app/src/main/java/app/passwordstore/ui/main/MainActivity.kt
@@ -21,7 +21,6 @@ import androidx.navigation.toRoute
import app.passwordstore.data.password.PasswordItem
import app.passwordstore.data.repo.PasswordRepository
import app.passwordstore.ui.compose.theme.APSTheme
-import app.passwordstore.ui.crypto.BasePGPActivity
import app.passwordstore.ui.editor.EditorDestination
import app.passwordstore.ui.editor.EditorForm
import app.passwordstore.ui.editor.editorForm
@@ -84,7 +83,7 @@ class MainActivity : BaseGitActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- val filePath = intent.getStringExtra(BasePGPActivity.EXTRA_FILE_PATH)
+ val filePath = intent.getStringExtra(LaunchActivity.EXTRA_FILE_PATH)
val setup =
intent.getStringExtra(EXTRA_SETUP)?.let { name ->
SetupStart.entries.firstOrNull { it.name == name }
diff --git a/app/src/test/java/app/passwordstore/ui/crypto/CryptoExtensionsTest.kt b/app/src/test/java/app/passwordstore/ui/crypto/CryptoExtensionsTest.kt
new file mode 100644
index 00000000..fee6bf72
--- /dev/null
+++ b/app/src/test/java/app/passwordstore/ui/crypto/CryptoExtensionsTest.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2026 Danilo M. <danix@danix.xyz>
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+package app.passwordstore.ui.crypto
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+
+/** Pins the path helpers' behavior across their move out of BasePGPActivity. */
+class CryptoExtensionsTest {
+
+ @Test
+ fun parentPathIsRelativeToTheStore() {
+ assertEquals("/social/", getParentPath("/store/social/facebook.gpg", "/store"))
+ assertEquals("/", getParentPath("/store/example.org.gpg", "/store"))
+ }
+
+ @Test
+ fun longNameJoinsFolderAndName() {
+ // StoreActions passes absolute folders, PasswordItem its path relative to the store.
+ assertEquals("work/x", getLongName("/store/work", "/store", "x"))
+ assertEquals("social/facebook", getLongName("/social/", "/store", "facebook"))
+ assertEquals("x", getLongName("/store", "/store", "x"))
+ }
+}