diff options
| author | Danilo M. <danix@danix.xyz> | 2026-11-02 12:59:30 +0100 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-11-02 12:59:30 +0100 |
| commit | 5972d8bb6ab2369e298be0e6641d92ab8f1b2f7a (patch) | |
| tree | 5a3b5fe9dfaf9d88965dfb23b857d1f7fef087eb | |
| parent | d5df91b3c63ca0445035c957c2bb07dc156ec2a8 (diff) | |
| download | HardenedPass-5972d8bb6ab2369e298be0e6641d92ab8f1b2f7a.tar.gz HardenedPass-5972d8bb6ab2369e298be0e6641d92ab8f1b2f7a.zip | |
build(deps): update JGit from 4.5.7 to 5.13.5
JGit 4.5.7 dates from 2019. 5.13 is the last line that needs only Java 8
APIs, so it works with Android's desugaring; it also fixes
GHSA-3p86-9955-h393 (arbitrary file overwrite on checkout).
GHSA-vrpq-qp53-qv56 (XXE in repo manifest parsing) has no 5.x fix but
the app never parses repo manifests.
API changes:
- SshSessionFactory now requires getType().
- CheckoutCommand.setForce is deprecated; setForceRefUpdate is its
documented replacement with the same meaning.
JGit 5 references java.lang.management and javax.management, which
Android lacks; R8 flagged them. Both paths are made unreachable rather
than only silenced:
- WindowCache registers JMX statistics by default; Application.onCreate
now installs a WindowCacheConfig with exposeStatsViaJmx = false.
- GC.gc() takes a PID lock that queries ManagementFactory and would
throw NoClassDefFoundError on a device. GcOperation now runs the GC
steps (packRefs, repack, prune) directly; the lock only guards against
concurrent gc processes, which cannot happen in the app. Aggressive
packing is dropped.
The R8 -dontwarn rules and the lint baseline entries are added only for
those two now-unreachable references.
JGitBehaviourTest covers the operations' command chains plus the GC
steps and passes on 5.13.5. It now ignores the developer's global git
config, which JGit 5 reads (commit.gpgsign made test commits try to
sign).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
8 files changed, 68 insertions, 7 deletions
diff --git a/app/lint-baseline.xml b/app/lint-baseline.xml index 8feed974..d9be2a91 100644 --- a/app/lint-baseline.xml +++ b/app/lint-baseline.xml @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="UTF-8"?> -<issues format="6" by="lint 8.6.0" type="baseline" client="gradle" dependencies="false" name="AGP (8.6.0)" variant="all" version="8.6.0"> +<issues format="6" by="lint 9.5.0" type="baseline" client="gradle" dependencies="false" name="AGP (9.5.0)" variant="all" version="9.5.0"> <issue id="InvalidPackage" @@ -17,9 +17,16 @@ <issue id="InvalidPackage" - message="Invalid package reference in org.eclipse.jgit:org.eclipse.jgit; not included in Android: `javax.xml.bind`. Referenced from `org.eclipse.jgit.transport.WalkEncryption.SymmetricEncryption`."> + message="Invalid package reference in org.eclipse.jgit:org.eclipse.jgit; not included in Android: `java.lang.management`. Referenced from `org.eclipse.jgit.internal.storage.file.GC.PidLock`."> <location - file="$GRADLE_USER_HOME/caches/modules-2/files-2.1/org.eclipse.jgit/org.eclipse.jgit/4.5.7.201904151645-r/e84008db495a7f48d65fb09afe53279b0df68c80/org.eclipse.jgit-4.5.7.201904151645-r.jar"/> + file="$GRADLE_USER_HOME/caches/modules-2/files-2.1/org.eclipse.jgit/org.eclipse.jgit/5.13.5.202508271544-r/166bc880beb891b201cd3216407fd4ffcd4930fd/org.eclipse.jgit-5.13.5.202508271544-r.jar"/> + </issue> + + <issue + id="InvalidPackage" + message="Invalid package reference in org.eclipse.jgit:org.eclipse.jgit; not included in Android: `javax.management`. Referenced from `org.eclipse.jgit.util.Monitoring`."> + <location + file="$GRADLE_USER_HOME/caches/modules-2/files-2.1/org.eclipse.jgit/org.eclipse.jgit/5.13.5.202508271544-r/166bc880beb891b201cd3216407fd4ffcd4930fd/org.eclipse.jgit-5.13.5.202508271544-r.jar"/> </issue> <issue diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 25887cac..6f354039 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -4,3 +4,9 @@ -keep class org.eclipse.jgit.internal.JGitText { *; } -keep class org.bouncycastle.jcajce.provider.** { *; } -keep class !org.bouncycastle.jce.provider.X509LDAPCertStoreSpi,org.bouncycastle.jce.provider.** { *; } + +# JGit 5 references JMX (java.lang.management, javax.management), absent on Android. Both +# paths are unreachable: WindowCache JMX stats are disabled in Application.onCreate and +# GcOperation runs the GC steps without the PID lock that queries ManagementFactory. +-dontwarn java.lang.management.** +-dontwarn javax.management.** diff --git a/app/src/main/java/app/passwordstore/Application.kt b/app/src/main/java/app/passwordstore/Application.kt index 794dea10..9bc4c83e 100644 --- a/app/src/main/java/app/passwordstore/Application.kt +++ b/app/src/main/java/app/passwordstore/Application.kt @@ -35,6 +35,7 @@ import logcat.LogPriority.DEBUG import logcat.LogPriority.VERBOSE import logcat.LogcatLogger import logcat.logcat +import org.eclipse.jgit.storage.file.WindowCacheConfig @Suppress("Unused") @HiltAndroidApp @@ -51,6 +52,8 @@ class Application : android.app.Application(), SharedPreferences.OnSharedPrefere override fun onCreate() { super.onCreate() instance = this + // JGit registers its pack cache statistics with JMX by default; Android has no JMX. + WindowCacheConfig().apply { exposeStatsViaJmx = false }.install() if ( BuildConfig.ENABLE_DEBUG_FEATURES || prefs.getBoolean(PreferenceKeys.ENABLE_DEBUG_LOGGING, false) diff --git a/app/src/main/java/app/passwordstore/util/git/operation/GcOperation.kt b/app/src/main/java/app/passwordstore/util/git/operation/GcOperation.kt index cf6ab1c4..c1487fbf 100644 --- a/app/src/main/java/app/passwordstore/util/git/operation/GcOperation.kt +++ b/app/src/main/java/app/passwordstore/util/git/operation/GcOperation.kt @@ -7,6 +7,8 @@ package app.passwordstore.util.git.operation import androidx.appcompat.app.AppCompatActivity import org.eclipse.jgit.api.GitCommand +import org.eclipse.jgit.internal.storage.file.FileRepository +import org.eclipse.jgit.internal.storage.file.GC /** * Run an aggressive garbage collection job on the repository, expiring every loose object to @@ -16,5 +18,20 @@ class GcOperation(callingActivity: AppCompatActivity) : GitOperation(callingActi override val requiresAuth: Boolean = false override val commands: Array<GitCommand<out Any>> = - arrayOf(git.gc().setAggressive(true).setExpire(null)) + arrayOf( + object : GitCommand<Unit>(repository) { + // ponytail: JGit's GC.gc() takes a PID lock that calls java.lang.management, which + // Android lacks, so run its steps directly. The lock only guards against concurrent gc + // processes on one repository, which cannot happen here. No aggressive packing either: + // too slow on a phone for a password store's size. + override fun call() { + GC(repository as FileRepository).run { + setExpire(null) + packRefs() + repack() + prune(emptySet()) + } + } + } + ) } diff --git a/app/src/main/java/app/passwordstore/util/git/operation/ResetToRemoteOperation.kt b/app/src/main/java/app/passwordstore/util/git/operation/ResetToRemoteOperation.kt index 17d3d3a1..7ca1282b 100644 --- a/app/src/main/java/app/passwordstore/util/git/operation/ResetToRemoteOperation.kt +++ b/app/src/main/java/app/passwordstore/util/git/operation/ResetToRemoteOperation.kt @@ -19,7 +19,7 @@ class ResetToRemoteOperation(callingActivity: AppCompatActivity, remoteBranch: S // Force-create $remoteBranch if it doesn't exist. This covers the case where a branch name is // changed. git.branchCreate().setName(remoteBranch).setForce(true), - git.checkout().setName(remoteBranch).setForce(true).setUpstreamMode(TRACK), + git.checkout().setName(remoteBranch).setForceRefUpdate(true).setUpstreamMode(TRACK), // Do a hard reset to the remote branch. Equivalent to git reset --hard // origin/$remoteBranch git.reset().setRef("origin/$remoteBranch").setMode(ResetCommand.ResetType.HARD), diff --git a/app/src/main/java/app/passwordstore/util/git/sshj/SshjSessionFactory.kt b/app/src/main/java/app/passwordstore/util/git/sshj/SshjSessionFactory.kt index 325ed1ad..51657b02 100644 --- a/app/src/main/java/app/passwordstore/util/git/sshj/SshjSessionFactory.kt +++ b/app/src/main/java/app/passwordstore/util/git/sshj/SshjSessionFactory.kt @@ -75,6 +75,8 @@ class SshjSessionFactory( private var currentSession: SshjSession? = null + override fun getType() = "sshj" + override fun getSession( uri: URIish, credentialsProvider: CredentialsProvider?, diff --git a/app/src/test/java/app/passwordstore/util/git/JGitBehaviourTest.kt b/app/src/test/java/app/passwordstore/util/git/JGitBehaviourTest.kt index 43f8b66a..b0eeb4ea 100644 --- a/app/src/test/java/app/passwordstore/util/git/JGitBehaviourTest.kt +++ b/app/src/test/java/app/passwordstore/util/git/JGitBehaviourTest.kt @@ -17,9 +17,12 @@ import org.eclipse.jgit.api.CreateBranchCommand.SetupUpstreamMode.TRACK import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.RebaseCommand import org.eclipse.jgit.api.ResetCommand +import org.eclipse.jgit.internal.storage.file.FileRepository +import org.eclipse.jgit.internal.storage.file.GC import org.eclipse.jgit.lib.PersonIdent import org.eclipse.jgit.lib.RepositoryState import org.eclipse.jgit.transport.RemoteRefUpdate +import org.eclipse.jgit.util.FS /** * Runs the JGit command chains the git operations in `util/git/operation` use, against a local bare @@ -33,6 +36,8 @@ class JGitBehaviourTest { private val identity = PersonIdent("test", "test@example.org") init { + // Ignore the developer's ~/.gitconfig (e.g. commit.gpgsign); the app has no global config. + FS.DETECTED.setUserHome(root) Git.init().setBare(true).setDirectory(remote).call().close() clone("seed").use { git -> git.write("example.org.gpg", "initial") @@ -147,7 +152,7 @@ class JGitBehaviourTest { // ResetToRemoteOperation b.fetch().setRemote("origin").setRemoveDeletedRefs(true).call() b.branchCreate().setName("master").setForce(true).call() - b.checkout().setName("master").setForce(true).setUpstreamMode(TRACK).call() + b.checkout().setName("master").setForceRefUpdate(true).setUpstreamMode(TRACK).call() b.reset().setRef("origin/master").setMode(ResetCommand.ResetType.HARD).call() assertEquals("edited by a", b.read("example.org.gpg")) @@ -155,4 +160,25 @@ class JGitBehaviourTest { a.close() b.close() } + + @Test + fun gcStepsPackObjectsWithoutThePidLock() { + clone("a").use { a -> + repeat(3) { + a.write("e$it.gpg", "entry $it") + a.commitAll("e$it") + } + val repo = a.repository as FileRepository + // GcOperation + GC(repo).run { + setExpire(null) + packRefs() + repack() + prune(emptySet()) + } + assertEquals(0, GC(repo).statistics.numberOfLooseObjects) + assertTrue(GC(repo).statistics.numberOfPackedObjects > 0) + assertEquals("entry 2", a.read("e2.gpg")) + } + } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e7d761c8..ca3ecaa5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -79,7 +79,7 @@ thirdparty-fastscroll = "me.zhanghai.android.fastscroll:library:1.3.0" thirdparty-flowbinding-android = { module = "io.github.reactivecircus.flowbinding:flowbinding-android", version.ref = "flowbinding" } # JGit upgrades also raise its minimum Java runtime requirements that we cannot satisfy on Android # noinspection GradleDependency -thirdparty-jgit = "org.eclipse.jgit:org.eclipse.jgit:4.5.7.201904151645-r" +thirdparty-jgit = "org.eclipse.jgit:org.eclipse.jgit:5.13.5.202508271544-r" thirdparty-kotlinResult = { module = "com.michael-bull.kotlin-result:kotlin-result", version.ref = "kotlinResult" } thirdparty-kotlinResult-coroutines = { module = "com.michael-bull.kotlin-result:kotlin-result-coroutines", version.ref = "kotlinResult" } thirdparty-leakcanary-plumber = "com.squareup.leakcanary:plumber-android-startup:2.14" |
