Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
bc17659
fix access_local_network runtime permission request (#1076)
agrahn Sep 10, 2026
43a83b6
chore: add opencode upstream-sync skill
pando85 Oct 27, 2026
547721a
fixes related to sshj-0.41.1 update (#1112)
agrahn Sep 24, 2026
fd869c4
check local network connection on Android 17+ (#1116)
agrahn Sep 24, 2026
430bd31
build: align Bouncy Castle and coroutines versions through BOMs
agrahn Sep 3, 2026
70b8a2b
local network connection test on Android 17+ (#1119)
agrahn Sep 25, 2026
cfd720a
Fix alert dialogs clipped by IME on short screens (#1065)
kevdotpub Sep 4, 2026
1606cde
dialog showing ssh authentication banner (if any) (#1129)
agrahn Oct 2, 2026
6930e5e
banner image file renamed
agrahn Sep 24, 2026
781d0fa
Fixed Spanish translation (#1149)
palonsoro Oct 13, 2026
16bd1a2
fix email setting, allow empty value (#1152)
agrahn Oct 15, 2026
4cc4f38
host key verification fixed (#1162)
agrahn Oct 21, 2026
1cfb5b3
chore(autofill-parser): refresh Public Suffix List snapshot from upst…
pando85 Oct 27, 2026
45d54a4
autofill-save fix: fill in credentials after creation (partial)
pando85 Oct 27, 2026
033c990
autofill inline suggestions with two parent levels (#1057)
agrahn Sep 1, 2026
d361e6b
autofill: search (AutofillFilterView) refactored (#1059)
agrahn Sep 2, 2026
a1646e2
autofill: allow space around search string (#1060)
agrahn Sep 2, 2026
0c12eb6
external repository option is back (#1168)
agrahn Oct 26, 2026
7dcc42f
autofill: improved username lookup (#1061)
agrahn Sep 2, 2026
1e6eebc
autofill: persistent settings for switches on search pane (#1062)
agrahn Sep 3, 2026
5c667e3
fix display of larger number of autofill suggestions (#1072)
agrahn Sep 9, 2026
4f33963
fix(passkeys): resolve passkey storage root from PasswordRepository (…
pando85 Oct 27, 2026
d148522
fix: close probe socket and avoid duplicate key-selection navigation
pando85 Oct 27, 2026
56944fd
autofill: never offer or fill passkey credentials (adapts upstream #1…
pando85 Oct 27, 2026
4c4f9ea
suppress empty autofill search result list item (#1080)
pando85 Oct 27, 2026
9848176
ellipsis at start of long paths in autofill views (#1081)
agrahn Sep 10, 2026
31ffe75
path display in autofill search activity (#1082)
agrahn Sep 11, 2026
91e5104
autofill search activity improvements (#1087)
pando85 Oct 27, 2026
1648e37
app settings enable autofill (#1090)
agrahn Sep 13, 2026
f916a08
fix initial switch state for enable autofill setting (#1091)
agrahn Sep 14, 2026
90bc8b0
autofill improvements (#1093)
pando85 Oct 27, 2026
9c62ba0
fix behaviour of settings switch for enabling autofill (#1094)
agrahn Sep 14, 2026
dbc080b
username extraction from file path (#1095)
agrahn Sep 15, 2026
f3c5a17
fix missing create-entry autofill suggestion (#1097)
agrahn Sep 16, 2026
e36696b
username extraction from file path cont'd (#1100)
agrahn Sep 17, 2026
af29d18
password creation & autofill-save: honour directory structure (upstre…
pando85 Oct 27, 2026
866cab6
merge: upstream sync group misc (2026-10-27)
pando85 Oct 27, 2026
3e04e9e
merge: upstream sync group ssh (2026-10-27)
pando85 Oct 27, 2026
5cfa31a
merge: upstream sync group autofill (2026-10-27)
pando85 Oct 27, 2026
872dff2
docs: record upstream reconciliation through 5f320126 (2026-10-27)
pando85 Oct 27, 2026
84e7cc5
test: fix nullable parentFile warning in AutofillPasskeyExclusionTest
pando85 Oct 27, 2026
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
2 changes: 1 addition & 1 deletion .github/upstream-sync-baseline
Original file line number Diff line number Diff line change
@@ -1 +1 @@
48ce3af5b4cd9b818d44edca4249a15d48e2170f
5f320126e14b9324db7a84f33e48fa9fdbdc30ca
95 changes: 95 additions & 0 deletions .opencode/skills/upstream-sync/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
---
name: upstream-sync
description: Sync this fork with upstream agrahn/Android-Password-Store by selectively porting non-passkey changes into a dated PR. Use when the user asks to sync, reconcile, pull or bring changes from upstream / agrahn.
---

## Purpose

Bring relevant upstream changes from [`agrahn/Android-Password-Store`](https://github.com/agrahn/Android-Password-Store) (`develop`) into this fork without importing upstream's passkey implementation. The full policy lives in `docs/upstream-reconciliation.md`; read it first.

Key facts:

- **Never `git merge upstream/develop`.** The fork has its own passkey/WebAuthn architecture. Changes are cherry-picked (`-x`) or ported one upstream commit/PR at a time.
- **The review baseline is an upstream SHA**, stored in `.github/upstream-sync-baseline`. It is *not* the Git merge-base: selective ports create no ancestry, so the merge-base stays old forever.
- **Passkey-owned paths are protected**: `passkeys/`, app passkey provider/injection packages, passkey provider XML, `PASSKEYS.md` and passkey docs. Upstream passkey changes are only an invariant/interoperability source; take the classic Password Store/Autofill part of mixed commits.
- **Dependency bumps are owned by the fork's Renovate.** Upstream CI, release, changelog and snapshot-bump commits are skipped. Only bump a dependency when a ported change requires it.

## Workflow

### 1. Audit

```bash
git fetch upstream # remote: https://github.com/agrahn/Android-Password-Store.git
scripts/upstream-audit.sh "$SCRATCH/audit.md"
git rev-parse upstream/develop # write down: this is the SHA you are reviewing up to
```

The report lists every upstream commit after the baseline plus changed files classified by area (PASSKEY-PROTECTED, AUTOFILL, PGP/CRYPTO, ...). Filter out dependency/action/PSL/release noise to get the list of meaningful commits.

### 2. Branch

```bash
DATE=$(date +%F)
git switch -c sync/upstream-$DATE main
```

### 3. Port by area, with subagents

Group the meaningful commits by area (e.g. autofill/password creation, SSH/git/network, misc UI/strings/build) and give each group to its own subagent in its own worktree so they run in parallel:

```bash
git worktree add -b sync/$DATE-<group> ../wt/<group> main
cp local.properties ../wt/<group>/ # untracked SDK config needed to build
```

Brief each subagent with: the worktree path, the exact upstream SHAs in its scope, the policy doc, the passkey boundary, "skip upstream CHANGELOG.md hunks", "no `clean`, no full lint (sibling builds)", and ask for a final table `| upstream SHA | PR | disposition | note |` covering **every** commit in scope. Use a stronger model for large or security-sensitive groups (autofill series, host-key verification, crypto); a cheaper one for mechanical groups.

Per commit:

- applies cleanly or with trivial conflicts → `git cherry-pick -x <sha>` (or `scripts/adopt-upstream-pr.sh <PR>`, which refuses passkey paths)
- needs rework → port manually, cite upstream SHA/PR in the commit message
- disposition is one of **adopted / adapted / already solved / skipped**

Also handle in one place:

- Public Suffix List: refresh `autofill-parser` PSL once from `upstream/develop` instead of replaying each update commit.
- Compare `gradle/libs.versions.toml` with upstream and list versions where the fork lags (report, don't bump unless needed).

### 4. Integrate

```bash
git switch sync/upstream-$DATE
git merge --no-ff sync/$DATE-<group> # for each group; resolve strings.xml / CHANGELOG conflicts
./gradlew spotlessApply
CI=true ./gradlew lint spotlessCheck test -PslimTests assembleDebug
```

All checks must pass, including passkey compatibility tests. Then remove the temporary worktrees and branches (`git worktree remove ../wt/<group>`; `git branch -D sync/$DATE-<group>`).

### 5. Record

- Add a `## Reconciliation (<DATE>)` section to `docs/upstream-reconciliation.md` with the reviewed-through SHA and the disposition table (adopted/adapted rows individually, skipped categories summarised).
- Set the baseline to the SHA recorded in step 1 (do not run `scripts/mark-upstream-reviewed.sh` if upstream moved during the review — it writes the *current* upstream head):

```bash
echo <reviewed-sha> > .github/upstream-sync-baseline
```

- Add user-visible changes to the `## [Unreleased]` section of `CHANGELOG.md`.

### 6. Pull request

```bash
git push -u origin sync/upstream-$DATE
gh pr create --repo pando85/Android-Password-Store --base main \
--title "chore: sync upstream changes ($DATE)" --body-file <body.md>
```

PR body sections: Summary (upstream range `<old-baseline>..<reviewed-sha>`), Adopted/adapted changes (with upstream PR numbers), Skipped (with reasons), Passkey boundary, Lagging dependencies, Validation.

## Pitfalls

- `scripts/upstream-audit.sh` and `mark-upstream-reviewed.sh` reset the `upstream` remote URL to HTTPS.
- 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.
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,28 @@ All notable changes to this project will be documented in this file

## [Unreleased]

### 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

### Changed

- Autofill search pane: whitespace-tolerant search, persistent filter switches, start-ellipsised paths, better username lookup (also from the file path) and more inline suggestions
- New password entries and Autofill-saved credentials honour the directory structure setting and keep the subfolder of the entry
- Bouncy Castle and coroutines versions are aligned through BOMs

### Fixed

- Classic Autofill no longer offers or fills passkey entries
- Autofill-saved credentials are filled in right after creation, with the username pre-filled
- SSH host key verification and compatibility with sshj 0.41
- Alert dialogs are no longer clipped by the keyboard on short screens
- An empty Git author email is now allowed
- Spanish translation fixes

## [1.26.0] - 2026-10-27

### Changed
Expand Down
3 changes: 2 additions & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ android {

dependencies {
implementation(platform(libs.compose.bom))
implementation(platform(libs.thirdparty.bouncycastle.bom))
ksp(libs.dagger.hilt.compiler)
implementation(libs.androidx.annotation)
coreLibraryDesugaring(libs.android.desugarJdkLibs)
Expand Down Expand Up @@ -76,7 +77,7 @@ dependencies {
implementation(libs.dagger.hilt.android)

implementation(libs.kotlinx.collections.immutable)
implementation(libs.kotlinx.coroutines.android)
implementation(platform(libs.kotlinx.coroutines.bom))
implementation(libs.kotlinx.coroutines.core)
implementation(libs.kotlinx.serialization.json)

Expand Down
4 changes: 2 additions & 2 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@
<activity
android:name=".ui.git.config.GitServerConfigActivity"
android:exported="false"
android:label="@string/title_activity_git_clone"
android:label="@string/title_activity_git_remote"
android:windowSoftInputMode="adjustResize" />

<activity
Expand Down Expand Up @@ -182,8 +182,8 @@
android:theme="@style/NoBackgroundThemeM3" />
<activity
android:name=".ui.autofill.AutofillFilterView"
android:configChanges="orientation|keyboardHidden"
android:exported="false"
android:configChanges="orientation|screenSize|keyboardHidden"
android:theme="@style/DialogLikeThemeM3"
android:windowSoftInputMode="adjustNothing" />
<activity
Expand Down
4 changes: 1 addition & 3 deletions app/src/main/java/app/passwordstore/Application.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ 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 @@ -48,7 +47,6 @@ 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 @@ -73,7 +71,7 @@ class Application : android.app.Application(), SharedPreferences.OnSharedPrefere
logcat { "Debug logging enabled." }
prefs.registerOnSharedPreferenceChangeListener(this)
setNightMode()
runMigrations(filesDirPath, prefs, gitSettings, persistentPassphrases)
runMigrations(prefs, gitSettings, persistentPassphrases)
proxyUtils.setDefaultProxy()
DynamicColors.applyToActivitiesIfAvailable(this)
setupScreenOffHandler()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*/
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 All @@ -15,6 +16,14 @@ import com.github.michaelbull.result.getOrElse
import com.github.michaelbull.result.onErr
import com.github.michaelbull.result.runCatching
import java.io.File
import java.io.IOException
import java.nio.file.FileVisitResult
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import java.nio.file.SimpleFileVisitor
import java.nio.file.attribute.BasicFileAttributes
import kotlin.io.path.name
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.lib.BranchTrackingStatus
import org.eclipse.jgit.lib.Constants
Expand All @@ -26,6 +35,9 @@ import org.eclipse.jgit.transport.URIish

object PasswordRepository {

const val TYPE_FILE: Int = 1
const val TYPE_DIR: Int = 2

var repository: Repository? = null
private val settings by unsafeLazy { Application.instance.sharedPrefs }
private val filesDir
Expand Down Expand Up @@ -105,12 +117,30 @@ object PasswordRepository {
fun closeRepository() {
repository?.close()
repository = null
settings.edit {
putBoolean(PreferenceKeys.REPOSITORY_INITIALIZED, false)
putBoolean(PreferenceKeys.PREF_SHARED_REPO, false)
}
}

fun getRepositoryDirectory(): File {
return File(filesDir.toString(), "/store")
fun deleteRepository() {
getRepositoryDirectory().let { dir ->
dir.deleteRecursively()
}
closeRepository()
}

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 Expand Up @@ -255,4 +285,58 @@ object PasswordRepository {
passwordList.sortWith(sortOrder.comparator)
return passwordList
}

/**
* Recursively finds files and/or directories (see [type]) below [startPath] whose name equals
* [name]. Hidden directories (such as `.git`) are not descended into.
*/
fun findByName(
startPath: String,
name: String,
type: Int = TYPE_DIR or TYPE_FILE,
ignoreCase: Boolean = false,
): List<Path> =
walkNonHidden(Paths.get(startPath), type) { it.name.equals(name, ignoreCase = ignoreCase) }

/**
* Recursively finds files and/or directories (see [type]) below [startPath] whose parent
* directory's name equals [name]. Hidden directories (such as `.git`) are not descended into.
*/
fun findByParentName(
startPath: String,
name: String,
type: Int = TYPE_DIR or TYPE_FILE,
ignoreCase: Boolean = false,
): List<Path> =
walkNonHidden(Paths.get(startPath), type) { path ->
val parent: Path? = path.parent
parent != null && parent.name.equals(name, ignoreCase = ignoreCase)
}

private fun walkNonHidden(start: Path, type: Int, predicate: (Path) -> Boolean): List<Path> {
val matches = mutableListOf<Path>()
if (!Files.isDirectory(start)) return matches
Files.walkFileTree(
start,
object : SimpleFileVisitor<Path>() {
override fun preVisitDirectory(
dir: Path,
attrs: BasicFileAttributes,
): FileVisitResult {
if (dir != start && dir.name.startsWith('.')) return FileVisitResult.SKIP_SUBTREE
if (dir != start && type and TYPE_DIR != 0 && predicate(dir)) matches.add(dir)
return FileVisitResult.CONTINUE
}

override fun visitFile(file: Path, attrs: BasicFileAttributes): FileVisitResult {
if (type and TYPE_FILE != 0 && attrs.isRegularFile && predicate(file)) matches.add(file)
return FileVisitResult.CONTINUE
}

override fun visitFileFailed(file: Path, exc: IOException): FileVisitResult =
FileVisitResult.CONTINUE
},
)
return matches
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ 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 @@ -46,7 +47,6 @@ 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,20 +89,17 @@ object PasskeysModule {

@Provides
@Singleton
fun provideRepositoryGenerationProvider(
@ApplicationContext context: Context
): RepositoryGenerationProvider =
DefaultRepositoryGenerationProvider(File(context.filesDir, "store"))
fun provideRepositoryGenerationProvider(): RepositoryGenerationProvider =
DefaultRepositoryGenerationProvider(PasswordRepository.getRepositoryDirectory())

@Provides
@Singleton
fun providePassRecipientResolver(
@ApplicationContext context: Context,
keyManager: PGPKeyManager,
providerRepository: OpenPgpProviderRepository,
interactionCoordinator: OpenPgpInteractionCoordinator,
): PassRecipientResolver<PGPKey> {
val repositoryRoot = File(context.filesDir, "store")
val repositoryRoot = PasswordRepository.getRepositoryDirectory()
val localResolver = DefaultPassRecipientResolver(repositoryRoot, keyManager)
return OpenPgpPassRecipientResolver(
localResolver,
Expand All @@ -114,15 +111,14 @@ 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 = File(context.filesDir, "store")
val repositoryRoot = PasswordRepository.getRepositoryDirectory()
val passkeyConfig = PasskeyStorageConfig(passkeyDirectory = "fido2", fileExtension = ".gpg")
val fileStorage =
FilePasskeyStorage(
Expand Down
Loading
Loading