From 59aa8d8ee80220e08d6c3dafc031237ed573b5cb Mon Sep 17 00:00:00 2001 From: Alexander Gil Date: Tue, 27 Oct 2026 22:46:16 +0100 Subject: [PATCH 1/3] Revert "fix(passkeys): resolve passkey storage root from PasswordRepository (adapts upstream #1168)" This reverts commit 4f33963bbb61b14b39169a945fe64df5a7f9e6df. --- .../injection/passkeys/PasskeysModule.kt | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/app/passwordstore/injection/passkeys/PasskeysModule.kt b/app/src/main/java/app/passwordstore/injection/passkeys/PasskeysModule.kt index 832c22c974..4994fbb2ec 100644 --- a/app/src/main/java/app/passwordstore/injection/passkeys/PasskeysModule.kt +++ b/app/src/main/java/app/passwordstore/injection/passkeys/PasskeysModule.kt @@ -16,7 +16,6 @@ import app.passwordstore.crypto.PGPainlessCryptoHandler import app.passwordstore.crypto.PgpainlessPasskeyDecryptor import app.passwordstore.data.crypto.OpenPgpInteractionCoordinator import app.passwordstore.data.crypto.OpenPgpProviderRepository -import app.passwordstore.data.repo.PasswordRepository import app.passwordstore.injection.prefs.SettingsPreferences import app.passwordstore.passkeys.BiometricPasskeyAuthenticator import app.passwordstore.passkeys.DefaultRepositoryGenerationProvider @@ -47,6 +46,7 @@ import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent +import java.io.File import javax.inject.Singleton @Module @@ -89,17 +89,20 @@ object PasskeysModule { @Provides @Singleton - fun provideRepositoryGenerationProvider(): RepositoryGenerationProvider = - DefaultRepositoryGenerationProvider(PasswordRepository.getRepositoryDirectory()) + fun provideRepositoryGenerationProvider( + @ApplicationContext context: Context + ): RepositoryGenerationProvider = + DefaultRepositoryGenerationProvider(File(context.filesDir, "store")) @Provides @Singleton fun providePassRecipientResolver( + @ApplicationContext context: Context, keyManager: PGPKeyManager, providerRepository: OpenPgpProviderRepository, interactionCoordinator: OpenPgpInteractionCoordinator, ): PassRecipientResolver { - val repositoryRoot = PasswordRepository.getRepositoryDirectory() + val repositoryRoot = File(context.filesDir, "store") val localResolver = DefaultPassRecipientResolver(repositoryRoot, keyManager) return OpenPgpPassRecipientResolver( localResolver, @@ -111,6 +114,7 @@ object PasskeysModule { @Provides @Singleton fun providePasskeyStorage( + @ApplicationContext context: Context, cryptoHandler: PGPainlessCryptoHandler, passkeyPgpDecryptor: PasskeyPgpDecryptor, pgpUnlockContext: PgpUnlockContext, @@ -118,7 +122,7 @@ object PasskeysModule { generationProvider: RepositoryGenerationProvider, metadataIndex: PasskeyMetadataIndex, ): PasskeyStorage { - val repositoryRoot = PasswordRepository.getRepositoryDirectory() + val repositoryRoot = File(context.filesDir, "store") val passkeyConfig = PasskeyStorageConfig(passkeyDirectory = "fido2", fileExtension = ".gpg") val fileStorage = FilePasskeyStorage( From 46499a81140931cf8359c630a2987f8810dae186 Mon Sep 17 00:00:00 2001 From: Alexander Gil Date: Tue, 27 Oct 2026 22:46:39 +0100 Subject: [PATCH 2/3] chore: remove external repository option (reverts upstream #1168 port) This fork syncs only through Git. Storing the password store, and with it passkeys, in shared Documents storage exposes passkey metadata to other apps, undermines the confined passkey file store and can lose access after a reinstall. The option was never released, so no migration is needed. Reverts 0c12eb60 and its passkey storage-root follow-up 4f33963b. --- CHANGELOG.md | 1 - .../java/app/passwordstore/Application.kt | 4 +- .../data/repo/PasswordRepository.kt | 23 +-- .../ui/onboarding/fragments/CloneFragment.kt | 66 +++++++- .../fragments/RepoVisibilityFragment.kt | 144 ------------------ .../ui/passwords/PasswordStore.kt | 6 +- .../ui/settings/RepositorySettings.kt | 19 +-- .../passwordstore/util/settings/Migrations.kt | 16 ++ .../util/settings/PreferenceKeys.kt | 6 +- .../SearchableRepositoryViewModel.kt | 4 +- .../res/layout/fragment_repo_visibility.xml | 119 --------------- app/src/main/res/values-de/strings.xml | 17 +-- app/src/main/res/values/strings.xml | 21 +-- .../util/settings/MigrationsTest.kt | 30 ++++ docs/upstream-reconciliation.md | 2 +- 15 files changed, 135 insertions(+), 343 deletions(-) delete mode 100644 app/src/main/java/app/passwordstore/ui/onboarding/fragments/RepoVisibilityFragment.kt delete mode 100644 app/src/main/res/layout/fragment_repo_visibility.xml diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d3b6b7cfd..1ef79abdbd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,6 @@ All notable changes to this project will be documented in this file ### Added -- Option to keep the password store in a shared external folder (`Documents/PasswordStore`); passkeys follow the selected repository - Setting to enable Autofill directly from the app settings - Dialog showing the SSH server's authentication banner, if any - Android 17: runtime request for the local network permission and a connectivity check for local Git servers diff --git a/app/src/main/java/app/passwordstore/Application.kt b/app/src/main/java/app/passwordstore/Application.kt index de52aef4db..fb110626a8 100644 --- a/app/src/main/java/app/passwordstore/Application.kt +++ b/app/src/main/java/app/passwordstore/Application.kt @@ -18,6 +18,7 @@ import android.os.StrictMode import android.view.View import android.view.WindowInsetsController import androidx.appcompat.app.AppCompatDelegate +import app.passwordstore.injection.context.FilesDirPath import app.passwordstore.injection.prefs.PGPPassphrases import app.passwordstore.injection.prefs.SettingsPreferences import app.passwordstore.passkeys.PasskeyPassphraseCache @@ -47,6 +48,7 @@ import logcat.logcat class Application : android.app.Application(), SharedPreferences.OnSharedPreferenceChangeListener { @Inject @SettingsPreferences lateinit var prefs: SharedPreferences + @Inject @FilesDirPath lateinit var filesDirPath: String @Inject @PGPPassphrases lateinit var persistentPassphrases: SharedPreferences @Inject lateinit var dispatcherProvider: DispatcherProvider @Inject lateinit var gitSettings: GitSettings @@ -71,7 +73,7 @@ class Application : android.app.Application(), SharedPreferences.OnSharedPrefere logcat { "Debug logging enabled." } prefs.registerOnSharedPreferenceChangeListener(this) setNightMode() - runMigrations(prefs, gitSettings, persistentPassphrases) + runMigrations(filesDirPath, prefs, gitSettings, persistentPassphrases) proxyUtils.setDefaultProxy() DynamicColors.applyToActivitiesIfAvailable(this) setupScreenOffHandler() diff --git a/app/src/main/java/app/passwordstore/data/repo/PasswordRepository.kt b/app/src/main/java/app/passwordstore/data/repo/PasswordRepository.kt index 5250a809ef..7e230e8bfa 100644 --- a/app/src/main/java/app/passwordstore/data/repo/PasswordRepository.kt +++ b/app/src/main/java/app/passwordstore/data/repo/PasswordRepository.kt @@ -4,7 +4,6 @@ */ package app.passwordstore.data.repo -import android.os.Environment import androidx.core.content.edit import app.passwordstore.Application import app.passwordstore.data.password.PasswordItem @@ -117,30 +116,12 @@ object PasswordRepository { fun closeRepository() { repository?.close() repository = null - settings.edit { - putBoolean(PreferenceKeys.REPOSITORY_INITIALIZED, false) - putBoolean(PreferenceKeys.PREF_SHARED_REPO, false) - } } - fun deleteRepository() { - getRepositoryDirectory().let { dir -> - dir.deleteRecursively() - } - closeRepository() + fun getRepositoryDirectory(): File { + return File(filesDir.toString(), "/store") } - fun getRepositoryDirectory(): File = - if (settings.getBoolean(PreferenceKeys.PREF_SHARED_REPO, false)) - File( - File( - Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS), - "PasswordStore", - ), - "store", - ) - else File(filesDir, "store") - /* referencePath: /a/b/c * /a/b/c/d/e/dir -> /d/e/dir * /a/b/c/d/e/file.ext -> /d/e/file.ext diff --git a/app/src/main/java/app/passwordstore/ui/onboarding/fragments/CloneFragment.kt b/app/src/main/java/app/passwordstore/ui/onboarding/fragments/CloneFragment.kt index e460e256bc..47f86be068 100644 --- a/app/src/main/java/app/passwordstore/ui/onboarding/fragments/CloneFragment.kt +++ b/app/src/main/java/app/passwordstore/ui/onboarding/fragments/CloneFragment.kt @@ -7,31 +7,83 @@ package app.passwordstore.ui.onboarding.fragments import android.os.Bundle import android.view.View +import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.edit import androidx.core.view.ViewCompat import androidx.fragment.app.Fragment import app.passwordstore.R +import app.passwordstore.data.repo.PasswordRepository import app.passwordstore.databinding.FragmentCloneBinding +import app.passwordstore.ui.git.config.GitServerConfigActivity +import app.passwordstore.util.extensions.finish import app.passwordstore.util.extensions.performTransactionWithBackStack +import app.passwordstore.util.extensions.sharedPrefs +import app.passwordstore.util.extensions.unsafeLazy import app.passwordstore.util.extensions.viewBinding import app.passwordstore.util.extensions.windowInsetsLambda +import app.passwordstore.util.settings.PreferenceKeys +import com.github.michaelbull.result.onErr +import com.github.michaelbull.result.runCatching +import java.io.File +import logcat.LogPriority.ERROR +import logcat.asLog +import logcat.logcat class CloneFragment : Fragment(R.layout.fragment_clone) { private val binding by viewBinding(FragmentCloneBinding::bind) + private val settings by unsafeLazy { requireActivity().applicationContext.sharedPrefs } + + private val cloneAction = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + if (result.resultCode == AppCompatActivity.RESULT_OK) { + if (File(PasswordRepository.getRepositoryDirectory(), ".gpg-id").isFile()) { + settings.edit { putBoolean(PreferenceKeys.REPOSITORY_INITIALIZED, true) } + finish() + } else { + // non-pass repository --> go to key selection + runCatching { + parentFragmentManager.performTransactionWithBackStack( + KeySelectionFragment.newInstance() + ) + } + .onErr { e -> logcat(ERROR) { e.asLog() } } + } + } + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) ViewCompat.setOnApplyWindowInsetsListener(view, windowInsetsLambda) - binding.cloneRemote.setOnClickListener { - parentFragmentManager.performTransactionWithBackStack(RepoVisibilityFragment.newInstance()) - } + binding.cloneRemote.setOnClickListener { cloneToHiddenDir() } + binding.createLocal.setOnClickListener { createRepository() } + } + + /** Clones a remote Git repository to the app's private directory */ + private fun cloneToHiddenDir() { + cloneAction.launch(GitServerConfigActivity.createCloneIntent(requireContext())) + } - binding.createLocal.setOnClickListener { - parentFragmentManager.performTransactionWithBackStack( - RepoVisibilityFragment.newInstance(shouldClone = false) - ) + private fun createRepository() { + val localDir = PasswordRepository.getRepositoryDirectory() + runCatching { + check(localDir.exists() || localDir.mkdir()) { "Failed to create directory!" } + PasswordRepository.createRepository(localDir) + if (!PasswordRepository.isInitialized) { + PasswordRepository.initialize() + } + parentFragmentManager.performTransactionWithBackStack(KeySelectionFragment.newInstance()) } + .onErr { e -> + logcat(ERROR) { e.asLog() } + if (!localDir.delete()) { + logcat { "Failed to delete local repository: $localDir" } + } + finish() + } } companion object { diff --git a/app/src/main/java/app/passwordstore/ui/onboarding/fragments/RepoVisibilityFragment.kt b/app/src/main/java/app/passwordstore/ui/onboarding/fragments/RepoVisibilityFragment.kt deleted file mode 100644 index fbc174454a..0000000000 --- a/app/src/main/java/app/passwordstore/ui/onboarding/fragments/RepoVisibilityFragment.kt +++ /dev/null @@ -1,144 +0,0 @@ -/* - * Copyright © 2014-2026 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ - -package app.passwordstore.ui.onboarding.fragments - -import android.os.Bundle -import android.view.View -import androidx.activity.result.contract.ActivityResultContracts -import androidx.appcompat.app.AppCompatActivity -import androidx.core.content.edit -import androidx.core.view.ViewCompat -import androidx.fragment.app.Fragment -import app.passwordstore.R -import app.passwordstore.data.repo.PasswordRepository -import app.passwordstore.databinding.FragmentRepoVisibilityBinding -import app.passwordstore.ui.git.config.GitServerConfigActivity -import app.passwordstore.util.extensions.finish -import app.passwordstore.util.extensions.performTransactionWithBackStack -import app.passwordstore.util.extensions.sharedPrefs -import app.passwordstore.util.extensions.unsafeLazy -import app.passwordstore.util.extensions.viewBinding -import app.passwordstore.util.extensions.windowInsetsLambda -import app.passwordstore.util.settings.PreferenceKeys -import com.github.michaelbull.result.onErr -import com.github.michaelbull.result.runCatching -import java.io.File -import logcat.LogPriority.ERROR -import logcat.asLog -import logcat.logcat - -class RepoVisibilityFragment : Fragment(R.layout.fragment_repo_visibility) { - - private val binding by viewBinding(FragmentRepoVisibilityBinding::bind) - - private val settings by unsafeLazy { requireActivity().applicationContext.sharedPrefs } - - private val cloneAction = - registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> - if (result.resultCode == AppCompatActivity.RESULT_OK) { - if (File(PasswordRepository.getRepositoryDirectory(), ".gpg-id").isFile()) { - settings.edit { putBoolean(PreferenceKeys.REPOSITORY_INITIALIZED, true) } - finish() - } else { - // non-pass repository --> go to key selection - runCatching { - parentFragmentManager.performTransactionWithBackStack( - KeySelectionFragment.newInstance() - ) - } - .onErr { e -> logcat(ERROR) { e.asLog() } } - } - } - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - ViewCompat.setOnApplyWindowInsetsListener(view, windowInsetsLambda) - - with(binding) { - if (requireArguments().getBoolean(SHOULD_CLONE_EXTRA)) - repoVisibilityText.setText(R.string.select_repo_visibility_clone_text) - else repoVisibilityText.setText(R.string.select_repo_visibility_create_text) - - internalRepo.setOnCheckedChangeListener { _, isChecked -> - if (isChecked) { - repoSharedParent.visibility = View.INVISIBLE - } - } - - sharedRepo.setOnCheckedChangeListener { _, isChecked -> - if (isChecked) { - repoSharedParent.text = getString(R.string.shared_repo_path, android.os.Build.MODEL) - repoSharedParent.visibility = View.VISIBLE - } - } - - continueButton.setOnClickListener { - settings.edit { putBoolean(PreferenceKeys.PREF_SHARED_REPO, sharedRepo.isChecked) } - if (requireArguments().getBoolean(SHOULD_CLONE_EXTRA)) cloneRepository() - else createRepository() - } - } - } - - /** Clones a remote Git repository */ - private fun cloneRepository() { - val localDir = PasswordRepository.getRepositoryDirectory() - runCatching { - check(localDir.exists() || localDir.mkdirs()) { "Failed to create directory!" } - PasswordRepository.initialize() - cloneAction.launch(GitServerConfigActivity.createCloneIntent(requireContext())) - } - .onErr { e -> - logcat(ERROR) { e.asLog() } - if (!localDir.delete()) { - logcat { "Failed to delete local repository: $localDir" } - } - finish() - } - } - - /** Fresh Git repository */ - private fun createRepository() { - val localDir = PasswordRepository.getRepositoryDirectory() - runCatching { - check(localDir.exists() || localDir.mkdirs()) { "Failed to create directory!" } - PasswordRepository.createRepository(localDir) - PasswordRepository.initialize() - if (File(PasswordRepository.getRepositoryDirectory(), ".gpg-id").isFile()) { - settings.edit { putBoolean(PreferenceKeys.REPOSITORY_INITIALIZED, true) } - finish() - } else { - // non-pass repository --> go to key selection - parentFragmentManager.performTransactionWithBackStack(KeySelectionFragment.newInstance()) - } - } - .onErr { e -> - logcat(ERROR) { e.asLog() } - if (!localDir.delete()) { - logcat { "Failed to delete local repository: $localDir" } - } - finish() - } - } - - companion object { - - private const val SHOULD_CLONE_EXTRA = "SHOULD_CLONE" - - fun newInstance(shouldClone: Boolean = true): RepoVisibilityFragment { - val extras = - Bundle().also { - it.apply { - putBoolean(SHOULD_CLONE_EXTRA, shouldClone) - } - } - val fragment = RepoVisibilityFragment() - fragment.arguments = extras - return fragment - } - } -} diff --git a/app/src/main/java/app/passwordstore/ui/passwords/PasswordStore.kt b/app/src/main/java/app/passwordstore/ui/passwords/PasswordStore.kt index 2c83caa1e4..10a5236f41 100644 --- a/app/src/main/java/app/passwordstore/ui/passwords/PasswordStore.kt +++ b/app/src/main/java/app/passwordstore/ui/passwords/PasswordStore.kt @@ -686,7 +686,7 @@ class PasswordStore : BaseGitActivity() { PasswordRepository.getRelativePath(target.absolutePath, repoPath) } if (relativeTargetPath != null) { - runCatching { model.reset() } + model.reset() model.navigateTo(PasswordRepository.getRepositoryDirectory(), pushPreviousLocation = false) relativeTargetPath.trim('/').split('/').forEach { item -> val file = File(model.currentDir.value, item) @@ -696,9 +696,9 @@ class PasswordStore : BaseGitActivity() { } else getPasswordFragment()?.scrollToOnNextRefresh(file) } } else if (model.currentDir.value.isDirectory) { - runCatching { model.forceRefresh() } + model.forceRefresh() } else { - runCatching { model.reset() } + model.reset() } supportActionBar?.setDisplayHomeAsUpEnabled(model.canNavigateBack) updateFabSync() diff --git a/app/src/main/java/app/passwordstore/ui/settings/RepositorySettings.kt b/app/src/main/java/app/passwordstore/ui/settings/RepositorySettings.kt index 761441b5bc..15efca4e9d 100644 --- a/app/src/main/java/app/passwordstore/ui/settings/RepositorySettings.kt +++ b/app/src/main/java/app/passwordstore/ui/settings/RepositorySettings.kt @@ -9,7 +9,6 @@ import android.content.Context import android.content.Intent import android.content.pm.ShortcutManager import android.net.Uri -import android.os.Build import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult import androidx.core.content.edit @@ -306,19 +305,9 @@ class RepositorySettings(private val activity: FragmentActivity) : SettingsProvi summaryOnRes = R.string.pref_rebase_on_pull_summary_on defaultValue = true } - - switch(PreferenceKeys.PREF_SHARED_REPO) { - titleRes = R.string.prefs_external_repository_title - summaryRes = R.string.prefs_external_repository_summary - summaryOn = activity.getString(R.string.shared_repo_path, android.os.Build.MODEL) - disableDependents = true - defaultValue = false - } - pref(PreferenceKeys.EXPORT_PASSWORDS) { titleRes = R.string.prefs_export_passwords_title summaryRes = R.string.prefs_export_passwords_summary - dependency = PreferenceKeys.PREF_SHARED_REPO onClick { storeExportAction.launch(null) true @@ -327,7 +316,6 @@ class RepositorySettings(private val activity: FragmentActivity) : SettingsProvi pref(PreferenceKeys.IMPORT_PASSWORDS) { titleRes = R.string.prefs_import_passwords_title summaryRes = R.string.prefs_import_passwords_summary - dependency = PreferenceKeys.PREF_SHARED_REPO onClick { if (PasswordRepository.isEmpty()) { storeImportAction.launch(null) @@ -345,7 +333,6 @@ class RepositorySettings(private val activity: FragmentActivity) : SettingsProvi pref(PreferenceKeys.GIT_DELETE_REPO) { titleRes = R.string.pref_git_delete_repo_title summaryRes = R.string.pref_git_delete_repo_summary - dependency = PreferenceKeys.PREF_SHARED_REPO onClick { val repoDir = PasswordRepository.getRepositoryDirectory() MaterialAlertDialogBuilder(activity) @@ -354,7 +341,11 @@ class RepositorySettings(private val activity: FragmentActivity) : SettingsProvi .setCancelable(false) .setPositiveButton(R.string.dialog_delete) { dialogInterface, _ -> runCatching { - PasswordRepository.deleteRepository() + PasswordRepository.closeRepository() + PasswordRepository.getRepositoryDirectory().let { dir -> + dir.deleteRecursively() + dir.mkdirs() + } } .onErr { it.message?.let { message -> activity.snackbar(message = message) } } diff --git a/app/src/main/java/app/passwordstore/util/settings/Migrations.kt b/app/src/main/java/app/passwordstore/util/settings/Migrations.kt index 9f55a7b781..9dc4d6bc44 100644 --- a/app/src/main/java/app/passwordstore/util/settings/Migrations.kt +++ b/app/src/main/java/app/passwordstore/util/settings/Migrations.kt @@ -28,6 +28,7 @@ import logcat.logcat private const val TAG = "Migrations" fun runMigrations( + filesDirPath: String, sharedPrefs: SharedPreferences, gitSettings: GitSettings, persistentPassphrases: SharedPreferences, @@ -38,6 +39,7 @@ fun runMigrations( migrateToHideAll(sharedPrefs) migrateToClipboardHistory(sharedPrefs) migrateToDiceware(sharedPrefs) + removeExternalStorageProperties(sharedPrefs) removeCurrentBranchValue(sharedPrefs) removePersistentCredentialCache(sharedPrefs, gitSettings, context, runTest) if (!runTest) moveToPasswordGeneratorPrefs(sharedPrefs, context) @@ -267,6 +269,20 @@ private fun migrateToDiceware(sharedPrefs: SharedPreferences) { } } +private fun removeExternalStorageProperties(prefs: SharedPreferences) { + prefs.edit { + if (prefs.contains(PreferenceKeys.GIT_EXTERNAL)) { + if (prefs.getBoolean(PreferenceKeys.GIT_EXTERNAL, false)) { + putBoolean(PreferenceKeys.GIT_EXTERNAL_MIGRATED, true) + } + remove(PreferenceKeys.GIT_EXTERNAL) + } + if (prefs.contains(PreferenceKeys.GIT_EXTERNAL_REPO)) { + remove(PreferenceKeys.GIT_EXTERNAL_REPO) + } + } +} + private fun createEncryptedPreferences(context: Context, fileName: String): SharedPreferences { val masterKeyAlias = MasterKey.Builder(context).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build() diff --git a/app/src/main/java/app/passwordstore/util/settings/PreferenceKeys.kt b/app/src/main/java/app/passwordstore/util/settings/PreferenceKeys.kt index f29ca5ee15..de7c84bcca 100644 --- a/app/src/main/java/app/passwordstore/util/settings/PreferenceKeys.kt +++ b/app/src/main/java/app/passwordstore/util/settings/PreferenceKeys.kt @@ -30,8 +30,12 @@ object PreferenceKeys { const val GIT_CONFIG_AUTHOR_NAME = "git_config_user_name" const val GIT_SHOW_SSH_SERVER_RESPONSE = "git_show_ssh_server_response" - const val PREF_SHARED_REPO = "pref_shared_repo" + @Deprecated(message = "We're removing support for external storage") + const val GIT_EXTERNAL = "git_external" + @Deprecated(message = "We're removing support for external storage") + const val GIT_EXTERNAL_REPO = "git_external_repo" + const val GIT_EXTERNAL_MIGRATED = "git_external_migrated" const val GIT_REMOTE_AUTH = "git_remote_auth" const val GIT_REMOTE_KEY_TYPE = "git_remote_key_type" diff --git a/app/src/main/java/app/passwordstore/util/viewmodel/SearchableRepositoryViewModel.kt b/app/src/main/java/app/passwordstore/util/viewmodel/SearchableRepositoryViewModel.kt index 8853635587..f52e5b6903 100644 --- a/app/src/main/java/app/passwordstore/util/viewmodel/SearchableRepositoryViewModel.kt +++ b/app/src/main/java/app/passwordstore/util/viewmodel/SearchableRepositoryViewModel.kt @@ -327,9 +327,7 @@ constructor( ) { require(newDirectory.isInsideRepository()) { "Trying to access target outside the repository" } if (!newDirectory.exists()) return - require(newDirectory.isDirectory) { - "Cannot navigate into non-directory ${newDirectory}" - } + require(newDirectory.isDirectory) { "Can only navigate to a directory" } if (pushPreviousLocation) { navigationStack.addFirst(NavigationStackEntry(_currentDir.value, recyclerViewState)) } diff --git a/app/src/main/res/layout/fragment_repo_visibility.xml b/app/src/main/res/layout/fragment_repo_visibility.xml deleted file mode 100644 index 051edbe69b..0000000000 --- a/app/src/main/res/layout/fragment_repo_visibility.xml +++ /dev/null @@ -1,119 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 4eb6b6fa7e..c72c3a34f7 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -135,9 +135,7 @@ Verschiedenes Zwischenablagenverlauf umgehen Zwischenablagenverlauf (Samsung-Geräte, Gboard) mit Zufallszahlen überschreiben, um ggf. bestehende Passwörter zu entfernen. - Löscht das versteckte, app-interne Repository. - Externes Repository - Das Passwort-Repository befindet sich in einem freigegebenen Ordner, auf den auch andere Apps zugreifen können. + Lösche das lokale (versteckte) Repository. Passwörter exportieren Exportiert das Password-Repository in ein externes Verzeichnis. Passwörter werden exportiert @@ -214,7 +212,6 @@ Ja Nein Abbruch - Weiter Synchronisiere Repository Git Pull Git Push @@ -381,21 +378,15 @@ Ein Ordner mit diesem Namen existiert bereits. - Los geht’s - Repository-Typ - Wählen Sie aus, ob Sie ein Remote-Repository klonen oder ein lokales Repository anlegen möchten. + Los geht\'s + Wählen Sie einen \nRepository-Typ. + Wählen Sie aus, ob Sie ein lokales Repository anlegen, oder ein Remote-Repository klonen möchten. Remote-Repository klonen Lokales Repository anlegen PGP-Schlüssel wählen Wählen Sie einen oder mehrere PGP-Schlüssel, mit denen der Store initialisiert werden soll. Schlüssel auswählen Schlüssel auswählen - Sichtbarkeit des Repositorys - Legen Sie fest, ob das Repository in das interne Verzeichnis der App (empfohlen) oder in einen freigegebenen Ordner geklont werden soll, auf den auch andere Apps zugreifen können. - Legen Sie fest, ob das Repository innerhalb des internen Verzeichnisses der App (empfohlen) oder in einem freigegebenen Ordner angelegt werden soll, auf den auch andere Apps zugreifen können. - app-intern (versteckt) - freigegebener Ordner - Möglicherweise inkorrekte URL diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 09d641a9af..5d298e25f8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -142,9 +142,7 @@ Automatically sync passkeys with Git remote when repository is configured. Scramble clipboard history Flood clipboard history (Samsung devices, Gboard) with random numbers, wiping out any passwords. - Deletes the hidden, app-internal repository. - External repository - The password repository is located in a shared folder that other apps can also access. + Deletes local (hidden) repository. Export repository Exports the password repository to an external directory. Exporting passwords @@ -227,7 +225,6 @@ Yes No Cancel - Continue Synchronise repository Pull from remote Push to remote @@ -401,21 +398,15 @@ A folder by that name already exists - Let’s Go - Repository type - Choose whether you want to clone a remote repo or create a local repo. - Clone remote repo - Create local repo + Let\'s Go + Select \nRepository Type + Select if you want to create a local repo or clone a remote repo. + Clone Remote Repo + Create Local Repo Select\nPGP key(s) Select one or multiple PGP keys that you want to initialise the store with. Select key(s) Select key - Repository visibility - Specify whether you want to clone the repository to the app’s internal directory (recommended) or to a shared folder that other apps can also access. - Specify whether you want to create the repository within the app’s internal directory (recommended) or in a shared folder that other apps can also access. - app-internal (hidden) - shared folder - %1$s › Documents › PasswordStore Potentially incorrect URL diff --git a/app/src/test/java/app/passwordstore/util/settings/MigrationsTest.kt b/app/src/test/java/app/passwordstore/util/settings/MigrationsTest.kt index 4b02d0190a..04463d3b70 100644 --- a/app/src/test/java/app/passwordstore/util/settings/MigrationsTest.kt +++ b/app/src/test/java/app/passwordstore/util/settings/MigrationsTest.kt @@ -16,6 +16,7 @@ import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertNull +import kotlin.test.assertTrue import org.junit.Rule import org.junit.rules.TemporaryFolder @@ -57,6 +58,7 @@ class MigrationsTest { putString(PreferenceKeys.GIT_REMOTE_AUTH, AuthMode.Password.pref) } runMigrations( + filesDir, sharedPrefs, GitSettings(sharedPrefs, gitSecrets, filesDir), sharedPrefs, @@ -80,6 +82,7 @@ class MigrationsTest { putString(PreferenceKeys.GIT_REMOTE_AUTH, AuthMode.SshKey.pref) } runMigrations( + filesDir, sharedPrefs, GitSettings(sharedPrefs, gitSecrets, filesDir), sharedPrefs, @@ -103,6 +106,7 @@ class MigrationsTest { putString(PreferenceKeys.GIT_REMOTE_AUTH, AuthMode.None.pref) } runMigrations( + filesDir, sharedPrefs, GitSettings(sharedPrefs, gitSecrets, filesDir), sharedPrefs, @@ -119,6 +123,7 @@ class MigrationsTest { @Test fun verifyHiddenFoldersMigrationIfDisabled() { runMigrations( + filesDir, sharedPrefs, GitSettings(sharedPrefs, gitSecrets, filesDir), sharedPrefs, @@ -133,6 +138,7 @@ class MigrationsTest { fun verifyHiddenFoldersMigrationIfEnabled() { sharedPrefs.edit { putBoolean(PreferenceKeys.SHOW_HIDDEN_FOLDERS, true) } runMigrations( + filesDir, sharedPrefs, GitSettings(sharedPrefs, gitSecrets, filesDir), sharedPrefs, @@ -147,6 +153,7 @@ class MigrationsTest { fun verifyClearClipboardHistoryMigration() { sharedPrefs.edit { putBoolean(PreferenceKeys.CLEAR_CLIPBOARD_20X, true) } runMigrations( + filesDir, sharedPrefs, GitSettings(sharedPrefs, gitSecrets, filesDir), sharedPrefs, @@ -161,6 +168,7 @@ class MigrationsTest { fun verifyClassicPasswordGeneratorMigration() { sharedPrefs.edit { putString(PreferenceKeys.PREF_KEY_PWGEN_TYPE, "classic") } runMigrations( + filesDir, sharedPrefs, GitSettings(sharedPrefs, gitSecrets, filesDir), sharedPrefs, @@ -174,6 +182,7 @@ class MigrationsTest { fun verifyXkPasswdPasswordGeneratorMigration() { sharedPrefs.edit { putString(PreferenceKeys.PREF_KEY_PWGEN_TYPE, "xkpasswd") } runMigrations( + filesDir, sharedPrefs, GitSettings(sharedPrefs, gitSecrets, filesDir), sharedPrefs, @@ -183,6 +192,25 @@ class MigrationsTest { assertEquals("diceware", sharedPrefs.getString(PreferenceKeys.PREF_KEY_PWGEN_TYPE)) } + @Test + fun verifyExternalStorageMigration() { + sharedPrefs.edit { + putBoolean(PreferenceKeys.GIT_EXTERNAL, true) + putString(PreferenceKeys.GIT_EXTERNAL_REPO, "/sdcard/") + } + runMigrations( + filesDir, + sharedPrefs, + GitSettings(sharedPrefs, gitSecrets, filesDir), + sharedPrefs, + context, + runTest = true, + ) + assertFalse { sharedPrefs.contains(PreferenceKeys.GIT_EXTERNAL) } + assertFalse { sharedPrefs.contains(PreferenceKeys.GIT_EXTERNAL_REPO) } + assertTrue { sharedPrefs.getBoolean(PreferenceKeys.GIT_EXTERNAL_MIGRATED, false) } + } + @Test fun verifyPersistentCredentialCache() { sharedPrefs.edit { @@ -190,6 +218,7 @@ class MigrationsTest { putString(PreferenceKeys.SSH_KEY_LOCAL_PASSPHRASE, "my secret local ssh key passphrase") } runMigrations( + filesDir, sharedPrefs, GitSettings(sharedPrefs, gitSecrets, filesDir), sharedPrefs, @@ -206,6 +235,7 @@ class MigrationsTest { putString("unlock_pin", "abcdefg") } runMigrations( + filesDir, sharedPrefs, GitSettings(sharedPrefs, gitSecrets, filesDir), sharedPrefs, diff --git a/docs/upstream-reconciliation.md b/docs/upstream-reconciliation.md index cc9566ff49..878f3dc91a 100644 --- a/docs/upstream-reconciliation.md +++ b/docs/upstream-reconciliation.md @@ -53,7 +53,7 @@ Reviewed through upstream `5f320126e14b9324db7a84f33e48fa9fdbdc30ca` (`develop`) | #1112 | adopted | sshj 0.41.x fixes (fork is already on sshj 0.41.2) | | #1129 | adopted | Dialog showing the SSH server authentication banner | | #1162 | adopted | SSH host key verification fix; also applies to the headless passkey SSH path | -| #1168 | adapted | External/shared repository option (`Documents/PasswordStore`). Passkey providers now resolve their storage root from `PasswordRepository` so passkeys stay in the Git working tree | +| #1168 | skipped (reverted) | External/shared repository option (`Documents/PasswordStore`). Ported, then removed before release: this fork is Git-only, and shared storage weakens the passkey storage guarantees (other apps can read/replace files, no storage permission, files may become unreadable after reinstall). Do not re-adopt | | #1063, #1115 | adapted | Bouncy Castle and coroutines versions aligned through BOMs (including passkey module build wiring) | | #1065 | adopted | Alert dialogs no longer clipped by the IME on short screens | | #1149, #1152, 1c048ab5 | adopted | Spanish translation fix; empty git author email allowed; fastlane banner renamed | From 51f89ab6df5f90bafb3f9cd690612c767ed81b85 Mon Sep 17 00:00:00 2001 From: Alexander Gil Date: Tue, 27 Oct 2026 22:46:45 +0100 Subject: [PATCH 3/3] docs(skill): note that the external repository option must not be re-adopted --- .opencode/skills/upstream-sync/SKILL.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.opencode/skills/upstream-sync/SKILL.md b/.opencode/skills/upstream-sync/SKILL.md index a2ec397f92..8b7491bda8 100644 --- a/.opencode/skills/upstream-sync/SKILL.md +++ b/.opencode/skills/upstream-sync/SKILL.md @@ -93,3 +93,4 @@ PR body sections: Summary (upstream range `..`), Ado - Upstream "autofill" commits sometimes touch passkey screens too (e.g. shortening passkey ids); port only the classic part. - Upstream runtime-permission and SSH host-key changes are security relevant; review them rather than blindly cherry-picking. - The fork's applicationId, CI workflows and release pipeline differ from upstream; never take upstream `.github/workflows/` or release commits. +- The external/shared repository option (upstream #1168, `PREF_SHARED_REPO`, `Documents/PasswordStore`) was deliberately removed: this fork is Git-only and keeps the store (and passkeys) in app-private storage. Skip later upstream changes that build on it.