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. 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/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( 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 |