diff options
Diffstat (limited to 'app/src')
| -rw-r--r-- | app/src/main/java/app/passwordstore/data/password/PasswordItem.kt | 6 | ||||
| -rw-r--r-- | app/src/main/java/app/passwordstore/ui/autofill/AutofillDecryptActivity.kt | 16 | ||||
| -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.kt | 2 | ||||
| -rw-r--r-- | app/src/main/java/app/passwordstore/ui/list/StoreActions.kt | 2 | ||||
| -rw-r--r-- | app/src/main/java/app/passwordstore/ui/main/LaunchActivity.kt | 8 | ||||
| -rw-r--r-- | app/src/main/java/app/passwordstore/ui/main/MainActivity.kt | 3 | ||||
| -rw-r--r-- | app/src/test/java/app/passwordstore/ui/crypto/CryptoExtensionsTest.kt | 27 |
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")) + } +} |
