Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .opencode/skills/upstream-sync/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,4 @@ PR body sections: Summary (upstream range `<old-baseline>..<reviewed-sha>`), 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.
1 change: 0 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion app/src/main/java/app/passwordstore/Application.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<PGPKey> {
val repositoryRoot = PasswordRepository.getRepositoryDirectory()
val repositoryRoot = File(context.filesDir, "store")
val localResolver = DefaultPassRecipientResolver(repositoryRoot, keyManager)
return OpenPgpPassRecipientResolver(
localResolver,
Expand All @@ -111,14 +114,15 @@ object PasskeysModule {
@Provides
@Singleton
fun providePasskeyStorage(
@ApplicationContext context: Context,
cryptoHandler: PGPainlessCryptoHandler,
passkeyPgpDecryptor: PasskeyPgpDecryptor,
pgpUnlockContext: PgpUnlockContext,
recipientResolver: PassRecipientResolver<PGPKey>,
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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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()
Expand Down
Loading
Loading