From 1e7bfb753a1dd9131e65454fdc4198a809cc96b1 Mon Sep 17 00:00:00 2001 From: Alexander Gil Casas Date: Sat, 29 Aug 2026 22:28:21 +0200 Subject: [PATCH 01/41] chore(upstream): record reviewed upstream baseline --- .github/upstream-sync-baseline | 1 + 1 file changed, 1 insertion(+) create mode 100644 .github/upstream-sync-baseline diff --git a/.github/upstream-sync-baseline b/.github/upstream-sync-baseline new file mode 100644 index 000000000..b06bbbbb5 --- /dev/null +++ b/.github/upstream-sync-baseline @@ -0,0 +1 @@ +48ce3af5b4cd9b818d44edca4249a15d48e2170f From e66e2b9206c7349c9ecb649de1432fdaa69c04d1 Mon Sep 17 00:00:00 2001 From: Alexander Gil Casas Date: Sat, 29 Aug 2026 22:28:37 +0200 Subject: [PATCH 02/41] chore(upstream): add repeatable upstream audit script --- scripts/upstream-audit.sh | 122 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 scripts/upstream-audit.sh diff --git a/scripts/upstream-audit.sh b/scripts/upstream-audit.sh new file mode 100644 index 000000000..74fe42cea --- /dev/null +++ b/scripts/upstream-audit.sh @@ -0,0 +1,122 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(git rev-parse --show-toplevel)" +cd "$ROOT" + +UPSTREAM_REMOTE="${UPSTREAM_REMOTE:-upstream}" +UPSTREAM_URL="${UPSTREAM_URL:-https://github.com/agrahn/Android-Password-Store.git}" +UPSTREAM_BRANCH="${UPSTREAM_BRANCH:-develop}" +BASELINE_FILE="${UPSTREAM_BASELINE_FILE:-.github/upstream-sync-baseline}" +OUTPUT="${1:-upstream-audit.md}" + +if [[ ! -f "$BASELINE_FILE" ]]; then + echo "Missing upstream baseline: $BASELINE_FILE" >&2 + exit 2 +fi + +BASELINE="$(tr -d '[:space:]' < "$BASELINE_FILE")" +if ! [[ "$BASELINE" =~ ^[0-9a-f]{40}$ ]]; then + echo "Invalid upstream baseline SHA in $BASELINE_FILE: $BASELINE" >&2 + exit 2 +fi + +if git remote get-url "$UPSTREAM_REMOTE" >/dev/null 2>&1; then + git remote set-url "$UPSTREAM_REMOTE" "$UPSTREAM_URL" +else + git remote add "$UPSTREAM_REMOTE" "$UPSTREAM_URL" +fi + +git fetch --quiet --no-tags "$UPSTREAM_REMOTE" "$UPSTREAM_BRANCH" +UPSTREAM_REF="$UPSTREAM_REMOTE/$UPSTREAM_BRANCH" +UPSTREAM_HEAD="$(git rev-parse "$UPSTREAM_REF")" + +if ! git cat-file -e "${BASELINE}^{commit}" 2>/dev/null; then + echo "Baseline $BASELINE is not available after fetching $UPSTREAM_REF" >&2 + exit 2 +fi +if ! git merge-base --is-ancestor "$BASELINE" "$UPSTREAM_HEAD"; then + echo "Baseline $BASELINE is not an ancestor of $UPSTREAM_REF ($UPSTREAM_HEAD)." >&2 + echo "Upstream may have been rebased; review manually before moving the baseline." >&2 + exit 3 +fi + +COUNT="$(git rev-list --count "$BASELINE..$UPSTREAM_HEAD")" + +classify_path() { + local path="$1" + case "$path" in + passkeys/*|PASSKEYS.md|PasskeyStorage.md|app/src/main/java/app/passwordstore/passkeys/*|app/src/main/java/app/passwordstore/injection/passkeys/*|app/src/main/res/xml/passkey_provider.xml|app/src/main/res/values-v34/bools.xml) + printf 'PASSKEY-PROTECTED' + ;; + autofill-parser/*|app/src/main/java/app/passwordstore/util/autofill/*|app/src/main/java/app/passwordstore/ui/autofill/*) + printf 'AUTOFILL' + ;; + app/src/main/java/app/passwordstore/ui/crypto/*|app/src/main/java/app/passwordstore/ui/pgp/*|crypto/*) + printf 'PGP/CRYPTO' + ;; + gradle/*|gradlew|gradlew.bat|renovate.json|*/build.gradle.kts|build.gradle.kts|settings.gradle.kts) + printf 'DEPENDENCY/BUILD' + ;; + .github/*) + printf 'CI/REPOSITORY' + ;; + *publicsuffix*) + printf 'GENERATED-DATA' + ;; + *) + printf 'CLASSIC-APP' + ;; + esac +} + +{ + echo "# Upstream reconciliation report" + echo + echo "- Fork: \`$(git rev-parse --short=12 HEAD)\`" + echo "- Last reviewed upstream: \`$BASELINE\`" + echo "- Current upstream \`$UPSTREAM_BRANCH\`: \`$UPSTREAM_HEAD\`" + echo "- New upstream commits: **$COUNT**" + echo + + if [[ "$COUNT" == "0" ]]; then + echo "No upstream changes require review." + else + echo "## Commits" + echo + git log --reverse --format='- `%h` %s' "$BASELINE..$UPSTREAM_HEAD" + echo + echo "## Changed files" + echo + echo "| Area | Path |" + echo "| --- | --- |" + while IFS= read -r path; do + [[ -z "$path" ]] && continue + printf '| %s | `%s` |\n' "$(classify_path "$path")" "$path" + done < <(git diff --name-only "$BASELINE..$UPSTREAM_HEAD") + echo + echo "## Review policy" + echo + echo "- **PASSKEY-PROTECTED** changes are never adopted mechanically. Review their behavioural/security invariant and reimplement only when it applies to this fork." + echo "- Classic app, Autofill, PGP and crypto fixes can normally be selectively cherry-picked or semantically ported." + echo "- Dependency/build updates stay under Renovate unless an upstream change carries behaviour not represented by a version bump." + echo "- Generated public-suffix data should be refreshed as one current snapshot rather than replaying historical update commits." + echo + echo "After every commit in this report has a disposition (adopted, adapted, already solved, or intentionally skipped), run \`scripts/mark-upstream-reviewed.sh\` and commit the baseline change." + fi +} > "$OUTPUT" + +HAS_CHANGES=false +if [[ "$COUNT" != "0" ]]; then + HAS_CHANGES=true +fi + +if [[ -n "${GITHUB_OUTPUT:-}" ]]; then + { + echo "has_changes=$HAS_CHANGES" + echo "commit_count=$COUNT" + echo "upstream_head=$UPSTREAM_HEAD" + } >> "$GITHUB_OUTPUT" +fi + +cat "$OUTPUT" From bce8e6c5b8a1d614a517d665e1fbecd4063b3ec1 Mon Sep 17 00:00:00 2001 From: Alexander Gil Casas Date: Sat, 29 Aug 2026 22:28:45 +0200 Subject: [PATCH 03/41] chore(upstream): add baseline update helper --- scripts/mark-upstream-reviewed.sh | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 scripts/mark-upstream-reviewed.sh diff --git a/scripts/mark-upstream-reviewed.sh b/scripts/mark-upstream-reviewed.sh new file mode 100644 index 000000000..95bc0ff61 --- /dev/null +++ b/scripts/mark-upstream-reviewed.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(git rev-parse --show-toplevel)" +cd "$ROOT" + +UPSTREAM_REMOTE="${UPSTREAM_REMOTE:-upstream}" +UPSTREAM_URL="${UPSTREAM_URL:-https://github.com/agrahn/Android-Password-Store.git}" +UPSTREAM_BRANCH="${UPSTREAM_BRANCH:-develop}" +BASELINE_FILE="${UPSTREAM_BASELINE_FILE:-.github/upstream-sync-baseline}" + +if ! git diff --quiet || ! git diff --cached --quiet; then + echo "Working tree must be clean before moving the upstream review baseline." >&2 + exit 2 +fi + +if git remote get-url "$UPSTREAM_REMOTE" >/dev/null 2>&1; then + git remote set-url "$UPSTREAM_REMOTE" "$UPSTREAM_URL" +else + git remote add "$UPSTREAM_REMOTE" "$UPSTREAM_URL" +fi + +git fetch --quiet --no-tags "$UPSTREAM_REMOTE" "$UPSTREAM_BRANCH" +UPSTREAM_HEAD="$(git rev-parse "$UPSTREAM_REMOTE/$UPSTREAM_BRANCH")" +printf '%s\n' "$UPSTREAM_HEAD" > "$BASELINE_FILE" + +echo "Updated $BASELINE_FILE to $UPSTREAM_HEAD" +echo "Commit this only after every upstream change up to that SHA has an explicit disposition." From cb9096e2db61413ed52e994c0b57e08fecbdf0b9 Mon Sep 17 00:00:00 2001 From: Alexander Gil Casas Date: Sat, 29 Aug 2026 22:28:57 +0200 Subject: [PATCH 04/41] chore(upstream): add guarded upstream PR adoption helper --- scripts/adopt-upstream-pr.sh | 71 ++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 scripts/adopt-upstream-pr.sh diff --git a/scripts/adopt-upstream-pr.sh b/scripts/adopt-upstream-pr.sh new file mode 100644 index 000000000..6a7813e71 --- /dev/null +++ b/scripts/adopt-upstream-pr.sh @@ -0,0 +1,71 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + cat <<'EOF' +Usage: scripts/adopt-upstream-pr.sh [--allow-passkeys] + +Find the squash/merge commit for an upstream PR on agrahn/Android-Password-Store:develop, +refuse protected passkey changes by default, then cherry-pick it with provenance. +EOF +} + +ALLOW_PASSKEYS=false +if [[ "${1:-}" == "--allow-passkeys" ]]; then + ALLOW_PASSKEYS=true + shift +fi + +PR="${1:-}" +if ! [[ "$PR" =~ ^[0-9]+$ ]]; then + usage >&2 + exit 2 +fi + +ROOT="$(git rev-parse --show-toplevel)" +cd "$ROOT" + +if ! git diff --quiet || ! git diff --cached --quiet; then + echo "Working tree must be clean before adopting an upstream PR." >&2 + exit 2 +fi + +UPSTREAM_REMOTE="${UPSTREAM_REMOTE:-upstream}" +UPSTREAM_URL="${UPSTREAM_URL:-https://github.com/agrahn/Android-Password-Store.git}" +UPSTREAM_BRANCH="${UPSTREAM_BRANCH:-develop}" + +if git remote get-url "$UPSTREAM_REMOTE" >/dev/null 2>&1; then + git remote set-url "$UPSTREAM_REMOTE" "$UPSTREAM_URL" +else + git remote add "$UPSTREAM_REMOTE" "$UPSTREAM_URL" +fi + +git fetch --quiet --no-tags "$UPSTREAM_REMOTE" "$UPSTREAM_BRANCH" +UPSTREAM_REF="$UPSTREAM_REMOTE/$UPSTREAM_BRANCH" + +COMMIT="$(git log "$UPSTREAM_REF" --format='%H' --grep="#${PR}" -n 1)" +if [[ -z "$COMMIT" ]]; then + echo "Could not find a commit for upstream PR #$PR on $UPSTREAM_REF." >&2 + exit 3 +fi + +mapfile -t FILES < <(git diff-tree --no-commit-id --name-only -r "$COMMIT") +PROTECTED=() +for path in "${FILES[@]}"; do + case "$path" in + passkeys/*|PASSKEYS.md|PasskeyStorage.md|app/src/main/java/app/passwordstore/passkeys/*|app/src/main/java/app/passwordstore/injection/passkeys/*|app/src/main/res/xml/passkey_provider.xml|app/src/main/res/values-v34/bools.xml) + PROTECTED+=("$path") + ;; + esac +done + +if (( ${#PROTECTED[@]} > 0 )) && [[ "$ALLOW_PASSKEYS" != true ]]; then + echo "Refusing to cherry-pick upstream PR #$PR because it touches fork-owned passkey code:" >&2 + printf ' - %s\n' "${PROTECTED[@]}" >&2 + echo "Review the behavioural invariant and port it manually. Use --allow-passkeys only after an explicit review." >&2 + exit 4 +fi + +echo "Upstream PR #$PR -> $COMMIT" +printf ' %s\n' "${FILES[@]}" +git cherry-pick -x "$COMMIT" From 651094cf47386761820c1922b979652724746358 Mon Sep 17 00:00:00 2001 From: Alexander Gil Casas Date: Sat, 29 Aug 2026 22:29:13 +0200 Subject: [PATCH 05/41] docs(upstream): document selective reconciliation policy --- docs/upstream-reconciliation.md | 51 +++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 docs/upstream-reconciliation.md diff --git a/docs/upstream-reconciliation.md b/docs/upstream-reconciliation.md new file mode 100644 index 000000000..493bf9cb4 --- /dev/null +++ b/docs/upstream-reconciliation.md @@ -0,0 +1,51 @@ +# Upstream reconciliation + +This repository intentionally diverges from [`agrahn/Android-Password-Store`](https://github.com/agrahn/Android-Password-Store), especially in the passkey implementation. Upstream remains valuable for classic Password Store, Autofill, PGP, Android platform, browser compatibility, and UX fixes, but it must not be merged wholesale. + +## Policy + +The review unit is an **upstream commit/PR**, not a Git merge. Every upstream change since `.github/upstream-sync-baseline` gets one disposition: + +- **adopted** — cherry-picked or ported without semantic changes; +- **adapted** — the upstream invariant applies, but the implementation is rewritten for this fork; +- **already solved** — the fork already covers the same bug or invariant; +- **skipped** — dependency, CI, release, obsolete implementation, or otherwise intentionally not applicable. + +Passkey-owned paths are protected by policy. Changes under `passkeys/`, the app passkey provider/injection packages, passkey provider XML, and passkey documentation are never mechanically adopted. Upstream passkey changes are treated as an interoperability/security test corpus: identify the invariant first, then verify or reimplement it in the fork architecture. + +## Routine review + +1. Run `scripts/upstream-audit.sh`. It fetches `agrahn/Android-Password-Store:develop` and produces a Markdown report for only the commits after the recorded review baseline. +2. Review the report by area. Pay particular attention to **PASSKEY-PROTECTED**, **AUTOFILL**, and **PGP/CRYPTO** entries. +3. For a clean classic-app PR, use `scripts/adopt-upstream-pr.sh `. The helper refuses passkey-owned changes unless `--allow-passkeys` is explicitly supplied. +4. For cross-cutting or security-sensitive changes, port the behaviour manually and preserve the upstream PR/commit in the commit message or PR description. +5. Run the full repository CI, including passkey compatibility tests. +6. Once **every** upstream commit in the report has a disposition, run `scripts/mark-upstream-reviewed.sh` and commit the baseline update. + +The baseline is deliberately an upstream SHA rather than a merge base. Selective cherry-picks do not create ancestry with upstream, so using the Git merge base would repeatedly report the same historical commits forever. + +## Automated audit + +`.github/workflows/upstream-audit.yml` runs weekly and can also be started manually. When new upstream commits exist it creates or refreshes one issue named **Upstream reconciliation pending** containing the same categorized report. The workflow never modifies source code or moves the review baseline automatically. + +This split is intentional: detection is automated; adoption remains explicit. + +## Current reconciliation (2026-08-29) + +Reviewed through upstream `48ce3af5b4cd9b818d44edca4249a15d48e2170f` (`develop`). The initial reconciliation ports or adapts the following upstream work while keeping the fork-owned passkey implementation: + +| Upstream PR | Disposition | Scope | +| --- | --- | --- | +| #936 | adopted | Avoid rebuilding the SSH key type lookup map on every parse | +| #938, #939, #941, #942 | adopted/adapted | Complete recent-password timestamp lifecycle: create/edit, delete, move, repository reset | +| #1000 | adapted | Contextual PIN wording for Autofill; no import of upstream's passkey UI | +| #1007 | adopted | Correct shortcut identity and LRU ordering | +| #1011 | adapted | Bind fast-unlock PIN state to each PGP ID, including migration/cleanup | +| #1014 | adopted | Optional concealment of extra password-entry content | +| #1016 | adopted | Reuse IME inline-presentation specs safely when suggestions outnumber specs | +| #1022 | adopted | Android 17 local-network permission for SSH | +| #1026 | adapted | Add Titanium/Helium to classic browser trust detection; passkey caller trust remains independently controlled | +| #1043 | adopted | Make system Back and toolbar Back consistent in the PGP key chooser | +| PSL updates | adopted as snapshot | Refresh the current Public Suffix List once instead of replaying generated-data commits | + +Dependency-only upstream commits remain owned by Renovate. Upstream CI/release workflow changes are not synchronized because this fork has its own release and validation pipeline. From 7f2db3cbe0c97e8048f35cef6fd8af18c97bf93e Mon Sep 17 00:00:00 2001 From: Alexander Gil Casas Date: Sat, 29 Aug 2026 22:29:20 +0200 Subject: [PATCH 06/41] ci(upstream): audit upstream changes weekly --- .github/workflows/upstream-audit.yml | 48 ++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 .github/workflows/upstream-audit.yml diff --git a/.github/workflows/upstream-audit.yml b/.github/workflows/upstream-audit.yml new file mode 100644 index 000000000..b34258d75 --- /dev/null +++ b/.github/workflows/upstream-audit.yml @@ -0,0 +1,48 @@ +name: Audit upstream changes + +on: + schedule: + - cron: "17 6 * * 1" + workflow_dispatch: + +permissions: + contents: read + issues: write + +jobs: + audit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + + - name: Generate upstream reconciliation report + id: audit + shell: bash + run: scripts/upstream-audit.sh "$RUNNER_TEMP/upstream-audit.md" + + - name: Create or update reconciliation issue + if: steps.audit.outputs.has_changes == 'true' + env: + GH_TOKEN: ${{ github.token }} + REPORT: ${{ runner.temp }}/upstream-audit.md + shell: bash + run: | + set -euo pipefail + issue_number="$( + gh issue list \ + --state open \ + --search '"Upstream reconciliation pending" in:title' \ + --json number,title \ + --jq '.[] | select(.title == "Upstream reconciliation pending") | .number' \ + | head -n 1 + )" + + if [[ -n "$issue_number" ]]; then + gh issue edit "$issue_number" --body-file "$REPORT" + else + gh issue create \ + --title "Upstream reconciliation pending" \ + --body-file "$REPORT" + fi From 5068e4926f25dd4dfba1dc1fe97bee6f90d22e80 Mon Sep 17 00:00:00 2001 From: Alexander Gil Casas Date: Sat, 29 Aug 2026 22:29:48 +0200 Subject: [PATCH 07/41] chore(upstream): add one-time selective sync bootstrap --- scripts/bootstrap-upstream-sync.sh | 133 +++++++++++++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 scripts/bootstrap-upstream-sync.sh diff --git a/scripts/bootstrap-upstream-sync.sh b/scripts/bootstrap-upstream-sync.sh new file mode 100644 index 000000000..03e7334a7 --- /dev/null +++ b/scripts/bootstrap-upstream-sync.sh @@ -0,0 +1,133 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(git rev-parse --show-toplevel)" +cd "$ROOT" + +UPSTREAM_URL="https://github.com/agrahn/Android-Password-Store.git" +UPSTREAM_BRANCH="develop" + +if git remote get-url upstream >/dev/null 2>&1; then + git remote set-url upstream "$UPSTREAM_URL" +else + git remote add upstream "$UPSTREAM_URL" +fi + +git fetch --quiet --no-tags upstream "$UPSTREAM_BRANCH" + +find_pr_commit() { + local pr="$1" + local sha + sha="$(git log "upstream/$UPSTREAM_BRANCH" --format='%H' --grep="#${pr}" -n 1)" + if [[ -z "$sha" ]]; then + echo "Unable to locate upstream PR #$pr on upstream/$UPSTREAM_BRANCH" >&2 + exit 10 + fi + printf '%s' "$sha" +} + +apply_pr_paths() { + local pr="$1" + shift + local sha + sha="$(find_pr_commit "$pr")" + echo "Applying upstream #$pr from $sha" + + local path patch + for path in "$@"; do + patch="$(mktemp)" + git diff --binary "${sha}^" "$sha" -- "$path" > "$patch" + if [[ ! -s "$patch" ]]; then + echo "Expected $path to change in upstream #$pr, but it did not." >&2 + rm -f "$patch" + exit 11 + fi + if ! git apply --3way --index --whitespace=nowarn "$patch"; then + echo "Failed to three-way apply upstream #$pr path: $path" >&2 + rm -f "$patch" + git status --short >&2 || true + exit 12 + fi + rm -f "$patch" + done + + if ! git diff --cached --quiet; then + git commit -m "chore(upstream): port upstream #$pr" -m "Selective port from agrahn/Android-Password-Store PR #$pr. Fork-owned passkey implementation is intentionally excluded unless explicitly listed." + fi +} + +# Small classic-app fixes and complete recent-password timestamp lifecycle. +apply_pr_paths 936 \ + app/src/main/java/app/passwordstore/util/git/sshj/SshKey.kt +apply_pr_paths 938 \ + app/src/main/java/app/passwordstore/ui/crypto/PasswordCreationActivity.kt +apply_pr_paths 939 \ + app/src/main/java/app/passwordstore/ui/passwords/PasswordStore.kt +apply_pr_paths 941 \ + app/src/main/java/app/passwordstore/ui/passwords/PasswordStore.kt +apply_pr_paths 942 \ + app/src/main/java/app/passwordstore/ui/settings/RepositorySettings.kt \ + app/src/main/java/app/passwordstore/util/extensions/AndroidExtensions.kt + +# Contextual fast-unlock wording. Deliberately omit upstream's ui/credman passkey activity. +apply_pr_paths 1000 \ + app/src/main/java/app/passwordstore/ui/autofill/AutofillDecryptActivity.kt \ + app/src/main/java/app/passwordstore/ui/crypto/BasePGPActivity.kt \ + app/src/main/res/values/strings.xml \ + app/src/main/res/values-de/strings.xml + +apply_pr_paths 1007 \ + app/src/main/java/app/passwordstore/util/shortcuts/ShortcutHandler.kt + +# Fast-unlock state is isolated per PGP identity. This is a security invariant, not a passkey port. +apply_pr_paths 1011 \ + app/src/main/java/app/passwordstore/Application.kt \ + app/src/main/java/app/passwordstore/injection/prefs/PreferenceModule.kt \ + app/src/main/java/app/passwordstore/injection/prefs/UnlockPins.kt \ + app/src/main/java/app/passwordstore/ui/crypto/BasePGPActivity.kt \ + app/src/main/java/app/passwordstore/ui/crypto/PinDialog.kt \ + app/src/main/java/app/passwordstore/ui/settings/PasswordSettings.kt \ + app/src/main/java/app/passwordstore/util/extensions/AndroidExtensions.kt \ + app/src/main/java/app/passwordstore/util/settings/Migrations.kt \ + app/src/test/java/app/passwordstore/util/settings/MigrationsTest.kt + +apply_pr_paths 1014 \ + app/src/main/java/app/passwordstore/ui/crypto/DecryptActivity.kt \ + app/src/main/java/app/passwordstore/ui/settings/PasswordSettings.kt \ + app/src/main/java/app/passwordstore/util/settings/PreferenceKeys.kt \ + app/src/main/res/values/strings.xml \ + app/src/main/res/values-de/strings.xml +apply_pr_paths 1016 \ + app/src/main/java/app/passwordstore/util/autofill/Api30AutofillResponseBuilder.kt +apply_pr_paths 1022 \ + app/src/main/AndroidManifest.xml +apply_pr_paths 1026 \ + autofill-parser/src/main/java/com/github/androidpasswordstore/autofillparser/FeatureAndTrustDetection.kt +apply_pr_paths 1043 \ + app/src/main/java/app/passwordstore/ui/pgp/PGPKeyListActivity.kt + +# Generated data is adopted as one current snapshot rather than replaying update commits. +git checkout "upstream/$UPSTREAM_BRANCH" -- autofill-parser/src/main/assets/publicsuffixes +if ! git diff --cached --quiet; then + git commit -m "chore(autofill): refresh Public Suffix List from upstream" +fi + +# Keep the recurring tools executable in normal clones. +chmod +x scripts/upstream-audit.sh scripts/adopt-upstream-pr.sh scripts/mark-upstream-reviewed.sh + +# One-time bootstrap machinery must not remain in the resulting PR. +rm -f scripts/bootstrap-upstream-sync.sh .github/workflows/bootstrap-upstream-sync.yml + +git add -A +if ! git diff --cached --quiet; then + git commit -m "chore(upstream): finalize recurring reconciliation tooling" +fi + +# Normalize Kotlin/XML formatting after the selective ports. +./gradlew spotlessApply +git add -A +if ! git diff --cached --quiet; then + git commit -m "style: format selective upstream ports" +fi + +git push origin "HEAD:${GITHUB_REF_NAME}" From c05ac73a09bcea16ceb2e9bf8239037502415ead Mon Sep 17 00:00:00 2001 From: Alexander Gil Casas Date: Sat, 29 Aug 2026 22:30:56 +0200 Subject: [PATCH 08/41] chore(upstream): classify generated PSL data explicitly --- scripts/upstream-audit.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/upstream-audit.sh b/scripts/upstream-audit.sh index 74fe42cea..58a7aee12 100644 --- a/scripts/upstream-audit.sh +++ b/scripts/upstream-audit.sh @@ -49,6 +49,9 @@ classify_path() { passkeys/*|PASSKEYS.md|PasskeyStorage.md|app/src/main/java/app/passwordstore/passkeys/*|app/src/main/java/app/passwordstore/injection/passkeys/*|app/src/main/res/xml/passkey_provider.xml|app/src/main/res/values-v34/bools.xml) printf 'PASSKEY-PROTECTED' ;; + *publicsuffix*) + printf 'GENERATED-DATA' + ;; autofill-parser/*|app/src/main/java/app/passwordstore/util/autofill/*|app/src/main/java/app/passwordstore/ui/autofill/*) printf 'AUTOFILL' ;; @@ -61,9 +64,6 @@ classify_path() { .github/*) printf 'CI/REPOSITORY' ;; - *publicsuffix*) - printf 'GENERATED-DATA' - ;; *) printf 'CLASSIC-APP' ;; From 38563430a76c1e0e18882321e9b163386a2c6bc7 Mon Sep 17 00:00:00 2001 From: Alexander Gil Casas Date: Sat, 29 Aug 2026 22:31:01 +0200 Subject: [PATCH 09/41] ci(upstream): run one-time selective sync bootstrap --- .github/workflows/bootstrap-upstream-sync.yml | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 .github/workflows/bootstrap-upstream-sync.yml diff --git a/.github/workflows/bootstrap-upstream-sync.yml b/.github/workflows/bootstrap-upstream-sync.yml new file mode 100644 index 000000000..4b7f92f5b --- /dev/null +++ b/.github/workflows/bootstrap-upstream-sync.yml @@ -0,0 +1,35 @@ +name: Bootstrap selective upstream sync + +on: + push: + branches: + - sync/upstream-2026-08-29 + workflow_dispatch: + +permissions: + contents: write + +jobs: + bootstrap: + if: github.actor != 'github-actions[bot]' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + ref: sync/upstream-2026-08-29 + + - name: Setup build environment + uses: ./.github/reusable-workflows/setup-gradle + with: + java-version: 21 + + - name: Configure Git identity + shell: bash + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + + - name: Apply reviewed upstream changes + shell: bash + run: bash scripts/bootstrap-upstream-sync.sh From c05b301bd4e4ebf20c1740ad668bd170e81055c2 Mon Sep 17 00:00:00 2001 From: Alexander Gil Casas Date: Sat, 29 Aug 2026 22:32:17 +0200 Subject: [PATCH 10/41] ci(upstream): resolve timestamp port semantically --- scripts/bootstrap-upstream-sync.sh | 39 ++++++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/scripts/bootstrap-upstream-sync.sh b/scripts/bootstrap-upstream-sync.sh index 03e7334a7..7421483fe 100644 --- a/scripts/bootstrap-upstream-sync.sh +++ b/scripts/bootstrap-upstream-sync.sh @@ -59,8 +59,43 @@ apply_pr_paths() { # Small classic-app fixes and complete recent-password timestamp lifecycle. apply_pr_paths 936 \ app/src/main/java/app/passwordstore/util/git/sshj/SshKey.kt -apply_pr_paths 938 \ - app/src/main/java/app/passwordstore/ui/crypto/PasswordCreationActivity.kt + +# #938 overlaps fork-specific password creation work. Apply only its timestamp-cache invariant. +python3 <<'PY' +from pathlib import Path +path = Path("app/src/main/java/app/passwordstore/ui/crypto/PasswordCreationActivity.kt") +text = path.read_text() +old = ''' // associate the new password name with the last name's timestamp in history + val preference = getSharedPreferences("recent_password_history", Context.MODE_PRIVATE) + val oldFilePathHash = "${fullPath.trimEnd('/')}/$suggestedName.gpg".base64() + val timestamp = preference.getString(oldFilePathHash) + if (timestamp != null) { + preference.edit { + remove(oldFilePathHash) + putString(passwordFile.absolutePathString().base64(), timestamp) + } + } +''' +new = ''' // create/update timestamp on the current password file + val preference = getSharedPreferences("recent_password_history", Context.MODE_PRIVATE) + preference.edit { + suggestedName?.let { oldFile -> + val oldFilePathHash = "${fullPath.trimEnd('/')}/$oldFile.gpg".base64() + remove(oldFilePathHash) + } + putString( + passwordFile.absolutePathString().base64(), + System.currentTimeMillis().toString(), + ) + } +''' +if old not in text: + raise SystemExit("Unable to locate fork timestamp block for upstream #938") +path.write_text(text.replace(old, new, 1)) +PY +git add app/src/main/java/app/passwordstore/ui/crypto/PasswordCreationActivity.kt +git commit -m "chore(upstream): adapt upstream #938 timestamp lifecycle" -m "Preserve fork-specific password creation logic while updating recent-password timestamps on every create/edit." + apply_pr_paths 939 \ app/src/main/java/app/passwordstore/ui/passwords/PasswordStore.kt apply_pr_paths 941 \ From 3a647b06ca5f28d159b9aaf8ee673ad2acc213b9 Mon Sep 17 00:00:00 2001 From: Alexander Gil Casas Date: Sat, 29 Aug 2026 22:33:23 +0200 Subject: [PATCH 11/41] ci(upstream): adapt contextual fast unlock to fork --- scripts/bootstrap-upstream-sync.sh | 74 +++++++++++++++++++++--------- 1 file changed, 52 insertions(+), 22 deletions(-) diff --git a/scripts/bootstrap-upstream-sync.sh b/scripts/bootstrap-upstream-sync.sh index 7421483fe..a2118b0c8 100644 --- a/scripts/bootstrap-upstream-sync.sh +++ b/scripts/bootstrap-upstream-sync.sh @@ -57,8 +57,7 @@ apply_pr_paths() { } # Small classic-app fixes and complete recent-password timestamp lifecycle. -apply_pr_paths 936 \ - app/src/main/java/app/passwordstore/util/git/sshj/SshKey.kt +apply_pr_paths 936 app/src/main/java/app/passwordstore/util/git/sshj/SshKey.kt # #938 overlaps fork-specific password creation work. Apply only its timestamp-cache invariant. python3 <<'PY' @@ -96,23 +95,58 @@ PY git add app/src/main/java/app/passwordstore/ui/crypto/PasswordCreationActivity.kt git commit -m "chore(upstream): adapt upstream #938 timestamp lifecycle" -m "Preserve fork-specific password creation logic while updating recent-password timestamps on every create/edit." -apply_pr_paths 939 \ - app/src/main/java/app/passwordstore/ui/passwords/PasswordStore.kt -apply_pr_paths 941 \ - app/src/main/java/app/passwordstore/ui/passwords/PasswordStore.kt +apply_pr_paths 939 app/src/main/java/app/passwordstore/ui/passwords/PasswordStore.kt +apply_pr_paths 941 app/src/main/java/app/passwordstore/ui/passwords/PasswordStore.kt apply_pr_paths 942 \ app/src/main/java/app/passwordstore/ui/settings/RepositorySettings.kt \ app/src/main/java/app/passwordstore/util/extensions/AndroidExtensions.kt -# Contextual fast-unlock wording. Deliberately omit upstream's ui/credman passkey activity. -apply_pr_paths 1000 \ - app/src/main/java/app/passwordstore/ui/autofill/AutofillDecryptActivity.kt \ - app/src/main/java/app/passwordstore/ui/crypto/BasePGPActivity.kt \ - app/src/main/res/values/strings.xml \ - app/src/main/res/values-de/strings.xml +# #1000: keep the fork passkey stack out of this port. The classic Autofill call site and +# strings come from upstream; BasePGPActivity is adapted narrowly because the fork changed it. +apply_pr_paths 1000 app/src/main/java/app/passwordstore/ui/autofill/AutofillDecryptActivity.kt +python3 <<'PY' +from pathlib import Path +path = Path("app/src/main/java/app/passwordstore/ui/crypto/BasePGPActivity.kt") +text = path.read_text() +replacements = [ + ( + " protected fun getPersistentAndDecrypt(identifiers: List) {", + " protected fun getPersistentAndDecrypt(identifiers: List, action: String? = null) {", + ), + ( + " verifyPin(pinEncrypted, persistentIds, identifiers)", + " verifyPin(pinEncrypted, persistentIds, identifiers, action)", + ), + ( + " identifiers: List,\n isError: Boolean = false,", + " identifiers: List,\n action: String?,\n isError: Boolean = false,", + ), + ( + " description = resources.getString(R.string.pin_entry_description),", + ''' description = + when (action) { + "autofill" -> resources.getString(R.string.pin_entry_autofill_description) + "passkey" -> resources.getString(R.string.pin_entry_passkey_description) + else -> resources.getString(R.string.pin_entry_description) + },''', + ), + ( + " verifyPin(pinEncryptedUpdate, ids, identifiers, isError = true)", + " verifyPin(pinEncryptedUpdate, ids, identifiers, action, isError = true)", + ), +] +for old, new in replacements: + if old not in text: + raise SystemExit(f"Unable to locate BasePGPActivity #1000 fragment: {old!r}") + text = text.replace(old, new, 1) +path.write_text(text) +PY +git add app/src/main/java/app/passwordstore/ui/crypto/BasePGPActivity.kt +git commit -m "chore(upstream): adapt upstream #1000 fast unlock context" -m "Preserve fork-specific crypto/passkey code while carrying the Autofill/PIN context through BasePGPActivity." +apply_pr_paths 1000 app/src/main/res/values/strings.xml +apply_pr_paths 1000 app/src/main/res/values-de/strings.xml -apply_pr_paths 1007 \ - app/src/main/java/app/passwordstore/util/shortcuts/ShortcutHandler.kt +apply_pr_paths 1007 app/src/main/java/app/passwordstore/util/shortcuts/ShortcutHandler.kt # Fast-unlock state is isolated per PGP identity. This is a security invariant, not a passkey port. apply_pr_paths 1011 \ @@ -132,14 +166,10 @@ apply_pr_paths 1014 \ app/src/main/java/app/passwordstore/util/settings/PreferenceKeys.kt \ app/src/main/res/values/strings.xml \ app/src/main/res/values-de/strings.xml -apply_pr_paths 1016 \ - app/src/main/java/app/passwordstore/util/autofill/Api30AutofillResponseBuilder.kt -apply_pr_paths 1022 \ - app/src/main/AndroidManifest.xml -apply_pr_paths 1026 \ - autofill-parser/src/main/java/com/github/androidpasswordstore/autofillparser/FeatureAndTrustDetection.kt -apply_pr_paths 1043 \ - app/src/main/java/app/passwordstore/ui/pgp/PGPKeyListActivity.kt +apply_pr_paths 1016 app/src/main/java/app/passwordstore/util/autofill/Api30AutofillResponseBuilder.kt +apply_pr_paths 1022 app/src/main/AndroidManifest.xml +apply_pr_paths 1026 autofill-parser/src/main/java/com/github/androidpasswordstore/autofillparser/FeatureAndTrustDetection.kt +apply_pr_paths 1043 app/src/main/java/app/passwordstore/ui/pgp/PGPKeyListActivity.kt # Generated data is adopted as one current snapshot rather than replaying update commits. git checkout "upstream/$UPSTREAM_BRANCH" -- autofill-parser/src/main/assets/publicsuffixes From 02d0ffd7393e5b044a9bc401a6cb444bfd650a61 Mon Sep 17 00:00:00 2001 From: Alexander Gil Casas Date: Sat, 29 Aug 2026 22:36:44 +0200 Subject: [PATCH 12/41] ci(upstream): adapt per-key unlock PIN isolation --- scripts/bootstrap-upstream-sync.sh | 302 ++++++++++++++++++++++++++++- 1 file changed, 294 insertions(+), 8 deletions(-) diff --git a/scripts/bootstrap-upstream-sync.sh b/scripts/bootstrap-upstream-sync.sh index a2118b0c8..f179016fe 100644 --- a/scripts/bootstrap-upstream-sync.sh +++ b/scripts/bootstrap-upstream-sync.sh @@ -148,17 +148,303 @@ apply_pr_paths 1000 app/src/main/res/values-de/strings.xml apply_pr_paths 1007 app/src/main/java/app/passwordstore/util/shortcuts/ShortcutHandler.kt -# Fast-unlock state is isolated per PGP identity. This is a security invariant, not a passkey port. +# #1011: isolate fast-unlock PINs by PGP identity. The upstream PR contains a qualifier file +# accidentally named "as UnlockPins.kt"; adopt its content and normalize the filename here. apply_pr_paths 1011 \ app/src/main/java/app/passwordstore/Application.kt \ app/src/main/java/app/passwordstore/injection/prefs/PreferenceModule.kt \ - app/src/main/java/app/passwordstore/injection/prefs/UnlockPins.kt \ - app/src/main/java/app/passwordstore/ui/crypto/BasePGPActivity.kt \ - app/src/main/java/app/passwordstore/ui/crypto/PinDialog.kt \ - app/src/main/java/app/passwordstore/ui/settings/PasswordSettings.kt \ - app/src/main/java/app/passwordstore/util/extensions/AndroidExtensions.kt \ - app/src/main/java/app/passwordstore/util/settings/Migrations.kt \ - app/src/test/java/app/passwordstore/util/settings/MigrationsTest.kt + "app/src/main/java/app/passwordstore/injection/prefs/as UnlockPins.kt" +git mv \ + "app/src/main/java/app/passwordstore/injection/prefs/as UnlockPins.kt" \ + app/src/main/java/app/passwordstore/injection/prefs/UnlockPins.kt +git commit -m "chore(upstream): normalize upstream #1011 qualifier filename" + +# BasePGPActivity has fork-owned authentication/passkey changes, so port #1011 semantically. +python3 <<'PY' +from pathlib import Path + +path = Path("app/src/main/java/app/passwordstore/ui/crypto/BasePGPActivity.kt") +text = path.read_text() + +replacements = [ + ( + "import app.passwordstore.injection.prefs.SettingsPreferences\n", + "import app.passwordstore.injection.prefs.SettingsPreferences\nimport app.passwordstore.injection.prefs.UnlockPins\n", + ), + ( + "import javax.inject.Inject\n", + "import javax.inject.Inject\nimport kotlin.math.max\n", + ), + ( + " @PGPPassphrases @Inject lateinit var persistentPassphrases: SharedPreferences\n\n", + " @PGPPassphrases @Inject lateinit var persistentPassphrases: SharedPreferences\n\n @UnlockPins @Inject lateinit var unlockPins: SharedPreferences\n\n", + ), +] +for old, new in replacements: + if old not in text: + raise SystemExit(f"Unable to locate #1011 BasePGPActivity fragment: {old!r}") + text = text.replace(old, new, 1) + +old_setup = ''' if (persistentPassphrases.getString("unlock_pin", null) == null) { + val pinDialog = + PinDialog.newInstance( + title = resources.getString(R.string.pin_new_entry_title), + description = resources.getString(R.string.pin_new_entry_description), + clearOnDismiss = passphrase, + ) + pinDialog.show(supportFragmentManager, "PIN_DIALOG") + pinDialog.setFragmentResultListener(PinDialog.PIN_RESULT_KEY) { key, bundle -> + if (key == PinDialog.PIN_RESULT_KEY) { + val pin = + requireNotNull(bundle.getCharArray(PinDialog.PIN_KEY)) { + "returned PIN is null" + } + if (pin.size >= 4) { + persistentPassphrases.edit { + putString( + "unlock_pin", // reset and prepend PIN attempt counter + AESEncryption.encrypt( + charArrayOf('0', ':') + pin, + keyType = KeyType.PERSISTENT, + ) + ?.concatToString(), + ) + putString( + id, + AESEncryption.encrypt(passphrase, keyType = KeyType.PERSISTENT) + ?.concatToString(), + ) + putLong( + PreferenceKeys.BIOMETRICS_AND_PIN_LAST_USE, + Instant.now().toEpochMilli(), + ) + } + } + pin.wipe() + } + } +''' +new_setup = ''' if (unlockPins.getString(id, null) == null) { + val pinDialog = + PinDialog.newInstance( + title = resources.getString(R.string.pin_new_entry_title), + description = resources.getString(R.string.pin_new_entry_description), + clearOnDismiss = passphrase, + ) + pinDialog.show(supportFragmentManager, "PIN_DIALOG") + pinDialog.setFragmentResultListener(PinDialog.PIN_RESULT_KEY) { key, bundle -> + if (key == PinDialog.PIN_RESULT_KEY) { + val pin = bundle.getCharArray(PinDialog.PIN_KEY) + if (pin != null && pin.size >= 4) { + unlockPins.edit { + putString( + id, // reset and prepend PIN attempt counter + AESEncryption.encrypt( + charArrayOf('0', ':') + pin, + keyType = KeyType.PERSISTENT, + ) + ?.concatToString(), + ) + } + persistentPassphrases.edit { + putString( + id, + AESEncryption.encrypt(passphrase, keyType = KeyType.PERSISTENT) + ?.concatToString(), + ) + putLong( + PreferenceKeys.BIOMETRICS_AND_PIN_LAST_USE, + Instant.now().toEpochMilli(), + ) + } + } + pin?.wipe() + } + } +''' +if old_setup not in text: + raise SystemExit("Unable to locate global PIN setup block for #1011") +text = text.replace(old_setup, new_setup, 1) + +old_timeout = ''' ) + persistentPassphrases.edit { clear() } + + val persistentIds = + identifiers.map { it.toString() }.filter { persistentPassphrases.contains(it) } + val pinEncrypted = persistentPassphrases.getString("unlock_pin", null)?.toCharArray() +''' +new_timeout = ''' ) { + persistentPassphrases.edit { clear() } + unlockPins.edit { clear() } + } + + val persistentIds = + identifiers.map { it.toString() }.filter { persistentPassphrases.contains(it) } + val encryptedPins = + unlockPins + .getAll() + .filterKeys { persistentIds.contains(it) } + .mapValues { (it.value as String).toCharArray() } +''' +if old_timeout not in text: + raise SystemExit("Unable to locate timeout/global PIN cache block for #1011") +text = text.replace(old_timeout, new_timeout, 1) + +old_pin_branch = ''' } else if ( + !persistentIds.none() && + identifiers.map { it.toString() }.filter { cachedPassphrases.containsKey(it) }.none() && + AESEncryption.isHardwareBacked(KeyType.PERSISTENT) && + settings.getString(PreferenceKeys.PREF_FAST_UNLOCK_OPTION, "disabled") == "PIN" && + pinEncrypted != null + ) { + verifyPin(pinEncrypted, persistentIds, identifiers, action) +''' +new_pin_branch = ''' } else if ( + !encryptedPins.none() && + identifiers.map { it.toString() }.filter { cachedPassphrases.containsKey(it) }.none() && + AESEncryption.isHardwareBacked(KeyType.PERSISTENT) && + settings.getString(PreferenceKeys.PREF_FAST_UNLOCK_OPTION, "disabled") == "PIN" + ) { + verifyPin(encryptedPins, identifiers, action) +''' +if old_pin_branch not in text: + raise SystemExit("Unable to locate global PIN branch for #1011") +text = text.replace(old_pin_branch, new_pin_branch, 1) + +start_marker = " /* Asks for and verifies the user PIN for unlocking a store entry. */\n private fun verifyPin(" +end_marker = "\n protected fun decrypt(identifiers: List, isError: Boolean = false) {" +start = text.find(start_marker) +end = text.find(end_marker, start) +if start < 0 or end < 0: + raise SystemExit("Unable to locate verifyPin function boundaries for #1011") + +new_verify = ''' /* Asks for and verifies the user PIN for unlocking a store entry. */ + private fun verifyPin( + encryptedPins: Map, + identifiers: List, + action: String?, + isError: Boolean = false, + ) { + val pinDialog = + PinDialog.newInstance( + title = resources.getString(R.string.pin_entry_title), + description = + when (action) { + "autofill" -> resources.getString(R.string.pin_entry_autofill_description) + "passkey" -> resources.getString(R.string.pin_entry_passkey_description) + else -> resources.getString(R.string.pin_entry_description) + }, + ) + if (isError) pinDialog.setError() + pinDialog.show(supportFragmentManager, "PIN_DIALOG") + pinDialog.setFragmentResultListener(PinDialog.PIN_RESULT_KEY) { key, bundle -> + if (key == PinDialog.PIN_RESULT_KEY) { + if (bundle.getBoolean(PinDialog.PIN_CANCEL)) { + decrypt(identifiers) + } else { + val pin = requireNotNull(bundle.getCharArray(PinDialog.PIN_KEY)) { "returned PIN is null" } + var pinRetries = 0 + var pinOk = false + + for ((id, encryptedPin) in encryptedPins) { + val cachedPin = + AESEncryption.decrypt(encryptedPin, keyType = KeyType.PERSISTENT)?.let { cached -> + cached.copyOfRange(cached.indexOf(':') + 1, cached.size).also { + pinRetries = + max( + pinRetries, + cached + .copyOfRange(0, cached.indexOf(':')) + .concatToString() + .toIntOrNull() ?: MAX_RETRIES, + ) + cached.wipe() + } + } + pinOk = cachedPin?.let { it.contentEquals(pin) } ?: false + cachedPin?.wipe() + if (pinOk) { + updatePinAttemptCounter(encryptedPins, 0) + persistentPassphrases + .getString(id, null) + ?.toCharArray() + ?.let { passEncrypted -> + AESEncryption.decrypt(passEncrypted, keyType = KeyType.PERSISTENT) + } + ?.let { pass -> + AESEncryption.encrypt(pass)?.let { cachedPassphrases.put(id, it) } + pass.wipe() + } + break + } + } + + pin.wipe() + + if (pinOk) { + decrypt(identifiers) + } else if (++pinRetries < MAX_RETRIES) { + val encryptedPinsUpdated = updatePinAttemptCounter(encryptedPins, pinRetries) + verifyPin(encryptedPinsUpdated, identifiers, action, isError = true) + } else { + // Reset only the relevant identities after the retry budget is exhausted. + encryptedPins.keys.forEach { id -> + cachedPassphrases.remove(id) + persistentPassphrases.edit { remove(id) } + unlockPins.edit { remove(id) } + } + decrypt(identifiers) + } + } + } + } + } + + /** Updates and persists the shared retry counter for the relevant per-PGP-ID PINs. */ + private fun updatePinAttemptCounter( + encryptedPins: Map, + attempts: Int, + ): Map { + val updatedEncryptedPins = mutableMapOf() + unlockPins.edit { + encryptedPins.forEach { (id, encryptedPin) -> + AESEncryption.decrypt(encryptedPin, keyType = KeyType.PERSISTENT) + ?.let { cached -> + cached.copyOfRange(cached.indexOf(':') + 1, cached.size).also { cached.wipe() } + } + ?.let { pin -> + AESEncryption.encrypt( + (attempts.toString() + ":").toCharArray() + pin, + keyType = KeyType.PERSISTENT, + ) + ?.let { updated -> + putString(id, updated.concatToString()) + updatedEncryptedPins[id] = updated + } + pin.wipe() + } + ?: remove(id) + } + } + if (attempts == 0) { + persistentPassphrases.edit { + putLong(PreferenceKeys.BIOMETRICS_AND_PIN_LAST_USE, Instant.now().toEpochMilli()) + } + } + return updatedEncryptedPins + } +''' +text = text[:start] + new_verify + text[end:] +path.write_text(text) +PY +git add app/src/main/java/app/passwordstore/ui/crypto/BasePGPActivity.kt +git commit -m "fix(crypto): isolate fast-unlock PINs per PGP identity" -m "Semantic port of upstream #1011. Preserve fork-owned auth/passkey behavior while ensuring a PIN can unlock only the PGP identity it belongs to." + +apply_pr_paths 1011 app/src/main/java/app/passwordstore/ui/crypto/PinDialog.kt +apply_pr_paths 1011 app/src/main/java/app/passwordstore/ui/settings/PasswordSettings.kt +apply_pr_paths 1011 app/src/main/java/app/passwordstore/util/extensions/AndroidExtensions.kt +apply_pr_paths 1011 app/src/main/java/app/passwordstore/util/settings/Migrations.kt +apply_pr_paths 1011 app/src/test/java/app/passwordstore/util/settings/MigrationsTest.kt apply_pr_paths 1014 \ app/src/main/java/app/passwordstore/ui/crypto/DecryptActivity.kt \ From b7f6fcea9b48feb5c76a12985b341ae3d04e01a4 Mon Sep 17 00:00:00 2001 From: Alexander Gil Casas Date: Sat, 29 Aug 2026 22:38:42 +0200 Subject: [PATCH 13/41] ci(upstream): preserve fork PIN dialog semantics --- scripts/bootstrap-upstream-sync.sh | 142 ++++++++++++++++++++++++++++- 1 file changed, 141 insertions(+), 1 deletion(-) diff --git a/scripts/bootstrap-upstream-sync.sh b/scripts/bootstrap-upstream-sync.sh index f179016fe..bcff9c927 100644 --- a/scripts/bootstrap-upstream-sync.sh +++ b/scripts/bootstrap-upstream-sync.sh @@ -440,7 +440,147 @@ PY git add app/src/main/java/app/passwordstore/ui/crypto/BasePGPActivity.kt git commit -m "fix(crypto): isolate fast-unlock PINs per PGP identity" -m "Semantic port of upstream #1011. Preserve fork-owned auth/passkey behavior while ensuring a PIN can unlock only the PGP identity it belongs to." -apply_pr_paths 1011 app/src/main/java/app/passwordstore/ui/crypto/PinDialog.kt +# PinDialog also diverged in the fork. Keep clear-on-dismiss wiping while adopting explicit +# cancel results needed by the per-identity PIN flow. +cat > app/src/main/java/app/passwordstore/ui/crypto/PinDialog.kt <<'EOF' +/* + * Copyright © 2014-2026 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ + +package app.passwordstore.ui.crypto + +import android.content.DialogInterface +import android.os.Bundle +import android.view.KeyEvent +import android.view.WindowManager +import androidx.appcompat.app.AlertDialog +import androidx.core.widget.doOnTextChanged +import androidx.fragment.app.DialogFragment +import androidx.fragment.app.setFragmentResult +import app.passwordstore.R +import app.passwordstore.databinding.DialogPinEntryBinding +import app.passwordstore.util.extensions.unsafeLazy +import app.passwordstore.util.extensions.wipe +import com.google.android.material.dialog.MaterialAlertDialogBuilder + +/** [DialogFragment] to request a PIN from the user and forward it along. */ +class PinDialog : DialogFragment() { + + private val binding by unsafeLazy { DialogPinEntryBinding.inflate(layoutInflater) } + private var isError: Boolean = false + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + isCancelable = false + } + + override fun onCreateDialog(savedInstanceState: Bundle?): AlertDialog { + val builder = MaterialAlertDialogBuilder(requireContext()) + builder.setView(binding.root) + + val titleText = requireArguments().getString(TITLE_TEXT_EXTRA) + builder.setTitle(titleText) + + val descriptionText = requireArguments().getString(DESCRIPTION_TEXT_EXTRA) + binding.descriptionText.setText(descriptionText) + + builder.setPositiveButton(android.R.string.ok) { _, _ -> setPinAndDismiss() } + builder.setNegativeButton(android.R.string.cancel) { dialog, _ -> dialog.cancel() } + + val dialog = builder.create() + dialog.setCanceledOnTouchOutside(false) + dialog.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE) + dialog.setOnShowListener { + var pinLength = 0 + if (isError) { + binding.pinField.error = getString(R.string.pin_entry_wrong_input) + } + dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = false + binding.pinEditText.apply { + doOnTextChanged { s, _, _, _ -> + s?.let { + dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = it.length >= 4 + pinLength = it.length + } + binding.pinField.error = null + } + setOnKeyListener { _, keyCode, _ -> + if (keyCode == KeyEvent.KEYCODE_ENTER && pinLength >= 4) { + setPinAndDismiss() + return@setOnKeyListener true + } + keyCode == KeyEvent.KEYCODE_BACK + } + } + } + dialog.window?.setFlags( + WindowManager.LayoutParams.FLAG_SECURE, + WindowManager.LayoutParams.FLAG_SECURE, + ) + return dialog + } + + fun setError() { + isError = true + } + + override fun onDismiss(dialog: DialogInterface) { + requireArguments().getCharArray(CLEAR_ON_DISMISS_TEXT_EXTRA)?.wipe() + binding.pinEditText.text?.clear() + super.onDismiss(dialog) + } + + override fun onCancel(dialog: DialogInterface) { + super.onCancel(dialog) + setFragmentResult(PIN_RESULT_KEY, Bundle().also { it.putBoolean(PIN_CANCEL, true) }) + } + + private fun setPinAndDismiss() { + val pin = binding.pinEditText.text?.let { CharArray(it.length) { i -> it[i] } } + setFragmentResult( + PIN_RESULT_KEY, + Bundle().also { + it.putCharArray(PIN_KEY, pin) + it.putBoolean(PIN_CANCEL, false) + }, + ) + dismissAllowingStateLoss() + } + + companion object { + + private const val TITLE_TEXT_EXTRA = "title_text" + private const val DESCRIPTION_TEXT_EXTRA = "description_text" + private const val CLEAR_ON_DISMISS_TEXT_EXTRA = "clear_chars_text" + + const val PIN_RESULT_KEY = "pin_result" + const val PIN_KEY = "pin" + const val PIN_CANCEL = "cancel" + + fun newInstance( + title: String, + description: String, + clearOnDismiss: CharArray? = null, + ): PinDialog { + val extras = + Bundle().also { + it.apply { + putString(TITLE_TEXT_EXTRA, title) + putString(DESCRIPTION_TEXT_EXTRA, description) + putCharArray(CLEAR_ON_DISMISS_TEXT_EXTRA, clearOnDismiss) + } + } + val fragment = PinDialog() + fragment.arguments = extras + return fragment + } + } +} +EOF +git add app/src/main/java/app/passwordstore/ui/crypto/PinDialog.kt +git commit -m "fix(crypto): adapt upstream #1011 PIN cancellation" -m "Keep fork secret-wiping behavior while returning an explicit cancellation result to the per-PGP-ID fast-unlock flow." + apply_pr_paths 1011 app/src/main/java/app/passwordstore/ui/settings/PasswordSettings.kt apply_pr_paths 1011 app/src/main/java/app/passwordstore/util/extensions/AndroidExtensions.kt apply_pr_paths 1011 app/src/main/java/app/passwordstore/util/settings/Migrations.kt From 95c371078279b4394767e119cd693ad418028eaf Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 29 Aug 2026 20:39:03 +0000 Subject: [PATCH 14/41] chore(upstream): port upstream #936 Selective port from agrahn/Android-Password-Store PR #936. Fork-owned passkey implementation is intentionally excluded unless explicitly listed. --- app/src/main/java/app/passwordstore/util/git/sshj/SshKey.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/app/passwordstore/util/git/sshj/SshKey.kt b/app/src/main/java/app/passwordstore/util/git/sshj/SshKey.kt index 2f058147f..122e751d7 100644 --- a/app/src/main/java/app/passwordstore/util/git/sshj/SshKey.kt +++ b/app/src/main/java/app/passwordstore/util/git/sshj/SshKey.kt @@ -181,15 +181,16 @@ object SshKey { else false } - public enum class Type(val value: String) { + enum class Type(val value: String) { Imported("imported"), KeystoreNative("keystore_native"), KeystoreWrappedEd25519("keystore_wrapped_eddsa"), ImportedPGP("imported_pgp"); companion object { + private val mapByValue = entries.associateBy { it.value } - fun fromValue(value: String?): Type? = entries.associateBy { it.value }[value] + fun fromValue(value: String?): Type? = mapByValue[value] } } From be7ab99b6df5303e087430dd1f1410f92836b04f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 29 Aug 2026 20:39:03 +0000 Subject: [PATCH 15/41] chore(upstream): adapt upstream #938 timestamp lifecycle Preserve fork-specific password creation logic while updating recent-password timestamps on every create/edit. --- .../ui/crypto/PasswordCreationActivity.kt | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/app/passwordstore/ui/crypto/PasswordCreationActivity.kt b/app/src/main/java/app/passwordstore/ui/crypto/PasswordCreationActivity.kt index a02c02526..f6c8bffbc 100644 --- a/app/src/main/java/app/passwordstore/ui/crypto/PasswordCreationActivity.kt +++ b/app/src/main/java/app/passwordstore/ui/crypto/PasswordCreationActivity.kt @@ -496,15 +496,17 @@ class PasswordCreationActivity : BasePGPActivity() { passwordFile.writeBytes(result.getOrThrow().toByteArray()) } - // associate the new password name with the last name's timestamp in history + // create/update timestamp on the current password file val preference = getSharedPreferences("recent_password_history", Context.MODE_PRIVATE) - val oldFilePathHash = "${fullPath.trimEnd('/')}/$suggestedName.gpg".base64() - val timestamp = preference.getString(oldFilePathHash) - if (timestamp != null) { - preference.edit { + preference.edit { + suggestedName?.let { oldFile -> + val oldFilePathHash = "${fullPath.trimEnd('/')}/$oldFile.gpg".base64() remove(oldFilePathHash) - putString(passwordFile.absolutePathString().base64(), timestamp) } + putString( + passwordFile.absolutePathString().base64(), + System.currentTimeMillis().toString(), + ) } val returnIntent = Intent() From cf1ba217f103cf5bc2faacf5b7aa9dd5cbf5444c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 29 Aug 2026 20:39:04 +0000 Subject: [PATCH 16/41] chore(upstream): port upstream #939 Selective port from agrahn/Android-Password-Store PR #939. Fork-owned passkey implementation is intentionally excluded unless explicitly listed. --- .../java/app/passwordstore/ui/passwords/PasswordStore.kt | 7 +++++++ 1 file changed, 7 insertions(+) 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 ebc02c12f..9b0f136ff 100644 --- a/app/src/main/java/app/passwordstore/ui/passwords/PasswordStore.kt +++ b/app/src/main/java/app/passwordstore/ui/passwords/PasswordStore.kt @@ -481,6 +481,13 @@ class PasswordStore : BaseGitActivity() { if (item.file.isDirectory) filesToDelete.addAll(item.file.listFilesRecursively()) else filesToDelete.add(item.file) } + // remove to-be-deleted files from history + val preference = getSharedPreferences("recent_password_history", Context.MODE_PRIVATE) + preference.edit { + filesToDelete.forEach { file -> + remove(file.absolutePath.base64()) + } + } selectedItems.map { item -> item.file.deleteRecursively() } refreshPasswordList() AutofillMatcher.updateMatches(applicationContext, delete = filesToDelete) From 4db6ff46fb18e6fcd934153e440873aee091c0f2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 29 Aug 2026 20:39:04 +0000 Subject: [PATCH 17/41] chore(upstream): port upstream #941 Selective port from agrahn/Android-Password-Store PR #941. Fork-owned passkey implementation is intentionally excluded unless explicitly listed. --- .../app/passwordstore/ui/passwords/PasswordStore.kt | 10 ++++++++++ 1 file changed, 10 insertions(+) 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 9b0f136ff..5bbb9f67e 100644 --- a/app/src/main/java/app/passwordstore/ui/passwords/PasswordStore.kt +++ b/app/src/main/java/app/passwordstore/ui/passwords/PasswordStore.kt @@ -664,6 +664,16 @@ class PasswordStore : BaseGitActivity() { .show() } } else { + // update timestamp cache with the new file locations + val preference = getSharedPreferences("recent_password_history", Context.MODE_PRIVATE) + preference.edit { + sourceDestinationMap.forEach { (src, dest) -> + val srcPathHash = src.absolutePath.base64() + val timestamp = preference.getString(srcPathHash) + remove(srcPathHash) + putString(dest.absolutePath.base64(), timestamp) + } + } AutofillMatcher.updateMatches(this, sourceDestinationMap) } } From a4d219dfc7363c5379c69e70e9fd28245e74cb89 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 29 Aug 2026 20:39:04 +0000 Subject: [PATCH 18/41] chore(upstream): port upstream #942 Selective port from agrahn/Android-Password-Store PR #942. Fork-owned passkey implementation is intentionally excluded unless explicitly listed. --- .../java/app/passwordstore/ui/settings/RepositorySettings.kt | 2 ++ .../app/passwordstore/util/extensions/AndroidExtensions.kt | 4 ++++ 2 files changed, 6 insertions(+) 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 05bec3728..20cbb8c39 100644 --- a/app/src/main/java/app/passwordstore/ui/settings/RepositorySettings.kt +++ b/app/src/main/java/app/passwordstore/ui/settings/RepositorySettings.kt @@ -36,6 +36,7 @@ import app.passwordstore.util.coroutines.DispatcherProvider import app.passwordstore.util.extensions.getString import app.passwordstore.util.extensions.gitSecrets import app.passwordstore.util.extensions.launchActivity +import app.passwordstore.util.extensions.passwordHistory import app.passwordstore.util.extensions.sharedPrefs import app.passwordstore.util.extensions.snackbar import app.passwordstore.util.extensions.unsafeLazy @@ -352,6 +353,7 @@ class RepositorySettings(private val activity: FragmentActivity) : SettingsProvi removeDynamicShortcuts(dynamicShortcuts.map { it.id }.toMutableList()) } activity.sharedPrefs.edit { putBoolean(PreferenceKeys.REPOSITORY_INITIALIZED, false) } + activity.passwordHistory.edit { clear() } dialogInterface.cancel() activity.finish() } diff --git a/app/src/main/java/app/passwordstore/util/extensions/AndroidExtensions.kt b/app/src/main/java/app/passwordstore/util/extensions/AndroidExtensions.kt index dc0fc2065..5a9454f70 100644 --- a/app/src/main/java/app/passwordstore/util/extensions/AndroidExtensions.kt +++ b/app/src/main/java/app/passwordstore/util/extensions/AndroidExtensions.kt @@ -57,6 +57,10 @@ val Context.persistentPassphrases: SharedPreferences val Context.gitSecrets: SharedPreferences get() = getSharedPreferences("${BuildConfig.APPLICATION_ID}_git_secrets", 0) +/** Get the persistent pass file timestamps */ +val Context.passwordHistory: SharedPreferences + get() = getSharedPreferences("recent_password_history", 0) + /** Resolve [attr] from the [Context]'s theme */ fun Context.resolveAttribute(attr: Int): Int { val typedValue = TypedValue() From cde76f9a7a6b358b9f71732c4d3ac13947a81f7d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 29 Aug 2026 20:39:04 +0000 Subject: [PATCH 19/41] chore(upstream): port upstream #1000 Selective port from agrahn/Android-Password-Store PR #1000. Fork-owned passkey implementation is intentionally excluded unless explicitly listed. --- .../app/passwordstore/ui/autofill/AutofillDecryptActivity.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/app/passwordstore/ui/autofill/AutofillDecryptActivity.kt b/app/src/main/java/app/passwordstore/ui/autofill/AutofillDecryptActivity.kt index 45aa59273..6e6b13f38 100644 --- a/app/src/main/java/app/passwordstore/ui/autofill/AutofillDecryptActivity.kt +++ b/app/src/main/java/app/passwordstore/ui/autofill/AutofillDecryptActivity.kt @@ -74,7 +74,7 @@ class AutofillDecryptActivity : BasePGPActivity() { requireKeysExist { requireDecryptionKeysExist(PasswordRepository.getParentPath(filePath, repositoryPath)) { ids -> - getPersistentAndDecrypt(ids) + getPersistentAndDecrypt(ids, action = "autofill") } } } From 65d56b8dc541a2958c428ef62f4464c42c440ba6 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 29 Aug 2026 20:39:04 +0000 Subject: [PATCH 20/41] chore(upstream): adapt upstream #1000 fast unlock context Preserve fork-specific crypto/passkey code while carrying the Autofill/PIN context through BasePGPActivity. --- .../app/passwordstore/ui/crypto/BasePGPActivity.kt | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/app/passwordstore/ui/crypto/BasePGPActivity.kt b/app/src/main/java/app/passwordstore/ui/crypto/BasePGPActivity.kt index 1c3e455cd..d8bb9461b 100644 --- a/app/src/main/java/app/passwordstore/ui/crypto/BasePGPActivity.kt +++ b/app/src/main/java/app/passwordstore/ui/crypto/BasePGPActivity.kt @@ -612,7 +612,7 @@ open class BasePGPActivity : AppCompatActivity() { /* Find persistent PGP passphrases with matching key ID, unlock the first one * with biometrics or after PIN verification */ - protected fun getPersistentAndDecrypt(identifiers: List) { + protected fun getPersistentAndDecrypt(identifiers: List, action: String? = null) { // Detect AES key invalidation due to enrollment of a new fingerprint and emit warning if ( BiometricAuthenticator.canAuthenticate(this@BasePGPActivity) && @@ -682,7 +682,7 @@ open class BasePGPActivity : AppCompatActivity() { settings.getString(PreferenceKeys.PREF_FAST_UNLOCK_OPTION, "disabled") == "PIN" && pinEncrypted != null ) { - verifyPin(pinEncrypted, persistentIds, identifiers) + verifyPin(pinEncrypted, persistentIds, identifiers, action) } else { decrypt(identifiers) } @@ -693,12 +693,18 @@ open class BasePGPActivity : AppCompatActivity() { pinEncrypted: CharArray, ids: List, identifiers: List, + action: String?, isError: Boolean = false, ) { val pinDialog = PinDialog.newInstance( title = resources.getString(R.string.pin_entry_title), - description = resources.getString(R.string.pin_entry_description), + description = + when (action) { + "autofill" -> resources.getString(R.string.pin_entry_autofill_description) + "passkey" -> resources.getString(R.string.pin_entry_passkey_description) + else -> resources.getString(R.string.pin_entry_description) + }, ) if (isError) pinDialog.setError() pinDialog.show(supportFragmentManager, "PIN_DIALOG") @@ -756,7 +762,7 @@ open class BasePGPActivity : AppCompatActivity() { persistentPassphrases.edit { putString("unlock_pin", pinEncryptedUpdate.concatToString()) } - verifyPin(pinEncryptedUpdate, ids, identifiers, isError = true) + verifyPin(pinEncryptedUpdate, ids, identifiers, action, isError = true) } ?: throw NullPointerException() } else { // PIN verification failed, do not try again persistentPassphrases.edit { clear() } // reset PIN to prevent bruteforcing From 2f5842814202f1d404c998a19b876336dd85bf6f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 29 Aug 2026 20:39:04 +0000 Subject: [PATCH 21/41] chore(upstream): port upstream #1000 Selective port from agrahn/Android-Password-Store PR #1000. Fork-owned passkey implementation is intentionally excluded unless explicitly listed. --- app/src/main/res/values/strings.xml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f6e851544..068438f44 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -501,8 +501,10 @@ PIN Set PIN - Your PIN code for fast unlocking must be at least 4 digits long + Your PIN for fast unlocking must be at least 4 digits long. PIN verification - Enter your PIN code to show this entry + Enter your PIN to show this entry. + Enter your PIN to unlock the passkey. + Enter your PIN to unlock the credentials. Wrong input From a18e6bd60b9957033368d4ef6580ebfcf5dbc332 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 29 Aug 2026 20:39:04 +0000 Subject: [PATCH 22/41] chore(upstream): port upstream #1000 Selective port from agrahn/Android-Password-Store PR #1000. Fork-owned passkey implementation is intentionally excluded unless explicitly listed. --- app/src/main/res/values-de/strings.xml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 3c2691cc3..c9e73a196 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -489,8 +489,10 @@ PIN festlegen - Ihr PIN-Code zum schnellen Entsperren muss mindestens vier Ziffern lang sein. + Ihre PIN zum schnellen Entsperren muss mindestens vier Ziffern lang sein. PIN-Verifizierung - Geben Sie Ihren PIN-Code ein, um den Eintrag anzuzeigen. + Geben Sie Ihre PIN ein, um den Eintrag anzuzeigen. + Geben Sie Ihre PIN ein, um den Passkey freizuschalten. + Geben Sie Ihre PIN ein, um die Zugangsdaten freizuschalten. Falsche Eingabe From 3b0e63fd9280fa78f7e45be988bf8a19a51aa542 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 29 Aug 2026 20:39:04 +0000 Subject: [PATCH 23/41] chore(upstream): port upstream #1007 Selective port from agrahn/Android-Password-Store PR #1007. Fork-owned passkey implementation is intentionally excluded unless explicitly listed. --- .../util/shortcuts/ShortcutHandler.kt | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/app/passwordstore/util/shortcuts/ShortcutHandler.kt b/app/src/main/java/app/passwordstore/util/shortcuts/ShortcutHandler.kt index 177ba2b40..57dd87bee 100644 --- a/app/src/main/java/app/passwordstore/util/shortcuts/ShortcutHandler.kt +++ b/app/src/main/java/app/passwordstore/util/shortcuts/ShortcutHandler.kt @@ -39,18 +39,16 @@ class ShortcutHandler @Inject constructor(@ApplicationContext val context: Conte val shortcutManager: ShortcutManager = context.getSystemService() ?: return val shortcut = buildShortcut(item, intent) val shortcuts = shortcutManager.dynamicShortcuts - // If we're above or equal to the maximum shortcuts allowed, drop the last item. - if (shortcuts.size >= MAX_SHORTCUT_COUNT) { + // Drop any stale entry for this exact password, it is about to be re-added at the front. + shortcuts.removeAll { it.id == shortcut.id } + // Make room for the new shortcut by evicting the least recently used ones. + while (shortcuts.size >= MAX_SHORTCUT_COUNT) { // We'd just do List.removeLast but the Kotlin stdlib extension gets shadowed by the API 35 // JDK implementation shortcuts.removeAt(shortcuts.lastIndex) } - // Reverse the list so we can append our new shortcut at the 'end'. - shortcuts.reverse() - shortcuts.add(shortcut) - // Reverse it again, so the previous items are now in the correct order and our new item - // is at the front like it's supposed to. - shortcuts.reverse() + // The new shortcut goes at the front + shortcuts.add(0, shortcut) // Write back the new shortcuts. shortcutManager.dynamicShortcuts = shortcuts.map(::rebuildShortcut) } @@ -73,9 +71,9 @@ class ShortcutHandler @Inject constructor(@ApplicationContext val context: Conte /** Creates a [ShortcutInfo] from [item] and assigns [intent] to it. */ private fun buildShortcut(item: PasswordItem, intent: Intent): ShortcutInfo { - return ShortcutInfo.Builder(context, item.fullPathToParent) + return ShortcutInfo.Builder(context, item.longName) .setShortLabel(item.toString()) - .setLongLabel(item.fullPathToParent + item.toString()) + .setLongLabel("/${item.longName}") .setIcon(Icon.createWithResource(context, R.drawable.ic_lock_open_24px)) .setIntent(intent) .build() From 9076d2cceed189aec539725a4023467f35aeaa59 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 29 Aug 2026 20:39:04 +0000 Subject: [PATCH 24/41] chore(upstream): port upstream #1011 Selective port from agrahn/Android-Password-Store PR #1011. Fork-owned passkey implementation is intentionally excluded unless explicitly listed. --- app/src/main/java/app/passwordstore/Application.kt | 4 +++- .../passwordstore/injection/prefs/PreferenceModule.kt | 5 +++++ .../app/passwordstore/injection/prefs/as UnlockPins.kt | 10 ++++++++++ 3 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/app/passwordstore/injection/prefs/as UnlockPins.kt diff --git a/app/src/main/java/app/passwordstore/Application.kt b/app/src/main/java/app/passwordstore/Application.kt index b61392a4b..c253f906e 100644 --- a/app/src/main/java/app/passwordstore/Application.kt +++ b/app/src/main/java/app/passwordstore/Application.kt @@ -19,6 +19,7 @@ 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.ui.crypto.BasePGPActivity.Companion.cachedPassphrases import app.passwordstore.util.coroutines.DispatcherProvider @@ -46,6 +47,7 @@ class Application : android.app.Application(), SharedPreferences.OnSharedPrefere @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 @Inject lateinit var proxyUtils: ProxyUtils @@ -68,7 +70,7 @@ class Application : android.app.Application(), SharedPreferences.OnSharedPrefere logcat { "Debug logging enabled." } prefs.registerOnSharedPreferenceChangeListener(this) setNightMode() - runMigrations(filesDirPath, prefs, gitSettings) + runMigrations(filesDirPath, prefs, gitSettings, persistentPassphrases) proxyUtils.setDefaultProxy() DynamicColors.applyToActivitiesIfAvailable(this) setupScreenOffHandler() diff --git a/app/src/main/java/app/passwordstore/injection/prefs/PreferenceModule.kt b/app/src/main/java/app/passwordstore/injection/prefs/PreferenceModule.kt index e04976210..1a1e7dcba 100644 --- a/app/src/main/java/app/passwordstore/injection/prefs/PreferenceModule.kt +++ b/app/src/main/java/app/passwordstore/injection/prefs/PreferenceModule.kt @@ -42,6 +42,11 @@ class PreferenceModule { return context.getSharedPreferences("${BuildConfig.APPLICATION_ID}_passphrases", MODE_PRIVATE) } + @[Provides UnlockPins Reusable] + fun provideUnlockPins(@ApplicationContext context: Context): SharedPreferences { + return context.getSharedPreferences("${BuildConfig.APPLICATION_ID}_unlock_pins", MODE_PRIVATE) + } + @[Provides GitSecrets Reusable] fun provideGitSecrets(@ApplicationContext context: Context): SharedPreferences { return context.getSharedPreferences("${BuildConfig.APPLICATION_ID}_git_secrets", MODE_PRIVATE) diff --git a/app/src/main/java/app/passwordstore/injection/prefs/as UnlockPins.kt b/app/src/main/java/app/passwordstore/injection/prefs/as UnlockPins.kt new file mode 100644 index 000000000..c36a2a4c4 --- /dev/null +++ b/app/src/main/java/app/passwordstore/injection/prefs/as UnlockPins.kt @@ -0,0 +1,10 @@ +/* + * Copyright © 2014-2026 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ + +package app.passwordstore.injection.prefs + +import javax.inject.Qualifier + +@Qualifier @Retention(AnnotationRetention.RUNTIME) annotation class UnlockPins From 3a04e819bf26b1ecc604c586829cc3972816abd7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 29 Aug 2026 20:39:04 +0000 Subject: [PATCH 25/41] chore(upstream): normalize upstream #1011 qualifier filename --- .../injection/prefs/{as UnlockPins.kt => UnlockPins.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/src/main/java/app/passwordstore/injection/prefs/{as UnlockPins.kt => UnlockPins.kt} (100%) diff --git a/app/src/main/java/app/passwordstore/injection/prefs/as UnlockPins.kt b/app/src/main/java/app/passwordstore/injection/prefs/UnlockPins.kt similarity index 100% rename from app/src/main/java/app/passwordstore/injection/prefs/as UnlockPins.kt rename to app/src/main/java/app/passwordstore/injection/prefs/UnlockPins.kt From 1e5fbeee8afc0a9d430942acfbac54f558a9068e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 29 Aug 2026 20:39:04 +0000 Subject: [PATCH 26/41] fix(crypto): isolate fast-unlock PINs per PGP identity Semantic port of upstream #1011. Preserve fork-owned auth/passkey behavior while ensuring a PIN can unlock only the PGP identity it belongs to. --- .../ui/crypto/BasePGPActivity.kt | 179 +++++++++++------- 1 file changed, 109 insertions(+), 70 deletions(-) diff --git a/app/src/main/java/app/passwordstore/ui/crypto/BasePGPActivity.kt b/app/src/main/java/app/passwordstore/ui/crypto/BasePGPActivity.kt index d8bb9461b..e92de68b8 100644 --- a/app/src/main/java/app/passwordstore/ui/crypto/BasePGPActivity.kt +++ b/app/src/main/java/app/passwordstore/ui/crypto/BasePGPActivity.kt @@ -25,6 +25,7 @@ import app.passwordstore.data.crypto.CryptoRepository import app.passwordstore.data.repo.PasswordRepository import app.passwordstore.injection.prefs.PGPPassphrases import app.passwordstore.injection.prefs.SettingsPreferences +import app.passwordstore.injection.prefs.UnlockPins import app.passwordstore.ui.dialogs.PasswordDialog import app.passwordstore.ui.pgp.PGPKeyListActivity import app.passwordstore.util.auth.BiometricAuthenticator @@ -53,6 +54,7 @@ import java.util.concurrent.Executors import java.util.concurrent.ScheduledExecutorService import java.util.concurrent.TimeUnit import javax.inject.Inject +import kotlin.math.max import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking @@ -146,6 +148,8 @@ open class BasePGPActivity : AppCompatActivity() { */ @PGPPassphrases @Inject lateinit var persistentPassphrases: SharedPreferences + @UnlockPins @Inject lateinit var unlockPins: SharedPreferences + @Inject lateinit var repository: CryptoRepository @Inject lateinit var dispatcherProvider: DispatcherProvider @@ -546,7 +550,7 @@ open class BasePGPActivity : AppCompatActivity() { ) { /* Ask user for setting a PIN if not yet existing, encrypt and store it on the * device, then update passphrase in cache */ - if (persistentPassphrases.getString("unlock_pin", null) == null) { + if (unlockPins.getString(id, null) == null) { val pinDialog = PinDialog.newInstance( title = resources.getString(R.string.pin_new_entry_title), @@ -556,20 +560,19 @@ open class BasePGPActivity : AppCompatActivity() { pinDialog.show(supportFragmentManager, "PIN_DIALOG") pinDialog.setFragmentResultListener(PinDialog.PIN_RESULT_KEY) { key, bundle -> if (key == PinDialog.PIN_RESULT_KEY) { - val pin = - requireNotNull(bundle.getCharArray(PinDialog.PIN_KEY)) { - "returned PIN is null" - } - if (pin.size >= 4) { - persistentPassphrases.edit { + val pin = bundle.getCharArray(PinDialog.PIN_KEY) + if (pin != null && pin.size >= 4) { + unlockPins.edit { putString( - "unlock_pin", // reset and prepend PIN attempt counter + id, // reset and prepend PIN attempt counter AESEncryption.encrypt( charArrayOf('0', ':') + pin, keyType = KeyType.PERSISTENT, ) ?.concatToString(), ) + } + persistentPassphrases.edit { putString( id, AESEncryption.encrypt(passphrase, keyType = KeyType.PERSISTENT) @@ -581,7 +584,7 @@ open class BasePGPActivity : AppCompatActivity() { ) } } - pin.wipe() + pin?.wipe() } } } else { @@ -637,12 +640,18 @@ open class BasePGPActivity : AppCompatActivity() { if ( biometrics_and_pin_timeout > 0L && now - biometrics_and_pin_last_use >= TimeUnit.DAYS.toMillis(biometrics_and_pin_timeout) - ) + ) { persistentPassphrases.edit { clear() } + unlockPins.edit { clear() } + } val persistentIds = identifiers.map { it.toString() }.filter { persistentPassphrases.contains(it) } - val pinEncrypted = persistentPassphrases.getString("unlock_pin", null)?.toCharArray() + val encryptedPins = + unlockPins + .getAll() + .filterKeys { persistentIds.contains(it) } + .mapValues { (it.value as String).toCharArray() } if ( !persistentIds.none() && identifiers.map { it.toString() }.filter { cachedPassphrases.containsKey(it) }.none() && @@ -676,13 +685,12 @@ open class BasePGPActivity : AppCompatActivity() { if (result !is BiometricResult.Retry) decrypt(identifiers) } } else if ( - !persistentIds.none() && + !encryptedPins.none() && identifiers.map { it.toString() }.filter { cachedPassphrases.containsKey(it) }.none() && AESEncryption.isHardwareBacked(KeyType.PERSISTENT) && - settings.getString(PreferenceKeys.PREF_FAST_UNLOCK_OPTION, "disabled") == "PIN" && - pinEncrypted != null + settings.getString(PreferenceKeys.PREF_FAST_UNLOCK_OPTION, "disabled") == "PIN" ) { - verifyPin(pinEncrypted, persistentIds, identifiers, action) + verifyPin(encryptedPins, identifiers, action) } else { decrypt(identifiers) } @@ -690,8 +698,7 @@ open class BasePGPActivity : AppCompatActivity() { /* Asks for and verifies the user PIN for unlocking a store entry. */ private fun verifyPin( - pinEncrypted: CharArray, - ids: List, + encryptedPins: Map, identifiers: List, action: String?, isError: Boolean = false, @@ -710,69 +717,101 @@ open class BasePGPActivity : AppCompatActivity() { pinDialog.show(supportFragmentManager, "PIN_DIALOG") pinDialog.setFragmentResultListener(PinDialog.PIN_RESULT_KEY) { key, bundle -> if (key == PinDialog.PIN_RESULT_KEY) { - val pin = requireNotNull(bundle.getCharArray(PinDialog.PIN_KEY)) { "returned PIN is null" } - var (pinRetries, cachedPin) = - AESEncryption.decrypt(pinEncrypted, keyType = KeyType.PERSISTENT)?.let { cached -> - if (cached[1] == ':') { - Pair(cached[0].digitToInt(), cached.filterIndexed { i, _ -> i > 1 }.toCharArray()) - } else { - // fix PIN cache that does not have an attempt count prepended (old app version) - persistentPassphrases.edit { - putString( - "unlock_pin", - AESEncryption.encrypt( - charArrayOf('0', ':') + cached, - keyType = KeyType.PERSISTENT, + if (bundle.getBoolean(PinDialog.PIN_CANCEL)) { + decrypt(identifiers) + } else { + val pin = requireNotNull(bundle.getCharArray(PinDialog.PIN_KEY)) { "returned PIN is null" } + var pinRetries = 0 + var pinOk = false + + for ((id, encryptedPin) in encryptedPins) { + val cachedPin = + AESEncryption.decrypt(encryptedPin, keyType = KeyType.PERSISTENT)?.let { cached -> + cached.copyOfRange(cached.indexOf(':') + 1, cached.size).also { + pinRetries = + max( + pinRetries, + cached + .copyOfRange(0, cached.indexOf(':')) + .concatToString() + .toIntOrNull() ?: MAX_RETRIES, ) - ?.concatToString(), - ) + cached.wipe() + } } - Pair(0, cached) + pinOk = cachedPin?.let { it.contentEquals(pin) } ?: false + cachedPin?.wipe() + if (pinOk) { + updatePinAttemptCounter(encryptedPins, 0) + persistentPassphrases + .getString(id, null) + ?.toCharArray() + ?.let { passEncrypted -> + AESEncryption.decrypt(passEncrypted, keyType = KeyType.PERSISTENT) + } + ?.let { pass -> + AESEncryption.encrypt(pass)?.let { cachedPassphrases.put(id, it) } + pass.wipe() + } + break } - } ?: Pair(MAX_RETRIES, null) - if (cachedPin?.let { it.contentEquals(pin) } ?: false) { // PIN verifies successfully - persistentPassphrases.edit { - putString( - "unlock_pin", // reset to zero and prepend attempt counter - AESEncryption.encrypt(charArrayOf('0', ':') + pin, keyType = KeyType.PERSISTENT) - ?.concatToString(), - ) - putLong(PreferenceKeys.BIOMETRICS_AND_PIN_LAST_USE, Instant.now().toEpochMilli()) - } - ids.forEach { id -> - val passEncrypted = persistentPassphrases.getString(id, null)?.toCharArray() - val pass = - // re-encrypt passphrase for use until screen-off - AESEncryption.encrypt( - // decrypt persistently cached passphrase - AESEncryption.decrypt(passEncrypted, keyType = KeyType.PERSISTENT) - ) - pass?.let { cachedPassphrases.put(id, it) } } - decrypt(identifiers) - } else if ( - cachedPin != null && ++pinRetries < MAX_RETRIES - ) { // PIN verification failed, try again - val pinEncryptedUpdate = - AESEncryption.encrypt( - charArrayOf(pinRetries.digitToChar(), ':') + cachedPin, - keyType = KeyType.PERSISTENT, - ) - pinEncryptedUpdate?.let { // update PIN cache with incremented attempt counter - persistentPassphrases.edit { - putString("unlock_pin", pinEncryptedUpdate.concatToString()) + + pin.wipe() + + if (pinOk) { + decrypt(identifiers) + } else if (++pinRetries < MAX_RETRIES) { + val encryptedPinsUpdated = updatePinAttemptCounter(encryptedPins, pinRetries) + verifyPin(encryptedPinsUpdated, identifiers, action, isError = true) + } else { + // Reset only the relevant identities after the retry budget is exhausted. + encryptedPins.keys.forEach { id -> + cachedPassphrases.remove(id) + persistentPassphrases.edit { remove(id) } + unlockPins.edit { remove(id) } } - verifyPin(pinEncryptedUpdate, ids, identifiers, action, isError = true) - } ?: throw NullPointerException() - } else { // PIN verification failed, do not try again - persistentPassphrases.edit { clear() } // reset PIN to prevent bruteforcing - decrypt(identifiers) // decrypt with passphrase verification + decrypt(identifiers) + } } - pin.wipe() } } } + /** Updates and persists the shared retry counter for the relevant per-PGP-ID PINs. */ + private fun updatePinAttemptCounter( + encryptedPins: Map, + attempts: Int, + ): Map { + val updatedEncryptedPins = mutableMapOf() + unlockPins.edit { + encryptedPins.forEach { (id, encryptedPin) -> + AESEncryption.decrypt(encryptedPin, keyType = KeyType.PERSISTENT) + ?.let { cached -> + cached.copyOfRange(cached.indexOf(':') + 1, cached.size).also { cached.wipe() } + } + ?.let { pin -> + AESEncryption.encrypt( + (attempts.toString() + ":").toCharArray() + pin, + keyType = KeyType.PERSISTENT, + ) + ?.let { updated -> + putString(id, updated.concatToString()) + updatedEncryptedPins[id] = updated + } + pin.wipe() + } + ?: remove(id) + } + } + if (attempts == 0) { + persistentPassphrases.edit { + putLong(PreferenceKeys.BIOMETRICS_AND_PIN_LAST_USE, Instant.now().toEpochMilli()) + } + } + return updatedEncryptedPins + } + protected fun decrypt(identifiers: List, isError: Boolean = false) { val passphrases = cachedPassphrases.filterKeys { identifiers.map { it.toString() }.contains(it) From 5ffe2e69718a4f5a00ea67669d54ac07af9318ce Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 29 Aug 2026 20:39:04 +0000 Subject: [PATCH 27/41] fix(crypto): adapt upstream #1011 PIN cancellation Keep fork secret-wiping behavior while returning an explicit cancellation result to the per-PGP-ID fast-unlock flow. --- .../app/passwordstore/ui/crypto/PinDialog.kt | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/app/passwordstore/ui/crypto/PinDialog.kt b/app/src/main/java/app/passwordstore/ui/crypto/PinDialog.kt index 01efd64c3..bdd6754ba 100644 --- a/app/src/main/java/app/passwordstore/ui/crypto/PinDialog.kt +++ b/app/src/main/java/app/passwordstore/ui/crypto/PinDialog.kt @@ -15,7 +15,6 @@ import androidx.fragment.app.DialogFragment import androidx.fragment.app.setFragmentResult import app.passwordstore.R import app.passwordstore.databinding.DialogPinEntryBinding -import app.passwordstore.util.extensions.finish import app.passwordstore.util.extensions.unsafeLazy import app.passwordstore.util.extensions.wipe import com.google.android.material.dialog.MaterialAlertDialogBuilder @@ -26,18 +25,26 @@ class PinDialog : DialogFragment() { private val binding by unsafeLazy { DialogPinEntryBinding.inflate(layoutInflater) } private var isError: Boolean = false + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + isCancelable = false + } + override fun onCreateDialog(savedInstanceState: Bundle?): AlertDialog { val builder = MaterialAlertDialogBuilder(requireContext()) builder.setView(binding.root) - var titleText = requireArguments().getString(TITLE_TEXT_EXTRA) + val titleText = requireArguments().getString(TITLE_TEXT_EXTRA) builder.setTitle(titleText) - var descriptionText = requireArguments().getString(DESCRIPTION_TEXT_EXTRA) + val descriptionText = requireArguments().getString(DESCRIPTION_TEXT_EXTRA) binding.descriptionText.setText(descriptionText) builder.setPositiveButton(android.R.string.ok) { _, _ -> setPinAndDismiss() } + builder.setNegativeButton(android.R.string.cancel) { dialog, _ -> dialog.cancel() } + val dialog = builder.create() + dialog.setCanceledOnTouchOutside(false) dialog.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE) dialog.setOnShowListener { var pinLength = 0 @@ -58,7 +65,7 @@ class PinDialog : DialogFragment() { setPinAndDismiss() return@setOnKeyListener true } - false + keyCode == KeyEvent.KEYCODE_BACK } } } @@ -81,12 +88,18 @@ class PinDialog : DialogFragment() { override fun onCancel(dialog: DialogInterface) { super.onCancel(dialog) - finish() + setFragmentResult(PIN_RESULT_KEY, Bundle().also { it.putBoolean(PIN_CANCEL, true) }) } private fun setPinAndDismiss() { val pin = binding.pinEditText.text?.let { CharArray(it.length) { i -> it[i] } } - setFragmentResult(PIN_RESULT_KEY, Bundle().also { it.putCharArray(PIN_KEY, pin) }) + setFragmentResult( + PIN_RESULT_KEY, + Bundle().also { + it.putCharArray(PIN_KEY, pin) + it.putBoolean(PIN_CANCEL, false) + }, + ) dismissAllowingStateLoss() } @@ -98,6 +111,7 @@ class PinDialog : DialogFragment() { const val PIN_RESULT_KEY = "pin_result" const val PIN_KEY = "pin" + const val PIN_CANCEL = "cancel" fun newInstance( title: String, From b4c6522907f2f06a60f3543ae51f1dfdcce585a9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 29 Aug 2026 20:39:04 +0000 Subject: [PATCH 28/41] chore(upstream): port upstream #1011 Selective port from agrahn/Android-Password-Store PR #1011. Fork-owned passkey implementation is intentionally excluded unless explicitly listed. --- .../app/passwordstore/ui/settings/PasswordSettings.kt | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/app/passwordstore/ui/settings/PasswordSettings.kt b/app/src/main/java/app/passwordstore/ui/settings/PasswordSettings.kt index b4f7aadff..1e97d72ea 100644 --- a/app/src/main/java/app/passwordstore/ui/settings/PasswordSettings.kt +++ b/app/src/main/java/app/passwordstore/ui/settings/PasswordSettings.kt @@ -5,15 +5,14 @@ package app.passwordstore.ui.settings -import android.content.SharedPreferences import android.text.InputType import androidx.core.content.edit import androidx.fragment.app.FragmentActivity import app.passwordstore.R -import app.passwordstore.injection.prefs.PGPPassphrases import app.passwordstore.util.auth.BiometricAuthenticator import app.passwordstore.util.extensions.persistentPassphrases import app.passwordstore.util.extensions.sharedPrefs +import app.passwordstore.util.extensions.unlockPins import app.passwordstore.util.settings.PreferenceKeys import de.Maxr1998.modernpreferences.PreferenceScreen import de.Maxr1998.modernpreferences.helpers.editText @@ -21,12 +20,9 @@ import de.Maxr1998.modernpreferences.helpers.onClick import de.Maxr1998.modernpreferences.helpers.singleChoice import de.Maxr1998.modernpreferences.helpers.switch import de.Maxr1998.modernpreferences.preferences.choice.SelectionItem -import javax.inject.Inject class PasswordSettings(private val activity: FragmentActivity) : SettingsProvider { - @PGPPassphrases @Inject lateinit var persistentPassphrases: SharedPreferences - override fun provideSettings(builder: PreferenceScreen.Builder) { builder.apply { val values = activity.resources.getStringArray(R.array.pwgen_provider_values) @@ -57,6 +53,7 @@ class PasswordSettings(private val activity: FragmentActivity) : SettingsProvide initialSelection = "disabled" onClick { activity.persistentPassphrases.edit { clear() } + activity.unlockPins.edit { clear() } true } } @@ -68,6 +65,7 @@ class PasswordSettings(private val activity: FragmentActivity) : SettingsProvide textInputType = InputType.TYPE_CLASS_NUMBER onClick { activity.persistentPassphrases.edit { clear() } + activity.unlockPins.edit { clear() } true } } From 0702990dc7a20b0fad8fff88f5d046a12dd97167 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 29 Aug 2026 20:39:04 +0000 Subject: [PATCH 29/41] chore(upstream): port upstream #1011 Selective port from agrahn/Android-Password-Store PR #1011. Fork-owned passkey implementation is intentionally excluded unless explicitly listed. --- .../app/passwordstore/util/extensions/AndroidExtensions.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/main/java/app/passwordstore/util/extensions/AndroidExtensions.kt b/app/src/main/java/app/passwordstore/util/extensions/AndroidExtensions.kt index 5a9454f70..f02725340 100644 --- a/app/src/main/java/app/passwordstore/util/extensions/AndroidExtensions.kt +++ b/app/src/main/java/app/passwordstore/util/extensions/AndroidExtensions.kt @@ -53,6 +53,10 @@ val Context.sharedPrefs: SharedPreferences val Context.persistentPassphrases: SharedPreferences get() = getSharedPreferences("${BuildConfig.APPLICATION_ID}_passphrases", 0) +/** Get the persistent unlock PINs [SharedPreferences] instance */ +val Context.unlockPins: SharedPreferences + get() = getSharedPreferences("${BuildConfig.APPLICATION_ID}_unlock_pins", MODE_PRIVATE) + /** Get the persistent Git server secrets [SharedPreferences] instance */ val Context.gitSecrets: SharedPreferences get() = getSharedPreferences("${BuildConfig.APPLICATION_ID}_git_secrets", 0) From 89545bc4f7585bac91ec9b3ec26a205880571147 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 29 Aug 2026 20:39:04 +0000 Subject: [PATCH 30/41] chore(upstream): port upstream #1011 Selective port from agrahn/Android-Password-Store PR #1011. Fork-owned passkey implementation is intentionally excluded unless explicitly listed. --- .../java/app/passwordstore/util/settings/Migrations.kt | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) 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 dcbf70e7c..b739f498e 100644 --- a/app/src/main/java/app/passwordstore/util/settings/Migrations.kt +++ b/app/src/main/java/app/passwordstore/util/settings/Migrations.kt @@ -31,6 +31,7 @@ fun runMigrations( filesDirPath: String, sharedPrefs: SharedPreferences, gitSettings: GitSettings, + persistentPassphrases: SharedPreferences, context: Context = Application.instance.applicationContext, runTest: Boolean = false, ) { @@ -43,7 +44,7 @@ fun runMigrations( removePersistentCredentialCache(sharedPrefs, gitSettings, context, runTest) if (!runTest) moveToPasswordGeneratorPrefs(sharedPrefs, context) deleteKeystoreWrappedEd25519Key(sharedPrefs, context) - migrateToFastUnlockOptions(sharedPrefs) + migrateToFastUnlockOptions(sharedPrefs, persistentPassphrases) } private fun deleteKeystoreWrappedEd25519Key(sharedPrefs: SharedPreferences, context: Context) { @@ -295,7 +296,11 @@ private fun createEncryptedPreferences(context: Context, fileName: String): Shar ) } -private fun migrateToFastUnlockOptions(sharedPrefs: SharedPreferences) { +private fun migrateToFastUnlockOptions( + sharedPrefs: SharedPreferences, + persistentPassphrases: SharedPreferences, +) { + persistentPassphrases.edit { remove("unlock_pin") } sharedPrefs.edit { if (sharedPrefs.getBoolean(PreferenceKeys.UNLOCK_PASSWORDS_WITH_PIN, false)) putString(PreferenceKeys.PREF_FAST_UNLOCK_OPTION, "fingerprint") From b05ab8c91ce7bc39612711afba7396922e87b2b1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 29 Aug 2026 20:39:04 +0000 Subject: [PATCH 31/41] chore(upstream): port upstream #1011 Selective port from agrahn/Android-Password-Store PR #1011. Fork-owned passkey implementation is intentionally excluded unless explicitly listed. --- .../util/settings/MigrationsTest.kt | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) 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 c3d83b7a1..04463d3b7 100644 --- a/app/src/test/java/app/passwordstore/util/settings/MigrationsTest.kt +++ b/app/src/test/java/app/passwordstore/util/settings/MigrationsTest.kt @@ -44,6 +44,7 @@ class MigrationsTest { assertNull(getString(PreferenceKeys.GIT_REMOTE_SERVER)) assertNull(getString(PreferenceKeys.GIT_REMOTE_LOCATION)) assertNull(getString(PreferenceKeys.GIT_REMOTE_PROTOCOL)) + assertNull(getString("unlock_pin")) } @Test @@ -60,6 +61,7 @@ class MigrationsTest { filesDir, sharedPrefs, GitSettings(sharedPrefs, gitSecrets, filesDir), + sharedPrefs, context, runTest = true, ) @@ -83,6 +85,7 @@ class MigrationsTest { filesDir, sharedPrefs, GitSettings(sharedPrefs, gitSecrets, filesDir), + sharedPrefs, context, runTest = true, ) @@ -106,6 +109,7 @@ class MigrationsTest { filesDir, sharedPrefs, GitSettings(sharedPrefs, gitSecrets, filesDir), + sharedPrefs, context, runTest = true, ) @@ -122,6 +126,7 @@ class MigrationsTest { filesDir, sharedPrefs, GitSettings(sharedPrefs, gitSecrets, filesDir), + sharedPrefs, context, runTest = true, ) @@ -136,6 +141,7 @@ class MigrationsTest { filesDir, sharedPrefs, GitSettings(sharedPrefs, gitSecrets, filesDir), + sharedPrefs, context, runTest = true, ) @@ -150,6 +156,7 @@ class MigrationsTest { filesDir, sharedPrefs, GitSettings(sharedPrefs, gitSecrets, filesDir), + sharedPrefs, context, runTest = true, ) @@ -164,6 +171,7 @@ class MigrationsTest { filesDir, sharedPrefs, GitSettings(sharedPrefs, gitSecrets, filesDir), + sharedPrefs, context, runTest = true, ) @@ -177,6 +185,7 @@ class MigrationsTest { filesDir, sharedPrefs, GitSettings(sharedPrefs, gitSecrets, filesDir), + sharedPrefs, context, runTest = true, ) @@ -193,6 +202,7 @@ class MigrationsTest { filesDir, sharedPrefs, GitSettings(sharedPrefs, gitSecrets, filesDir), + sharedPrefs, context, runTest = true, ) @@ -211,10 +221,27 @@ class MigrationsTest { filesDir, sharedPrefs, GitSettings(sharedPrefs, gitSecrets, filesDir), + sharedPrefs, context, runTest = true, ) assertFalse { sharedPrefs.contains(PreferenceKeys.CLEAR_PASSPHRASE_CACHE) } assertFalse { sharedPrefs.contains(PreferenceKeys.SSH_KEY_LOCAL_PASSPHRASE) } } + + @Test + fun verifyPersistentUnlockPin() { + sharedPrefs.edit { + putString("unlock_pin", "abcdefg") + } + runMigrations( + filesDir, + sharedPrefs, + GitSettings(sharedPrefs, gitSecrets, filesDir), + sharedPrefs, + context, + runTest = true, + ) + checkOldKeysAreRemoved() + } } From 696dffb4df281fd77aae902b6ca36a5a290ff677 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 29 Aug 2026 20:39:04 +0000 Subject: [PATCH 32/41] chore(upstream): port upstream #1014 Selective port from agrahn/Android-Password-Store PR #1014. Fork-owned passkey implementation is intentionally excluded unless explicitly listed. --- .../main/java/app/passwordstore/ui/crypto/DecryptActivity.kt | 5 ++++- .../java/app/passwordstore/ui/settings/PasswordSettings.kt | 5 +++++ .../java/app/passwordstore/util/settings/PreferenceKeys.kt | 1 + app/src/main/res/values-de/strings.xml | 2 ++ app/src/main/res/values/strings.xml | 2 ++ 5 files changed, 14 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/app/passwordstore/ui/crypto/DecryptActivity.kt b/app/src/main/java/app/passwordstore/ui/crypto/DecryptActivity.kt index 5e75aa55d..48e0854c9 100644 --- a/app/src/main/java/app/passwordstore/ui/crypto/DecryptActivity.kt +++ b/app/src/main/java/app/passwordstore/ui/crypto/DecryptActivity.kt @@ -271,7 +271,10 @@ class DecryptActivity : BasePGPActivity() { entry.extraContent.forEach { (key, value) -> if (key.contentEquals(PasswordEntry.EXTRA_CONTENT)) - items.add(FieldItem.createFreeformField(getString(R.string.crypto_extra_label), value)) + if (settings.getBoolean(PreferenceKeys.SHOW_EXTRA_CONTENT, true)) + items.add(FieldItem.createFreeformField(getString(R.string.crypto_extra_label), value)) + else + items.add(FieldItem.createPasswordField(getString(R.string.crypto_extra_label), value)) } val adapter = diff --git a/app/src/main/java/app/passwordstore/ui/settings/PasswordSettings.kt b/app/src/main/java/app/passwordstore/ui/settings/PasswordSettings.kt index 1e97d72ea..74a3c5574 100644 --- a/app/src/main/java/app/passwordstore/ui/settings/PasswordSettings.kt +++ b/app/src/main/java/app/passwordstore/ui/settings/PasswordSettings.kt @@ -91,6 +91,11 @@ class PasswordSettings(private val activity: FragmentActivity) : SettingsProvide titleRes = R.string.pref_clear_clipboard_title summaryRes = R.string.pref_clear_clipboard_summary } + switch(PreferenceKeys.SHOW_EXTRA_CONTENT) { + defaultValue = true + titleRes = R.string.show_extra_content_pref_title + summaryRes = R.string.show_extra_content_pref_summary + } } } } 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 c090b7cc0..80cfad3c5 100644 --- a/app/src/main/java/app/passwordstore/util/settings/PreferenceKeys.kt +++ b/app/src/main/java/app/passwordstore/util/settings/PreferenceKeys.kt @@ -73,6 +73,7 @@ object PreferenceKeys { const val SHOW_HIDDEN_CONTENTS = "show_hidden_contents" const val SORT_ORDER = "sort_order" const val SHOW_PASSWORD = "show_password" + const val SHOW_EXTRA_CONTENT = "show_extra_content" @Deprecated( message = "Use PREF_FAST_UNLOCK_OPTION instead", replaceWith = ReplaceWith("PreferenceKeys.PREF_FAST_UNLOCK_OPTION"), diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index c9e73a196..1c74a595e 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -218,6 +218,8 @@ Commit-Log anzeigen Zeige das Passwort Soll das entschlüsselte Passwort sichtbar sein? Dies deaktiviert nicht das Kopieren. + Zusatzangaben anzeigen + Soll der Inhalt des Felds mit den Zusatzangaben nach dem Entschlüsseln angezeigt werden? Schnelles Entsperren von Einträgen Erstellen Liste aktualisieren diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 068438f44..b153caf9f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -225,6 +225,8 @@ Show commit log Show the password Control the visibility of the passwords once decrypted. This does not disable copying to clipboard. + Show extra content + Control the visibility of the extra content once decrypted. Fast unlocking of entries Generate Refresh list From 747985d79e56741fc25c9937b97e28107bdcd223 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 29 Aug 2026 20:39:04 +0000 Subject: [PATCH 33/41] chore(upstream): port upstream #1016 Selective port from agrahn/Android-Password-Store PR #1016. Fork-owned passkey implementation is intentionally excluded unless explicitly listed. --- .../util/autofill/Api30AutofillResponseBuilder.kt | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/app/passwordstore/util/autofill/Api30AutofillResponseBuilder.kt b/app/src/main/java/app/passwordstore/util/autofill/Api30AutofillResponseBuilder.kt index c106ec344..2d9cec39e 100644 --- a/app/src/main/java/app/passwordstore/util/autofill/Api30AutofillResponseBuilder.kt +++ b/app/src/main/java/app/passwordstore/util/autofill/Api30AutofillResponseBuilder.kt @@ -184,19 +184,27 @@ class Api30AutofillResponseBuilder private constructor(form: FillableForm) : matchedFiles: List, ): FillResponse? { var datasetCount = 0 + val maxSuggestions = + inlineSuggestionsRequest?.maxSuggestionCount + ?: InlineSuggestionsRequest.SUGGESTION_COUNT_UNLIMITED val imeSpecs = inlineSuggestionsRequest?.inlinePresentationSpecs ?: emptyList() + fun nextImeSpec(): InlinePresentationSpec? = + when { + datasetCount < maxSuggestions -> imeSpecs.getOrNull(datasetCount) ?: imeSpecs.lastOrNull() + else -> null + } return FillResponse.Builder().run { for (file in matchedFiles) { - makeMatchDataset(context, file, imeSpecs.getOrNull(datasetCount))?.let { + makeMatchDataset(context, file, nextImeSpec())?.let { datasetCount++ addDataset(it) } } - makeGenerateDataset(context, imeSpecs.getOrNull(datasetCount))?.let { + makeGenerateDataset(context, nextImeSpec())?.let { datasetCount++ addDataset(it) } - makeSearchDataset(context, imeSpecs.getOrNull(datasetCount))?.let { + makeSearchDataset(context, nextImeSpec())?.let { datasetCount++ addDataset(it) } From c9cb3f46b4255b664b4603bda4777173534f033d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 29 Aug 2026 20:39:04 +0000 Subject: [PATCH 34/41] chore(upstream): port upstream #1022 Selective port from agrahn/Android-Password-Store PR #1022. Fork-owned passkey implementation is intentionally excluded unless explicitly listed. --- app/src/main/AndroidManifest.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index c3d606dce..318121780 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -8,6 +8,7 @@ android:installLocation="auto"> + Date: Sat, 29 Aug 2026 20:39:04 +0000 Subject: [PATCH 35/41] chore(upstream): port upstream #1026 Selective port from agrahn/Android-Password-Store PR #1026. Fork-owned passkey implementation is intentionally excluded unless explicitly listed. --- .../autofillparser/FeatureAndTrustDetection.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/autofill-parser/src/main/java/com/github/androidpasswordstore/autofillparser/FeatureAndTrustDetection.kt b/autofill-parser/src/main/java/com/github/androidpasswordstore/autofillparser/FeatureAndTrustDetection.kt index cf768f015..2d0d08c5d 100644 --- a/autofill-parser/src/main/java/com/github/androidpasswordstore/autofillparser/FeatureAndTrustDetection.kt +++ b/autofill-parser/src/main/java/com/github/androidpasswordstore/autofillparser/FeatureAndTrustDetection.kt @@ -77,6 +77,7 @@ private val TRUSTED_BROWSER_CERTIFICATE_HASH = "nA0iN59Ie3Ck+fi+wBc8+RoWRPCPkzhbW3gs43ZguoE=", // Original (GitHub release) "eRN80/UC/Z35sLtql9UY+ig0XwP/8M76QJ9omrs54hQ=", // GoodyOG's OLED fork (GitHub release) ), + "io.github.jqssun.helium" to arrayOf("CVVASU1T9IygUbyGw5LEbtspcl2lWSvYrzw9AnsxZrs="), "net.waterfox.android.release" to arrayOf( "8JHKOZi0nhWdI+6VWGmZx10LcCP8+cVBkqgTAKwWbhc=", // GitHub release @@ -144,6 +145,7 @@ private val BROWSER_MULTI_ORIGIN_METHOD = "com.vivaldi.browser" to BrowserMultiOriginMethod.Field, "eu.weblibre.gecko" to BrowserMultiOriginMethod.Field, "io.github.forkmaintainers.iceraven" to BrowserMultiOriginMethod.WebView, + "io.github.jqssun.helium" to BrowserMultiOriginMethod.Field, "net.waterfox.android.release" to BrowserMultiOriginMethod.Field, "org.bromite.bromite" to BrowserMultiOriginMethod.Field, "org.cromite.cromite" to BrowserMultiOriginMethod.Field, @@ -206,6 +208,7 @@ private val BROWSER_SAVE_FLAG_IF_NO_ACCESSIBILITY = "com.chrome.dev" to SaveInfo.FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE, "com.microsoft.emmx" to SaveInfo.FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE, "com.vivaldi.browser" to SaveInfo.FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE, + "io.github.jqssun.helium" to SaveInfo.FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE, "org.bromite.bromite" to SaveInfo.FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE, "org.cromite.cromite" to SaveInfo.FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE, "org.ungoogled.chromium.extensions.stable" to SaveInfo.FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE, From 490e55af13e0aaaa1d8c92f47c69ca2f386792f7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 29 Aug 2026 20:39:04 +0000 Subject: [PATCH 36/41] chore(upstream): port upstream #1043 Selective port from agrahn/Android-Password-Store PR #1043. Fork-owned passkey implementation is intentionally excluded unless explicitly listed. --- .../ui/pgp/PGPKeyListActivity.kt | 89 +++++++++++-------- 1 file changed, 54 insertions(+), 35 deletions(-) diff --git a/app/src/main/java/app/passwordstore/ui/pgp/PGPKeyListActivity.kt b/app/src/main/java/app/passwordstore/ui/pgp/PGPKeyListActivity.kt index d9fd6c6b9..9c937915e 100644 --- a/app/src/main/java/app/passwordstore/ui/pgp/PGPKeyListActivity.kt +++ b/app/src/main/java/app/passwordstore/ui/pgp/PGPKeyListActivity.kt @@ -12,6 +12,7 @@ import android.content.res.Configuration import android.net.Uri import android.os.Bundle import android.widget.CheckBox +import androidx.activity.OnBackPressedCallback import androidx.activity.compose.setContent import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts.CreateDocument @@ -128,6 +129,58 @@ class PGPKeyListActivity : AppCompatActivity() { } } + fun onNavigateBack( + isSelectingKeys: Boolean, + singleSelection: Boolean, + selectedKeyIds: Set, + ) { + val result = Intent() + if (isSelectingKeys) { + if (selectedKeyIds.isNotEmpty()) { + result.putExtra( + EXTRA_SELECTED_KEY, + selectedKeyIds.joinToString(separator = "\n"), + ) + val gpgIdDest = intent.getStringExtra("SUB_PATH") + gpgIdDest?.let { result.putExtra("SUB_PATH", it) } + setResult(RESULT_OK, result) + finish() + } else { + val okButtonText = + if (singleSelection) resources.getString(R.string.gpg_key_single_select) + else resources.getString(R.string.gpg_key_select) + MaterialAlertDialogBuilder(this) + .setTitle(R.string.no_keys_selected_dialog_title) + .setPositiveButton(okButtonText, null) + .setNegativeButton( + R.string.pgp_key_insecure_passphrase_warning_confirm // continue anyway + ) { _, _ -> + if (singleSelection && SshKey.pgpLongKeyId != 0L) SshKey.delete() + setResult(RESULT_CANCELED) + finish() + } + .setCancelable(false) + .show() + } + } else if (isAddingKeys && keysAdded) { + setResult(RESULT_OK, result) + finish() + } else { + setResult(RESULT_CANCELED, result) + finish() + } + } + + onBackPressedDispatcher.addCallback( + this, + object : + OnBackPressedCallback(enabled = isSelectingKeys) { // only enable if in key selection mode + override fun handleOnBackPressed() { + onNavigateBack(isSelectingKeys, singleSelection, selectedKeyIds) + } + }, + ) + setContent { APSTheme { Scaffold( @@ -140,41 +193,7 @@ class PGPKeyListActivity : AppCompatActivity() { } else stringResource(R.string.activity_label_pgp_key_manager), navigationIcon = painterResource(R.drawable.ic_arrow_back_24dp), onNavigationIconClick = { - val result = Intent() - if (isSelectingKeys) { - if (selectedKeyIds.isNotEmpty()) { - result.putExtra( - EXTRA_SELECTED_KEY, - selectedKeyIds.joinToString(separator = "\n"), - ) - val gpgIdDest = intent.getStringExtra("SUB_PATH") - gpgIdDest?.let { result.putExtra("SUB_PATH", it) } - setResult(RESULT_OK, result) - finish() - } else { - val okButtonText = - if (singleSelection) resources.getString(R.string.gpg_key_single_select) - else resources.getString(R.string.gpg_key_select) - MaterialAlertDialogBuilder(this) - .setTitle(R.string.no_keys_selected_dialog_title) - .setPositiveButton(okButtonText, null) - .setNegativeButton( - R.string.pgp_key_insecure_passphrase_warning_confirm // continue anyway - ) { _, _ -> - if (singleSelection && SshKey.pgpLongKeyId != 0L) SshKey.delete() - setResult(RESULT_CANCELED) - finish() - } - .setCancelable(false) - .show() - } - } else if (isAddingKeys && keysAdded) { - setResult(RESULT_OK, result) - finish() - } else { - setResult(RESULT_CANCELED, result) - finish() - } + onNavigateBack(isSelectingKeys, singleSelection, selectedKeyIds) }, backgroundColor = MaterialTheme.colorScheme.surface, ) From d023363363e1af99d2dbe577beeb9fbedf1fc889 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 29 Aug 2026 20:39:04 +0000 Subject: [PATCH 37/41] chore(autofill): refresh Public Suffix List from upstream --- .../src/main/assets/publicsuffixes | 69 ++++++++++++------- 1 file changed, 44 insertions(+), 25 deletions(-) diff --git a/autofill-parser/src/main/assets/publicsuffixes b/autofill-parser/src/main/assets/publicsuffixes index f24281da5..945457953 100644 --- a/autofill-parser/src/main/assets/publicsuffixes +++ b/autofill-parser/src/main/assets/publicsuffixes @@ -1,4 +1,4 @@ -141697 +141904 119 *.001.test.code-builder-stg.platform.salesforce.com *.0e.vc @@ -44,6 +44,7 @@ *.airflow.us-east-2.on.aws *.airflow.us-west-1.on.aws *.airflow.us-west-2.on.aws +*.aivencloud.com *.alces.network *.ap-east-1.airflow.amazonaws.com *.ap-east-1.rds.amazonaws.com @@ -108,7 +109,7 @@ *.compute.amazonaws.com *.compute.amazonaws.com.cn *.compute.estate -*.cryptonomic.net +*.cursorusercontent.com *.customer-oci.com *.d.crm.dev *.database.run @@ -189,7 +190,6 @@ *.oci.customer-oci.com *.ocp.customer-oci.com *.ocs.customer-oci.com -*.on-acorn.io *.on-k3s.io *.on-rancher.cloud *.on-rio.io @@ -417,7 +417,6 @@ ac.rw ac.se ac.sz ac.th -ac.tj ac.tz ac.ug ac.uk @@ -444,7 +443,6 @@ actor ad ad.jp adachi.tokyo.jp -adaptable.app adimo.co.uk adm.br adm.ec @@ -469,6 +467,7 @@ aem.network aem.page aem.reviews aero +aero.in aero.mv aerobatic.aero aeroclub.aero @@ -531,7 +530,6 @@ airtraffic.aero aisai.aichi.jp aisho.shiga.jp aiven.app -aivencloud.com aizubange.fukushima.jp aizumi.tokushima.jp aizumisato.fukushima.jp @@ -595,6 +593,7 @@ alta.no altervista.org alto-adige.it altoadige.it +alumni.in alvdal.no alwaysdata.net am @@ -618,6 +617,7 @@ amica amli.no amot.no amplifyapp.com +ams.scw.site amsterdam an.it analytics @@ -687,7 +687,6 @@ apartments api.br api.gov.uk api.lp.dev -api.stdlib.com apigee.io app app-ionos.space @@ -712,7 +711,6 @@ appwrite.network aq aq.it aquarelle -aquila.it ar ar.it ar.us @@ -833,7 +831,7 @@ auction audi audible audio -audnedaln.no +audnedal.no augustow.pl aukra.no aure.no @@ -1118,7 +1116,6 @@ birkenes.no bitbucket.io bitter.jp biz -biz.at biz.az biz.bb biz.cy @@ -1592,6 +1589,7 @@ cl.it claims clan.rip claude.app +claudeusercontent.com cleaning clerk.app clerkstage.app @@ -1750,6 +1748,8 @@ cockpit.nl-ams.scw.cloud cockpit.pl-waw.scw.cloud cocotte.jp codeberg.page +codepen.app +codepen.dev codes codespot.com coffee @@ -2013,6 +2013,7 @@ cx cx.ua cy cy.eu.org +cyb.ge cymru cyon.link cyon.site @@ -2127,14 +2128,13 @@ desi design design.aero det.br -deta.app -deta.dev deus-canvas.com deuxfleurs.eu deuxfleurs.page dev dev-myqnapcloud.com dev.br +dev.cv dev.project-study.com development.run devices.resinstaging.io @@ -2143,7 +2143,6 @@ df.leg.br dfirma.pl dgca.aero dgn.ec -dh.bytemark.co.uk dhl diadem.cloud diamonds @@ -2958,6 +2957,7 @@ fr-par-2.baremetal.scw.cloud fr.eu.org fr.it fra1-de.cloudjiffy.net +frame.claudeusercontent.com framer.ai framer.app framer.media @@ -2978,7 +2978,6 @@ freedesktop.org freemyip.com freesite.host freetls.fastly.net -frei.no freight.aero frenchkiss.jp fresenius @@ -3204,6 +3203,7 @@ gh.srv.us gi gialai.vn giehtavuoatna.no +gielda.no gift gifts gifu.gifu.jp @@ -3501,6 +3501,7 @@ grimstad.no gripe griw.gov.pl grocery +grok.me groks-the.info groks-this.info grondar.za @@ -3615,6 +3616,7 @@ hamada.shimane.jp hamamatsu.shizuoka.jp hamar.no hamaroy.no +hamarøy.no hamatama.saga.jp hamatonbetsu.hokkaido.jp hamburg @@ -3698,6 +3700,7 @@ hercules-app.com hercules-dev.com here here-for-more.info +here.now hermes herokuapp.com heroy.more-og-romsdal.no @@ -3827,6 +3830,7 @@ holy.jp home-webserver.de home.arpa home.dyndns.org +home64.de homebuilt.aero homedepot homedns.org @@ -3907,7 +3911,6 @@ hyogo.jp hypernode.io hyuga.miyazaki.jp hyundai -hzc.io hábmer.no hámmárfeasta.no hápmir.no @@ -4082,7 +4085,6 @@ inf.mk inf.ua infiniti info -info.at info.az info.bb info.bd @@ -4173,6 +4175,8 @@ ipfs.storacha.link ipfs.w3s.link ipifony.net ipiranga +ipv64.de +ipv64.net iq ir ir.md @@ -4593,6 +4597,7 @@ karelia.su kariwa.niigata.jp kariya.aichi.jp karlsoy.no +karlsøy.no karmoy.no karmøy.no karpacz.pl @@ -5169,6 +5174,7 @@ living livorno.it lk llc +llc.ge llp ln.cn lo.it @@ -5378,7 +5384,6 @@ matsuzaki.shizuoka.jp matta-varjjat.no mattel mayfirst.info -mayfirst.org mazowsze.pl mazury.pl mb.ca @@ -5858,6 +5863,7 @@ myfritz.link myfritz.net myftp.biz myftp.org +mygov.scot myhome-server.de myiphost.com myjino.ru @@ -6627,7 +6633,6 @@ onagawa.miyagi.jp oncilla.mythic-beasts.com ondigitalocean.app one -onfabrica.com ong ong.br onga.fukuoka.jp @@ -6638,6 +6643,8 @@ onion onjuku.chiba.jp onl online +online-server.cloud +online.ge online.th onna.okinawa.jp ono.fukui.jp @@ -7073,6 +7080,7 @@ pl.ua place platter-app.dev play +playcode.site playit.plus playstation playstation-cloud.com @@ -7208,7 +7216,6 @@ promo properties property protection -protonet.io pru prudential pruszkow.pl @@ -7329,6 +7336,7 @@ rdy.jp re re.it re.kr +re.no read read-books.org readmyblog.org @@ -7995,6 +8003,7 @@ scb scbl.fr-par.scw.cloud scbl.nl-ams.scw.cloud scbl.pl-waw.scw.cloud +sch.ac sch.ae sch.bd sch.id @@ -8015,6 +8024,7 @@ schokokeks.net scholarships school school.ge +school.in school.nz school.za schoolbus.jp @@ -8030,6 +8040,7 @@ scot scrapper-site.net scrapping.cc scrysec.com +scw.site sd sd.cn sd.us @@ -8493,6 +8504,7 @@ stord.no stordal.no store store.bb +store.cv store.dk store.nf store.ro @@ -8500,7 +8512,6 @@ store.st store.ve storebase.store storfjord.no -storj.farm strand.no stranda.no strapiapp.com @@ -8552,10 +8563,13 @@ stuff-4-sale.us stufftoread.com style su +su.it sub.jp subsc-pay.com subsc-pay.net sucks +sud-sardegna.it +sudsardegna.it sue.fukuoka.jp suedtirol.it suginami.tokyo.jp @@ -8578,6 +8592,7 @@ sunagawa.hokkaido.jp sund.no sunndal.no sunnyday.jp +suohkan.no supabase.co supabase.in supabase.net @@ -8832,6 +8847,7 @@ tirol tj tj.cn tjeldsund.no +tjielte.no tjmaxx tjome.no tjx @@ -8859,6 +8875,7 @@ tn tn.it tn.oxa.cloud tn.us +tnx.ge to to.gov.br to.it @@ -9064,7 +9081,6 @@ trentinoaltoadige.it trentinos-tirol.it trentinostirol.it trentinosud-tirol.it -trentinosudtirol.it trentinosued-tirol.it trentinosuedtirol.it trentinosüd-tirol.it @@ -9183,6 +9199,7 @@ u.se u2-local.xnbay.com u2.xnbay.com ua +ub.in ubank ube.yamaguchi.jp uber.space @@ -9199,6 +9216,7 @@ udono.mie.jp ueda.nagano.jp ueno.gunma.jp uenohara.yamanashi.jp +uenorge.no ufcfan.org ug ug.gov.pl @@ -9218,12 +9236,12 @@ uk.net uk.oxa.cloud uk.primetel.cloud uk.reclaim.cloud -uk0.bigv.io uki.kumamoto.jp ukiha.fukuoka.jp ullensaker.no ullensvang.no ulsan.kr +ulstein.no ulvik.no um.gov.pl umaji.kochi.jp @@ -9350,7 +9368,6 @@ val-d-aosta.it val-daosta.it val.run vald-aosta.it -valdaosta.it valer.hedmark.no valer.ostfold.no valle-aosta.it @@ -9399,6 +9416,7 @@ venice.it vennesla.no ventures verbania.it +verbano-cusio-ossola.it vercel.app vercel.dev vercel.run @@ -9498,7 +9516,6 @@ vladikavkaz.su vladimir.ru vladimir.su vlog.br -vm.bytemark.co.uk vn vn.ua voagat.no @@ -9518,7 +9535,7 @@ vp4.me vpndns.net vpnplus.to vps-host.net -vps.hrsn.au +vps.hrsn.net vps.mcdir.ru vr.it vs.it @@ -9575,10 +9592,12 @@ watch watches watson.jp waw.pl +waw.scw.site wazuka.kyoto.jp we.bs weather weatherchannel +web web.app web.bo web.core.usgovcloudapi.net From f8c3a78f2b7adb3c5b39fe2f3abbc4ac8097e49a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 29 Aug 2026 20:39:04 +0000 Subject: [PATCH 38/41] chore(upstream): finalize recurring reconciliation tooling --- .github/workflows/bootstrap-upstream-sync.yml | 35 - scripts/adopt-upstream-pr.sh | 0 scripts/bootstrap-upstream-sync.sh | 624 ------------------ scripts/mark-upstream-reviewed.sh | 0 scripts/upstream-audit.sh | 0 5 files changed, 659 deletions(-) delete mode 100644 .github/workflows/bootstrap-upstream-sync.yml mode change 100644 => 100755 scripts/adopt-upstream-pr.sh delete mode 100644 scripts/bootstrap-upstream-sync.sh mode change 100644 => 100755 scripts/mark-upstream-reviewed.sh mode change 100644 => 100755 scripts/upstream-audit.sh diff --git a/.github/workflows/bootstrap-upstream-sync.yml b/.github/workflows/bootstrap-upstream-sync.yml deleted file mode 100644 index 4b7f92f5b..000000000 --- a/.github/workflows/bootstrap-upstream-sync.yml +++ /dev/null @@ -1,35 +0,0 @@ -name: Bootstrap selective upstream sync - -on: - push: - branches: - - sync/upstream-2026-08-29 - workflow_dispatch: - -permissions: - contents: write - -jobs: - bootstrap: - if: github.actor != 'github-actions[bot]' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - fetch-depth: 0 - ref: sync/upstream-2026-08-29 - - - name: Setup build environment - uses: ./.github/reusable-workflows/setup-gradle - with: - java-version: 21 - - - name: Configure Git identity - shell: bash - run: | - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - - - name: Apply reviewed upstream changes - shell: bash - run: bash scripts/bootstrap-upstream-sync.sh diff --git a/scripts/adopt-upstream-pr.sh b/scripts/adopt-upstream-pr.sh old mode 100644 new mode 100755 diff --git a/scripts/bootstrap-upstream-sync.sh b/scripts/bootstrap-upstream-sync.sh deleted file mode 100644 index bcff9c927..000000000 --- a/scripts/bootstrap-upstream-sync.sh +++ /dev/null @@ -1,624 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -ROOT="$(git rev-parse --show-toplevel)" -cd "$ROOT" - -UPSTREAM_URL="https://github.com/agrahn/Android-Password-Store.git" -UPSTREAM_BRANCH="develop" - -if git remote get-url upstream >/dev/null 2>&1; then - git remote set-url upstream "$UPSTREAM_URL" -else - git remote add upstream "$UPSTREAM_URL" -fi - -git fetch --quiet --no-tags upstream "$UPSTREAM_BRANCH" - -find_pr_commit() { - local pr="$1" - local sha - sha="$(git log "upstream/$UPSTREAM_BRANCH" --format='%H' --grep="#${pr}" -n 1)" - if [[ -z "$sha" ]]; then - echo "Unable to locate upstream PR #$pr on upstream/$UPSTREAM_BRANCH" >&2 - exit 10 - fi - printf '%s' "$sha" -} - -apply_pr_paths() { - local pr="$1" - shift - local sha - sha="$(find_pr_commit "$pr")" - echo "Applying upstream #$pr from $sha" - - local path patch - for path in "$@"; do - patch="$(mktemp)" - git diff --binary "${sha}^" "$sha" -- "$path" > "$patch" - if [[ ! -s "$patch" ]]; then - echo "Expected $path to change in upstream #$pr, but it did not." >&2 - rm -f "$patch" - exit 11 - fi - if ! git apply --3way --index --whitespace=nowarn "$patch"; then - echo "Failed to three-way apply upstream #$pr path: $path" >&2 - rm -f "$patch" - git status --short >&2 || true - exit 12 - fi - rm -f "$patch" - done - - if ! git diff --cached --quiet; then - git commit -m "chore(upstream): port upstream #$pr" -m "Selective port from agrahn/Android-Password-Store PR #$pr. Fork-owned passkey implementation is intentionally excluded unless explicitly listed." - fi -} - -# Small classic-app fixes and complete recent-password timestamp lifecycle. -apply_pr_paths 936 app/src/main/java/app/passwordstore/util/git/sshj/SshKey.kt - -# #938 overlaps fork-specific password creation work. Apply only its timestamp-cache invariant. -python3 <<'PY' -from pathlib import Path -path = Path("app/src/main/java/app/passwordstore/ui/crypto/PasswordCreationActivity.kt") -text = path.read_text() -old = ''' // associate the new password name with the last name's timestamp in history - val preference = getSharedPreferences("recent_password_history", Context.MODE_PRIVATE) - val oldFilePathHash = "${fullPath.trimEnd('/')}/$suggestedName.gpg".base64() - val timestamp = preference.getString(oldFilePathHash) - if (timestamp != null) { - preference.edit { - remove(oldFilePathHash) - putString(passwordFile.absolutePathString().base64(), timestamp) - } - } -''' -new = ''' // create/update timestamp on the current password file - val preference = getSharedPreferences("recent_password_history", Context.MODE_PRIVATE) - preference.edit { - suggestedName?.let { oldFile -> - val oldFilePathHash = "${fullPath.trimEnd('/')}/$oldFile.gpg".base64() - remove(oldFilePathHash) - } - putString( - passwordFile.absolutePathString().base64(), - System.currentTimeMillis().toString(), - ) - } -''' -if old not in text: - raise SystemExit("Unable to locate fork timestamp block for upstream #938") -path.write_text(text.replace(old, new, 1)) -PY -git add app/src/main/java/app/passwordstore/ui/crypto/PasswordCreationActivity.kt -git commit -m "chore(upstream): adapt upstream #938 timestamp lifecycle" -m "Preserve fork-specific password creation logic while updating recent-password timestamps on every create/edit." - -apply_pr_paths 939 app/src/main/java/app/passwordstore/ui/passwords/PasswordStore.kt -apply_pr_paths 941 app/src/main/java/app/passwordstore/ui/passwords/PasswordStore.kt -apply_pr_paths 942 \ - app/src/main/java/app/passwordstore/ui/settings/RepositorySettings.kt \ - app/src/main/java/app/passwordstore/util/extensions/AndroidExtensions.kt - -# #1000: keep the fork passkey stack out of this port. The classic Autofill call site and -# strings come from upstream; BasePGPActivity is adapted narrowly because the fork changed it. -apply_pr_paths 1000 app/src/main/java/app/passwordstore/ui/autofill/AutofillDecryptActivity.kt -python3 <<'PY' -from pathlib import Path -path = Path("app/src/main/java/app/passwordstore/ui/crypto/BasePGPActivity.kt") -text = path.read_text() -replacements = [ - ( - " protected fun getPersistentAndDecrypt(identifiers: List) {", - " protected fun getPersistentAndDecrypt(identifiers: List, action: String? = null) {", - ), - ( - " verifyPin(pinEncrypted, persistentIds, identifiers)", - " verifyPin(pinEncrypted, persistentIds, identifiers, action)", - ), - ( - " identifiers: List,\n isError: Boolean = false,", - " identifiers: List,\n action: String?,\n isError: Boolean = false,", - ), - ( - " description = resources.getString(R.string.pin_entry_description),", - ''' description = - when (action) { - "autofill" -> resources.getString(R.string.pin_entry_autofill_description) - "passkey" -> resources.getString(R.string.pin_entry_passkey_description) - else -> resources.getString(R.string.pin_entry_description) - },''', - ), - ( - " verifyPin(pinEncryptedUpdate, ids, identifiers, isError = true)", - " verifyPin(pinEncryptedUpdate, ids, identifiers, action, isError = true)", - ), -] -for old, new in replacements: - if old not in text: - raise SystemExit(f"Unable to locate BasePGPActivity #1000 fragment: {old!r}") - text = text.replace(old, new, 1) -path.write_text(text) -PY -git add app/src/main/java/app/passwordstore/ui/crypto/BasePGPActivity.kt -git commit -m "chore(upstream): adapt upstream #1000 fast unlock context" -m "Preserve fork-specific crypto/passkey code while carrying the Autofill/PIN context through BasePGPActivity." -apply_pr_paths 1000 app/src/main/res/values/strings.xml -apply_pr_paths 1000 app/src/main/res/values-de/strings.xml - -apply_pr_paths 1007 app/src/main/java/app/passwordstore/util/shortcuts/ShortcutHandler.kt - -# #1011: isolate fast-unlock PINs by PGP identity. The upstream PR contains a qualifier file -# accidentally named "as UnlockPins.kt"; adopt its content and normalize the filename here. -apply_pr_paths 1011 \ - app/src/main/java/app/passwordstore/Application.kt \ - app/src/main/java/app/passwordstore/injection/prefs/PreferenceModule.kt \ - "app/src/main/java/app/passwordstore/injection/prefs/as UnlockPins.kt" -git mv \ - "app/src/main/java/app/passwordstore/injection/prefs/as UnlockPins.kt" \ - app/src/main/java/app/passwordstore/injection/prefs/UnlockPins.kt -git commit -m "chore(upstream): normalize upstream #1011 qualifier filename" - -# BasePGPActivity has fork-owned authentication/passkey changes, so port #1011 semantically. -python3 <<'PY' -from pathlib import Path - -path = Path("app/src/main/java/app/passwordstore/ui/crypto/BasePGPActivity.kt") -text = path.read_text() - -replacements = [ - ( - "import app.passwordstore.injection.prefs.SettingsPreferences\n", - "import app.passwordstore.injection.prefs.SettingsPreferences\nimport app.passwordstore.injection.prefs.UnlockPins\n", - ), - ( - "import javax.inject.Inject\n", - "import javax.inject.Inject\nimport kotlin.math.max\n", - ), - ( - " @PGPPassphrases @Inject lateinit var persistentPassphrases: SharedPreferences\n\n", - " @PGPPassphrases @Inject lateinit var persistentPassphrases: SharedPreferences\n\n @UnlockPins @Inject lateinit var unlockPins: SharedPreferences\n\n", - ), -] -for old, new in replacements: - if old not in text: - raise SystemExit(f"Unable to locate #1011 BasePGPActivity fragment: {old!r}") - text = text.replace(old, new, 1) - -old_setup = ''' if (persistentPassphrases.getString("unlock_pin", null) == null) { - val pinDialog = - PinDialog.newInstance( - title = resources.getString(R.string.pin_new_entry_title), - description = resources.getString(R.string.pin_new_entry_description), - clearOnDismiss = passphrase, - ) - pinDialog.show(supportFragmentManager, "PIN_DIALOG") - pinDialog.setFragmentResultListener(PinDialog.PIN_RESULT_KEY) { key, bundle -> - if (key == PinDialog.PIN_RESULT_KEY) { - val pin = - requireNotNull(bundle.getCharArray(PinDialog.PIN_KEY)) { - "returned PIN is null" - } - if (pin.size >= 4) { - persistentPassphrases.edit { - putString( - "unlock_pin", // reset and prepend PIN attempt counter - AESEncryption.encrypt( - charArrayOf('0', ':') + pin, - keyType = KeyType.PERSISTENT, - ) - ?.concatToString(), - ) - putString( - id, - AESEncryption.encrypt(passphrase, keyType = KeyType.PERSISTENT) - ?.concatToString(), - ) - putLong( - PreferenceKeys.BIOMETRICS_AND_PIN_LAST_USE, - Instant.now().toEpochMilli(), - ) - } - } - pin.wipe() - } - } -''' -new_setup = ''' if (unlockPins.getString(id, null) == null) { - val pinDialog = - PinDialog.newInstance( - title = resources.getString(R.string.pin_new_entry_title), - description = resources.getString(R.string.pin_new_entry_description), - clearOnDismiss = passphrase, - ) - pinDialog.show(supportFragmentManager, "PIN_DIALOG") - pinDialog.setFragmentResultListener(PinDialog.PIN_RESULT_KEY) { key, bundle -> - if (key == PinDialog.PIN_RESULT_KEY) { - val pin = bundle.getCharArray(PinDialog.PIN_KEY) - if (pin != null && pin.size >= 4) { - unlockPins.edit { - putString( - id, // reset and prepend PIN attempt counter - AESEncryption.encrypt( - charArrayOf('0', ':') + pin, - keyType = KeyType.PERSISTENT, - ) - ?.concatToString(), - ) - } - persistentPassphrases.edit { - putString( - id, - AESEncryption.encrypt(passphrase, keyType = KeyType.PERSISTENT) - ?.concatToString(), - ) - putLong( - PreferenceKeys.BIOMETRICS_AND_PIN_LAST_USE, - Instant.now().toEpochMilli(), - ) - } - } - pin?.wipe() - } - } -''' -if old_setup not in text: - raise SystemExit("Unable to locate global PIN setup block for #1011") -text = text.replace(old_setup, new_setup, 1) - -old_timeout = ''' ) - persistentPassphrases.edit { clear() } - - val persistentIds = - identifiers.map { it.toString() }.filter { persistentPassphrases.contains(it) } - val pinEncrypted = persistentPassphrases.getString("unlock_pin", null)?.toCharArray() -''' -new_timeout = ''' ) { - persistentPassphrases.edit { clear() } - unlockPins.edit { clear() } - } - - val persistentIds = - identifiers.map { it.toString() }.filter { persistentPassphrases.contains(it) } - val encryptedPins = - unlockPins - .getAll() - .filterKeys { persistentIds.contains(it) } - .mapValues { (it.value as String).toCharArray() } -''' -if old_timeout not in text: - raise SystemExit("Unable to locate timeout/global PIN cache block for #1011") -text = text.replace(old_timeout, new_timeout, 1) - -old_pin_branch = ''' } else if ( - !persistentIds.none() && - identifiers.map { it.toString() }.filter { cachedPassphrases.containsKey(it) }.none() && - AESEncryption.isHardwareBacked(KeyType.PERSISTENT) && - settings.getString(PreferenceKeys.PREF_FAST_UNLOCK_OPTION, "disabled") == "PIN" && - pinEncrypted != null - ) { - verifyPin(pinEncrypted, persistentIds, identifiers, action) -''' -new_pin_branch = ''' } else if ( - !encryptedPins.none() && - identifiers.map { it.toString() }.filter { cachedPassphrases.containsKey(it) }.none() && - AESEncryption.isHardwareBacked(KeyType.PERSISTENT) && - settings.getString(PreferenceKeys.PREF_FAST_UNLOCK_OPTION, "disabled") == "PIN" - ) { - verifyPin(encryptedPins, identifiers, action) -''' -if old_pin_branch not in text: - raise SystemExit("Unable to locate global PIN branch for #1011") -text = text.replace(old_pin_branch, new_pin_branch, 1) - -start_marker = " /* Asks for and verifies the user PIN for unlocking a store entry. */\n private fun verifyPin(" -end_marker = "\n protected fun decrypt(identifiers: List, isError: Boolean = false) {" -start = text.find(start_marker) -end = text.find(end_marker, start) -if start < 0 or end < 0: - raise SystemExit("Unable to locate verifyPin function boundaries for #1011") - -new_verify = ''' /* Asks for and verifies the user PIN for unlocking a store entry. */ - private fun verifyPin( - encryptedPins: Map, - identifiers: List, - action: String?, - isError: Boolean = false, - ) { - val pinDialog = - PinDialog.newInstance( - title = resources.getString(R.string.pin_entry_title), - description = - when (action) { - "autofill" -> resources.getString(R.string.pin_entry_autofill_description) - "passkey" -> resources.getString(R.string.pin_entry_passkey_description) - else -> resources.getString(R.string.pin_entry_description) - }, - ) - if (isError) pinDialog.setError() - pinDialog.show(supportFragmentManager, "PIN_DIALOG") - pinDialog.setFragmentResultListener(PinDialog.PIN_RESULT_KEY) { key, bundle -> - if (key == PinDialog.PIN_RESULT_KEY) { - if (bundle.getBoolean(PinDialog.PIN_CANCEL)) { - decrypt(identifiers) - } else { - val pin = requireNotNull(bundle.getCharArray(PinDialog.PIN_KEY)) { "returned PIN is null" } - var pinRetries = 0 - var pinOk = false - - for ((id, encryptedPin) in encryptedPins) { - val cachedPin = - AESEncryption.decrypt(encryptedPin, keyType = KeyType.PERSISTENT)?.let { cached -> - cached.copyOfRange(cached.indexOf(':') + 1, cached.size).also { - pinRetries = - max( - pinRetries, - cached - .copyOfRange(0, cached.indexOf(':')) - .concatToString() - .toIntOrNull() ?: MAX_RETRIES, - ) - cached.wipe() - } - } - pinOk = cachedPin?.let { it.contentEquals(pin) } ?: false - cachedPin?.wipe() - if (pinOk) { - updatePinAttemptCounter(encryptedPins, 0) - persistentPassphrases - .getString(id, null) - ?.toCharArray() - ?.let { passEncrypted -> - AESEncryption.decrypt(passEncrypted, keyType = KeyType.PERSISTENT) - } - ?.let { pass -> - AESEncryption.encrypt(pass)?.let { cachedPassphrases.put(id, it) } - pass.wipe() - } - break - } - } - - pin.wipe() - - if (pinOk) { - decrypt(identifiers) - } else if (++pinRetries < MAX_RETRIES) { - val encryptedPinsUpdated = updatePinAttemptCounter(encryptedPins, pinRetries) - verifyPin(encryptedPinsUpdated, identifiers, action, isError = true) - } else { - // Reset only the relevant identities after the retry budget is exhausted. - encryptedPins.keys.forEach { id -> - cachedPassphrases.remove(id) - persistentPassphrases.edit { remove(id) } - unlockPins.edit { remove(id) } - } - decrypt(identifiers) - } - } - } - } - } - - /** Updates and persists the shared retry counter for the relevant per-PGP-ID PINs. */ - private fun updatePinAttemptCounter( - encryptedPins: Map, - attempts: Int, - ): Map { - val updatedEncryptedPins = mutableMapOf() - unlockPins.edit { - encryptedPins.forEach { (id, encryptedPin) -> - AESEncryption.decrypt(encryptedPin, keyType = KeyType.PERSISTENT) - ?.let { cached -> - cached.copyOfRange(cached.indexOf(':') + 1, cached.size).also { cached.wipe() } - } - ?.let { pin -> - AESEncryption.encrypt( - (attempts.toString() + ":").toCharArray() + pin, - keyType = KeyType.PERSISTENT, - ) - ?.let { updated -> - putString(id, updated.concatToString()) - updatedEncryptedPins[id] = updated - } - pin.wipe() - } - ?: remove(id) - } - } - if (attempts == 0) { - persistentPassphrases.edit { - putLong(PreferenceKeys.BIOMETRICS_AND_PIN_LAST_USE, Instant.now().toEpochMilli()) - } - } - return updatedEncryptedPins - } -''' -text = text[:start] + new_verify + text[end:] -path.write_text(text) -PY -git add app/src/main/java/app/passwordstore/ui/crypto/BasePGPActivity.kt -git commit -m "fix(crypto): isolate fast-unlock PINs per PGP identity" -m "Semantic port of upstream #1011. Preserve fork-owned auth/passkey behavior while ensuring a PIN can unlock only the PGP identity it belongs to." - -# PinDialog also diverged in the fork. Keep clear-on-dismiss wiping while adopting explicit -# cancel results needed by the per-identity PIN flow. -cat > app/src/main/java/app/passwordstore/ui/crypto/PinDialog.kt <<'EOF' -/* - * Copyright © 2014-2026 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ - -package app.passwordstore.ui.crypto - -import android.content.DialogInterface -import android.os.Bundle -import android.view.KeyEvent -import android.view.WindowManager -import androidx.appcompat.app.AlertDialog -import androidx.core.widget.doOnTextChanged -import androidx.fragment.app.DialogFragment -import androidx.fragment.app.setFragmentResult -import app.passwordstore.R -import app.passwordstore.databinding.DialogPinEntryBinding -import app.passwordstore.util.extensions.unsafeLazy -import app.passwordstore.util.extensions.wipe -import com.google.android.material.dialog.MaterialAlertDialogBuilder - -/** [DialogFragment] to request a PIN from the user and forward it along. */ -class PinDialog : DialogFragment() { - - private val binding by unsafeLazy { DialogPinEntryBinding.inflate(layoutInflater) } - private var isError: Boolean = false - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - isCancelable = false - } - - override fun onCreateDialog(savedInstanceState: Bundle?): AlertDialog { - val builder = MaterialAlertDialogBuilder(requireContext()) - builder.setView(binding.root) - - val titleText = requireArguments().getString(TITLE_TEXT_EXTRA) - builder.setTitle(titleText) - - val descriptionText = requireArguments().getString(DESCRIPTION_TEXT_EXTRA) - binding.descriptionText.setText(descriptionText) - - builder.setPositiveButton(android.R.string.ok) { _, _ -> setPinAndDismiss() } - builder.setNegativeButton(android.R.string.cancel) { dialog, _ -> dialog.cancel() } - - val dialog = builder.create() - dialog.setCanceledOnTouchOutside(false) - dialog.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE) - dialog.setOnShowListener { - var pinLength = 0 - if (isError) { - binding.pinField.error = getString(R.string.pin_entry_wrong_input) - } - dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = false - binding.pinEditText.apply { - doOnTextChanged { s, _, _, _ -> - s?.let { - dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = it.length >= 4 - pinLength = it.length - } - binding.pinField.error = null - } - setOnKeyListener { _, keyCode, _ -> - if (keyCode == KeyEvent.KEYCODE_ENTER && pinLength >= 4) { - setPinAndDismiss() - return@setOnKeyListener true - } - keyCode == KeyEvent.KEYCODE_BACK - } - } - } - dialog.window?.setFlags( - WindowManager.LayoutParams.FLAG_SECURE, - WindowManager.LayoutParams.FLAG_SECURE, - ) - return dialog - } - - fun setError() { - isError = true - } - - override fun onDismiss(dialog: DialogInterface) { - requireArguments().getCharArray(CLEAR_ON_DISMISS_TEXT_EXTRA)?.wipe() - binding.pinEditText.text?.clear() - super.onDismiss(dialog) - } - - override fun onCancel(dialog: DialogInterface) { - super.onCancel(dialog) - setFragmentResult(PIN_RESULT_KEY, Bundle().also { it.putBoolean(PIN_CANCEL, true) }) - } - - private fun setPinAndDismiss() { - val pin = binding.pinEditText.text?.let { CharArray(it.length) { i -> it[i] } } - setFragmentResult( - PIN_RESULT_KEY, - Bundle().also { - it.putCharArray(PIN_KEY, pin) - it.putBoolean(PIN_CANCEL, false) - }, - ) - dismissAllowingStateLoss() - } - - companion object { - - private const val TITLE_TEXT_EXTRA = "title_text" - private const val DESCRIPTION_TEXT_EXTRA = "description_text" - private const val CLEAR_ON_DISMISS_TEXT_EXTRA = "clear_chars_text" - - const val PIN_RESULT_KEY = "pin_result" - const val PIN_KEY = "pin" - const val PIN_CANCEL = "cancel" - - fun newInstance( - title: String, - description: String, - clearOnDismiss: CharArray? = null, - ): PinDialog { - val extras = - Bundle().also { - it.apply { - putString(TITLE_TEXT_EXTRA, title) - putString(DESCRIPTION_TEXT_EXTRA, description) - putCharArray(CLEAR_ON_DISMISS_TEXT_EXTRA, clearOnDismiss) - } - } - val fragment = PinDialog() - fragment.arguments = extras - return fragment - } - } -} -EOF -git add app/src/main/java/app/passwordstore/ui/crypto/PinDialog.kt -git commit -m "fix(crypto): adapt upstream #1011 PIN cancellation" -m "Keep fork secret-wiping behavior while returning an explicit cancellation result to the per-PGP-ID fast-unlock flow." - -apply_pr_paths 1011 app/src/main/java/app/passwordstore/ui/settings/PasswordSettings.kt -apply_pr_paths 1011 app/src/main/java/app/passwordstore/util/extensions/AndroidExtensions.kt -apply_pr_paths 1011 app/src/main/java/app/passwordstore/util/settings/Migrations.kt -apply_pr_paths 1011 app/src/test/java/app/passwordstore/util/settings/MigrationsTest.kt - -apply_pr_paths 1014 \ - app/src/main/java/app/passwordstore/ui/crypto/DecryptActivity.kt \ - app/src/main/java/app/passwordstore/ui/settings/PasswordSettings.kt \ - app/src/main/java/app/passwordstore/util/settings/PreferenceKeys.kt \ - app/src/main/res/values/strings.xml \ - app/src/main/res/values-de/strings.xml -apply_pr_paths 1016 app/src/main/java/app/passwordstore/util/autofill/Api30AutofillResponseBuilder.kt -apply_pr_paths 1022 app/src/main/AndroidManifest.xml -apply_pr_paths 1026 autofill-parser/src/main/java/com/github/androidpasswordstore/autofillparser/FeatureAndTrustDetection.kt -apply_pr_paths 1043 app/src/main/java/app/passwordstore/ui/pgp/PGPKeyListActivity.kt - -# Generated data is adopted as one current snapshot rather than replaying update commits. -git checkout "upstream/$UPSTREAM_BRANCH" -- autofill-parser/src/main/assets/publicsuffixes -if ! git diff --cached --quiet; then - git commit -m "chore(autofill): refresh Public Suffix List from upstream" -fi - -# Keep the recurring tools executable in normal clones. -chmod +x scripts/upstream-audit.sh scripts/adopt-upstream-pr.sh scripts/mark-upstream-reviewed.sh - -# One-time bootstrap machinery must not remain in the resulting PR. -rm -f scripts/bootstrap-upstream-sync.sh .github/workflows/bootstrap-upstream-sync.yml - -git add -A -if ! git diff --cached --quiet; then - git commit -m "chore(upstream): finalize recurring reconciliation tooling" -fi - -# Normalize Kotlin/XML formatting after the selective ports. -./gradlew spotlessApply -git add -A -if ! git diff --cached --quiet; then - git commit -m "style: format selective upstream ports" -fi - -git push origin "HEAD:${GITHUB_REF_NAME}" diff --git a/scripts/mark-upstream-reviewed.sh b/scripts/mark-upstream-reviewed.sh old mode 100644 new mode 100755 diff --git a/scripts/upstream-audit.sh b/scripts/upstream-audit.sh old mode 100644 new mode 100755 From c7e0b95e84163e7f59c213bf0a2001e558d6fb63 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 29 Aug 2026 20:39:34 +0000 Subject: [PATCH 39/41] style: format selective upstream ports --- .../app/passwordstore/ui/crypto/BasePGPActivity.kt | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/app/passwordstore/ui/crypto/BasePGPActivity.kt b/app/src/main/java/app/passwordstore/ui/crypto/BasePGPActivity.kt index e92de68b8..ec2977401 100644 --- a/app/src/main/java/app/passwordstore/ui/crypto/BasePGPActivity.kt +++ b/app/src/main/java/app/passwordstore/ui/crypto/BasePGPActivity.kt @@ -720,7 +720,8 @@ open class BasePGPActivity : AppCompatActivity() { if (bundle.getBoolean(PinDialog.PIN_CANCEL)) { decrypt(identifiers) } else { - val pin = requireNotNull(bundle.getCharArray(PinDialog.PIN_KEY)) { "returned PIN is null" } + val pin = + requireNotNull(bundle.getCharArray(PinDialog.PIN_KEY)) { "returned PIN is null" } var pinRetries = 0 var pinOk = false @@ -731,10 +732,8 @@ open class BasePGPActivity : AppCompatActivity() { pinRetries = max( pinRetries, - cached - .copyOfRange(0, cached.indexOf(':')) - .concatToString() - .toIntOrNull() ?: MAX_RETRIES, + cached.copyOfRange(0, cached.indexOf(':')).concatToString().toIntOrNull() + ?: MAX_RETRIES, ) cached.wipe() } @@ -800,8 +799,7 @@ open class BasePGPActivity : AppCompatActivity() { updatedEncryptedPins[id] = updated } pin.wipe() - } - ?: remove(id) + } ?: remove(id) } } if (attempts == 0) { From 3e58ed28b2a38b7463138f7e97da09ae4b298adc Mon Sep 17 00:00:00 2001 From: Alexander Gil Casas Date: Sat, 29 Aug 2026 22:43:35 +0200 Subject: [PATCH 40/41] fix(upstream): qualify unlock PIN preference mode --- .../java/app/passwordstore/util/extensions/AndroidExtensions.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/app/passwordstore/util/extensions/AndroidExtensions.kt b/app/src/main/java/app/passwordstore/util/extensions/AndroidExtensions.kt index f02725340..bd4e4cc5f 100644 --- a/app/src/main/java/app/passwordstore/util/extensions/AndroidExtensions.kt +++ b/app/src/main/java/app/passwordstore/util/extensions/AndroidExtensions.kt @@ -55,7 +55,7 @@ val Context.persistentPassphrases: SharedPreferences /** Get the persistent unlock PINs [SharedPreferences] instance */ val Context.unlockPins: SharedPreferences - get() = getSharedPreferences("${BuildConfig.APPLICATION_ID}_unlock_pins", MODE_PRIVATE) + get() = getSharedPreferences("${BuildConfig.APPLICATION_ID}_unlock_pins", Context.MODE_PRIVATE) /** Get the persistent Git server secrets [SharedPreferences] instance */ val Context.gitSecrets: SharedPreferences From 52601bf2ab17c7a069a81511e7d32e30cd16c359 Mon Sep 17 00:00:00 2001 From: "forkline-dev[bot]" Date: Sat, 29 Aug 2026 20:52:23 +0000 Subject: [PATCH 41/41] Fix unresolved MODE_PRIVATE reference in Android SDK 37 Replace Context.MODE_PRIVATE with 0 (its actual value) across all files. MODE_PRIVATE constant was removed in Android SDK 37 (Android 16). --- .../passwordstore/injection/prefs/PreferenceModule.kt | 9 ++++----- .../passwordstore/ui/crypto/PasswordCreationActivity.kt | 3 +-- .../dialogs/DicewarePasswordGeneratorDialogFragment.kt | 3 +-- .../ui/dialogs/PasswordGeneratorDialogFragment.kt | 4 ++-- .../app/passwordstore/ui/passwords/PasswordFragment.kt | 3 +-- .../java/app/passwordstore/ui/passwords/PasswordStore.kt | 8 +++----- .../app/passwordstore/util/autofill/AutofillMatcher.kt | 4 ++-- .../passwordstore/util/extensions/AndroidExtensions.kt | 2 +- .../java/app/passwordstore/util/settings/Migrations.kt | 5 ++--- .../app/passwordstore/util/settings/PasswordSortOrder.kt | 4 +--- 10 files changed, 18 insertions(+), 27 deletions(-) diff --git a/app/src/main/java/app/passwordstore/injection/prefs/PreferenceModule.kt b/app/src/main/java/app/passwordstore/injection/prefs/PreferenceModule.kt index 1a1e7dcba..ba0ab2e86 100644 --- a/app/src/main/java/app/passwordstore/injection/prefs/PreferenceModule.kt +++ b/app/src/main/java/app/passwordstore/injection/prefs/PreferenceModule.kt @@ -6,7 +6,6 @@ package app.passwordstore.injection.prefs import android.content.Context -import android.content.Context.MODE_PRIVATE import android.content.SharedPreferences import app.passwordstore.BuildConfig import dagger.Module @@ -34,21 +33,21 @@ class PreferenceModule { @[Provides SettingsPreferences Reusable] fun provideSettingsPreferences(@ApplicationContext context: Context): SharedPreferences { - return context.getSharedPreferences("${BuildConfig.APPLICATION_ID}_preferences", MODE_PRIVATE) + return context.getSharedPreferences("${BuildConfig.APPLICATION_ID}_preferences", 0) } @[Provides PGPPassphrases Reusable] fun providePGPPassphrases(@ApplicationContext context: Context): SharedPreferences { - return context.getSharedPreferences("${BuildConfig.APPLICATION_ID}_passphrases", MODE_PRIVATE) + return context.getSharedPreferences("${BuildConfig.APPLICATION_ID}_passphrases", 0) } @[Provides UnlockPins Reusable] fun provideUnlockPins(@ApplicationContext context: Context): SharedPreferences { - return context.getSharedPreferences("${BuildConfig.APPLICATION_ID}_unlock_pins", MODE_PRIVATE) + return context.getSharedPreferences("${BuildConfig.APPLICATION_ID}_unlock_pins", 0) } @[Provides GitSecrets Reusable] fun provideGitSecrets(@ApplicationContext context: Context): SharedPreferences { - return context.getSharedPreferences("${BuildConfig.APPLICATION_ID}_git_secrets", MODE_PRIVATE) + return context.getSharedPreferences("${BuildConfig.APPLICATION_ID}_git_secrets", 0) } } diff --git a/app/src/main/java/app/passwordstore/ui/crypto/PasswordCreationActivity.kt b/app/src/main/java/app/passwordstore/ui/crypto/PasswordCreationActivity.kt index f6c8bffbc..eed4e0d72 100644 --- a/app/src/main/java/app/passwordstore/ui/crypto/PasswordCreationActivity.kt +++ b/app/src/main/java/app/passwordstore/ui/crypto/PasswordCreationActivity.kt @@ -5,7 +5,6 @@ package app.passwordstore.ui.crypto -import android.content.Context import android.content.Intent import android.content.pm.PackageManager import android.graphics.Bitmap @@ -497,7 +496,7 @@ class PasswordCreationActivity : BasePGPActivity() { } // create/update timestamp on the current password file - val preference = getSharedPreferences("recent_password_history", Context.MODE_PRIVATE) + val preference = getSharedPreferences("recent_password_history", 0) preference.edit { suggestedName?.let { oldFile -> val oldFilePathHash = "${fullPath.trimEnd('/')}/$oldFile.gpg".base64() diff --git a/app/src/main/java/app/passwordstore/ui/dialogs/DicewarePasswordGeneratorDialogFragment.kt b/app/src/main/java/app/passwordstore/ui/dialogs/DicewarePasswordGeneratorDialogFragment.kt index dbdee107d..ddc301e94 100644 --- a/app/src/main/java/app/passwordstore/ui/dialogs/DicewarePasswordGeneratorDialogFragment.kt +++ b/app/src/main/java/app/passwordstore/ui/dialogs/DicewarePasswordGeneratorDialogFragment.kt @@ -7,7 +7,6 @@ package app.passwordstore.ui.dialogs import android.app.AlertDialog import android.app.Dialog -import android.content.Context import android.content.SharedPreferences import android.graphics.Typeface import android.os.Bundle @@ -42,7 +41,7 @@ class DicewarePasswordGeneratorDialogFragment : DialogFragment() { lateinit var prefs: SharedPreferences override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - prefs = requireContext().getSharedPreferences("PasswordGenerator", Context.MODE_PRIVATE) + prefs = requireContext().getSharedPreferences("PasswordGenerator", 0) val builder = MaterialAlertDialogBuilder(requireContext()) val binding = FragmentPwgenDicewareBinding.inflate(layoutInflater) diff --git a/app/src/main/java/app/passwordstore/ui/dialogs/PasswordGeneratorDialogFragment.kt b/app/src/main/java/app/passwordstore/ui/dialogs/PasswordGeneratorDialogFragment.kt index 53a934526..9fa240275 100644 --- a/app/src/main/java/app/passwordstore/ui/dialogs/PasswordGeneratorDialogFragment.kt +++ b/app/src/main/java/app/passwordstore/ui/dialogs/PasswordGeneratorDialogFragment.kt @@ -39,7 +39,7 @@ import reactivecircus.flowbinding.android.widget.checkedChanges class PasswordGeneratorDialogFragment : DialogFragment() { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - val prefs = requireContext().getSharedPreferences("PasswordGenerator", Context.MODE_PRIVATE) + val prefs = requireContext().getSharedPreferences("PasswordGenerator", 0) val builder = MaterialAlertDialogBuilder(requireContext()) val binding = FragmentPwgenBinding.inflate(layoutInflater) @@ -139,7 +139,7 @@ class PasswordGeneratorDialogFragment : DialogFragment() { * passwords. */ private fun setPrefs(ctx: Context, options: List, targetLength: Int): Boolean { - ctx.getSharedPreferences("PasswordGenerator", Context.MODE_PRIVATE).edit { + ctx.getSharedPreferences("PasswordGenerator", 0).edit { for (possibleOption in PasswordOption.entries) { putBoolean(possibleOption.key, possibleOption in options) } diff --git a/app/src/main/java/app/passwordstore/ui/passwords/PasswordFragment.kt b/app/src/main/java/app/passwordstore/ui/passwords/PasswordFragment.kt index dfeb43a69..8f5fb6dbd 100644 --- a/app/src/main/java/app/passwordstore/ui/passwords/PasswordFragment.kt +++ b/app/src/main/java/app/passwordstore/ui/passwords/PasswordFragment.kt @@ -371,8 +371,7 @@ class PasswordFragment : Fragment(R.layout.password_recycler_view) { settings.getString(PreferenceKeys.SORT_ORDER) == PasswordSortOrder.RECENTLY_USED.name ) { // save the time when password was used - val preferences = - context.getSharedPreferences("recent_password_history", Context.MODE_PRIVATE) + val preferences = context.getSharedPreferences("recent_password_history", 0) preferences.edit { putString(item.file.absolutePath.base64(), System.currentTimeMillis().toString()) } 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 5bbb9f67e..067d7a581 100644 --- a/app/src/main/java/app/passwordstore/ui/passwords/PasswordStore.kt +++ b/app/src/main/java/app/passwordstore/ui/passwords/PasswordStore.kt @@ -5,7 +5,6 @@ package app.passwordstore.ui.passwords import android.content.ComponentName -import android.content.Context import android.content.Intent import android.os.Bundle import android.view.KeyEvent @@ -482,7 +481,7 @@ class PasswordStore : BaseGitActivity() { else filesToDelete.add(item.file) } // remove to-be-deleted files from history - val preference = getSharedPreferences("recent_password_history", Context.MODE_PRIVATE) + val preference = getSharedPreferences("recent_password_history", 0) preference.edit { filesToDelete.forEach { file -> remove(file.absolutePath.base64()) @@ -564,8 +563,7 @@ class PasswordStore : BaseGitActivity() { // associate the new category with the last category's timestamp in // history - val preference = - getSharedPreferences("recent_password_history", Context.MODE_PRIVATE) + val preference = getSharedPreferences("recent_password_history", 0) val timestamp = preference.getString(oldCategory.file.absolutePath.base64()) if (timestamp != null) { preference.edit { @@ -665,7 +663,7 @@ class PasswordStore : BaseGitActivity() { } } else { // update timestamp cache with the new file locations - val preference = getSharedPreferences("recent_password_history", Context.MODE_PRIVATE) + val preference = getSharedPreferences("recent_password_history", 0) preference.edit { sourceDestinationMap.forEach { (src, dest) -> val srcPathHash = src.absolutePath.base64() diff --git a/app/src/main/java/app/passwordstore/util/autofill/AutofillMatcher.kt b/app/src/main/java/app/passwordstore/util/autofill/AutofillMatcher.kt index 79b0963ef..196de6e5b 100644 --- a/app/src/main/java/app/passwordstore/util/autofill/AutofillMatcher.kt +++ b/app/src/main/java/app/passwordstore/util/autofill/AutofillMatcher.kt @@ -21,11 +21,11 @@ import logcat.logcat private const val PREFERENCES_AUTOFILL_APP_MATCHES = "oreo_autofill_app_matches" private val Context.autofillAppMatches - get() = getSharedPreferences(PREFERENCES_AUTOFILL_APP_MATCHES, Context.MODE_PRIVATE) + get() = getSharedPreferences(PREFERENCES_AUTOFILL_APP_MATCHES, 0) private const val PREFERENCES_AUTOFILL_WEB_MATCHES = "oreo_autofill_web_matches" private val Context.autofillWebMatches - get() = getSharedPreferences(PREFERENCES_AUTOFILL_WEB_MATCHES, Context.MODE_PRIVATE) + get() = getSharedPreferences(PREFERENCES_AUTOFILL_WEB_MATCHES, 0) private fun Context.matchPreferences(formOrigin: FormOrigin): SharedPreferences { return when (formOrigin) { diff --git a/app/src/main/java/app/passwordstore/util/extensions/AndroidExtensions.kt b/app/src/main/java/app/passwordstore/util/extensions/AndroidExtensions.kt index bd4e4cc5f..d34720918 100644 --- a/app/src/main/java/app/passwordstore/util/extensions/AndroidExtensions.kt +++ b/app/src/main/java/app/passwordstore/util/extensions/AndroidExtensions.kt @@ -55,7 +55,7 @@ val Context.persistentPassphrases: SharedPreferences /** Get the persistent unlock PINs [SharedPreferences] instance */ val Context.unlockPins: SharedPreferences - get() = getSharedPreferences("${BuildConfig.APPLICATION_ID}_unlock_pins", Context.MODE_PRIVATE) + get() = getSharedPreferences("${BuildConfig.APPLICATION_ID}_unlock_pins", 0) /** Get the persistent Git server secrets [SharedPreferences] instance */ val Context.gitSecrets: SharedPreferences 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 b739f498e..9dc4d6bc4 100644 --- a/app/src/main/java/app/passwordstore/util/settings/Migrations.kt +++ b/app/src/main/java/app/passwordstore/util/settings/Migrations.kt @@ -62,7 +62,7 @@ private fun deleteKeystoreWrappedEd25519Key(sharedPrefs: SharedPreferences, cont androidKeystore.deleteEntry(KEYSTORE_ALIAS) val ANDROIDX_SECURITY_KEYSET_PREF_NAME = "androidx_sshkey_keyset_prefs" - context.getSharedPreferences(ANDROIDX_SECURITY_KEYSET_PREF_NAME, Context.MODE_PRIVATE).edit { + context.getSharedPreferences(ANDROIDX_SECURITY_KEYSET_PREF_NAME, 0).edit { clear() } @@ -133,8 +133,7 @@ private fun moveToPasswordGeneratorPrefs(sharedPrefs: SharedPreferences, context // Old, encrypted preferences val pwgenPrefs = createEncryptedPreferences(context, "pwgen_preferences") // New destination - val passwordGeneratorPrefs = - context.getSharedPreferences("PasswordGenerator", Context.MODE_PRIVATE) + val passwordGeneratorPrefs = context.getSharedPreferences("PasswordGenerator", 0) val separator = pwgenPrefs.getString(PreferenceKeys.DICEWARE_SEPARATOR, null) diff --git a/app/src/main/java/app/passwordstore/util/settings/PasswordSortOrder.kt b/app/src/main/java/app/passwordstore/util/settings/PasswordSortOrder.kt index 3c5f0b7be..2baa605ea 100644 --- a/app/src/main/java/app/passwordstore/util/settings/PasswordSortOrder.kt +++ b/app/src/main/java/app/passwordstore/util/settings/PasswordSortOrder.kt @@ -5,7 +5,6 @@ package app.passwordstore.util.settings -import android.content.Context import android.content.SharedPreferences import app.passwordstore.Application import app.passwordstore.data.password.PasswordItem @@ -25,8 +24,7 @@ enum class PasswordSortOrder(val comparator: java.util.Comparator) ), RECENTLY_USED( Comparator { p1: PasswordItem, p2: PasswordItem -> - val recentHistory = - Application.instance.getSharedPreferences("recent_password_history", Context.MODE_PRIVATE) + val recentHistory = Application.instance.getSharedPreferences("recent_password_history", 0) val timeP1 = recentHistory.getString(p1.file.absolutePath.base64()) val timeP2 = recentHistory.getString(p2.file.absolutePath.base64()) when {