aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--AGENTS.md6
-rw-r--r--CHANGELOG.md7
-rw-r--r--app/src/main/java/app/passwordstore/util/git/sshj/SshjConfig.kt18
-rw-r--r--app/src/test/java/app/passwordstore/util/git/sshj/SshjConfigTest.kt14
-rw-r--r--app/version.properties4
5 files changed, 46 insertions, 3 deletions
diff --git a/AGENTS.md b/AGENTS.md
index 6708cd53..503fd065 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -117,6 +117,10 @@ passed.
and hash included. A dependency bump that moves a baselined jar needs the path updated.
- `LaunchActivity` is exported and forwards `FILE_PATH` to `DecryptActivity` (not exported)
only after `isFileInRepo` checks it canonically resolves inside the store.
+- sshj gets Ed25519 keys from Bouncy Castle: `SshjConfig.kt` pins `KeyFactory.Ed25519` to it at
+ provider position 1. Conscrypt from Android 14 mainline updates serves that factory with keys
+ sshj maps to `KeyType.UNKNOWN` ("Don't know how to encode key"), breaking every Ed25519 host
+ and user key. The emulator image does not show it; a 2026-03 Android 14 tablet does.
- AGP 9 applies packaging excludes (`AndroidCommon`, e.g. `**/*.txt`) to unit-test java
resources as well. A module whose tests load such a file must remove the exclude locally
(see `passgen/diceware/build.gradle.kts`).
@@ -147,7 +151,7 @@ manual because bot commits on the GitHub mirror would diverge from the main repo
./gradlew :autofill-parser:test -PslimTests
git commit -am "autofill-parser: update public suffix list" && git push
-Last run: 2026-11-02.
+Last run: 2026-11-03.
## Releases
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 206ed993..8acf69c2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,13 @@
All notable changes to this project will be documented in this file.
+## [0.1.5] - 2026-11-03
+
+### Fixed
+
+- Clone and sync over SSH failed with "Don't know how to encode key" for servers with an Ed25519 host key (and with Ed25519 SSH keys) on Android 14 devices whose system crypto library was updated; Ed25519 keys now always come from Bouncy Castle
+- A typed entry or folder name such as `../store-evil/x` could pass the "inside the store" check, which compared paths as strings; it now compares whole path components
+
## [0.1.4] - 2026-11-02
### Changed
diff --git a/app/src/main/java/app/passwordstore/util/git/sshj/SshjConfig.kt b/app/src/main/java/app/passwordstore/util/git/sshj/SshjConfig.kt
index b43d0007..3a3538ac 100644
--- a/app/src/main/java/app/passwordstore/util/git/sshj/SshjConfig.kt
+++ b/app/src/main/java/app/passwordstore/util/git/sshj/SshjConfig.kt
@@ -12,6 +12,7 @@ import com.hierynomus.sshj.transport.cipher.GcmCiphers
import com.hierynomus.sshj.transport.kex.ExtInfoClientFactory
import com.hierynomus.sshj.transport.mac.Macs
import com.hierynomus.sshj.userauth.keyprovider.OpenSSHKeyV1KeyFile
+import java.security.Provider
import java.security.Security
import logcat.LogPriority.VERBOSE
import logcat.logcat
@@ -32,6 +33,18 @@ import net.schmizz.sshj.userauth.keyprovider.PuTTYKeyFile
import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.slf4j.Logger
+private object Ed25519KeyFactoryProvider :
+ Provider("HardenedPassEd25519", 1.0, "Ed25519 KeyFactory from Bouncy Castle") {
+ private fun readResolve(): Any = Ed25519KeyFactoryProvider
+
+ init {
+ put(
+ "KeyFactory.Ed25519",
+ "org.bouncycastle.jcajce.provider.asymmetric.edec.KeyFactorySpi\$Ed25519",
+ )
+ }
+}
+
fun setUpBouncyCastleForSshj() {
// Replace the Android BC provider with the Java BouncyCastle provider since the former does
// not include all the required algorithms.
@@ -48,6 +61,11 @@ fun setUpBouncyCastleForSshj() {
runCatching { Class.forName("sun.security.jca.Providers") }
Security.insertProviderAt(BouncyCastleProvider(), bcIndex + 1)
}
+ // Recent Conscrypt (Android 14 mainline updates) serves KeyFactory "Ed25519" with keys whose
+ // algorithm name sshj does not map to ssh-ed25519 (KeyType.UNKNOWN, "Don't know how to encode
+ // key"), which breaks every Ed25519 host and user key. Pin only that factory to Bouncy Castle,
+ // ahead of every other provider; signatures still pick a provider per key.
+ Security.insertProviderAt(Ed25519KeyFactoryProvider, 1)
logcat("setUpBouncyCastleForSshj", priority = VERBOSE) {
"JCE providers: ${Security.getProviders().joinToString { "${it.name} (${it.version})" }}"
}
diff --git a/app/src/test/java/app/passwordstore/util/git/sshj/SshjConfigTest.kt b/app/src/test/java/app/passwordstore/util/git/sshj/SshjConfigTest.kt
index af6eee47..e4bc3eec 100644
--- a/app/src/test/java/app/passwordstore/util/git/sshj/SshjConfigTest.kt
+++ b/app/src/test/java/app/passwordstore/util/git/sshj/SshjConfigTest.kt
@@ -6,7 +6,11 @@
package app.passwordstore.util.git.sshj
import kotlin.test.Test
+import kotlin.test.assertEquals
import kotlin.test.assertNull
+import kotlin.test.assertTrue
+import net.schmizz.sshj.common.Ed25519KeyFactory
+import net.schmizz.sshj.common.KeyType
import net.schmizz.sshj.common.SecurityUtils
class SshjConfigTest {
@@ -19,4 +23,14 @@ class SshjConfigTest {
setUpBouncyCastleForSshj()
assertNull(SecurityUtils.getSecurityProvider())
}
+
+ // Conscrypt's Ed25519 keys map to KeyType.UNKNOWN in sshj, so Ed25519 keys must come from
+ // Bouncy Castle whatever provider the platform puts first.
+ @Test
+ fun ed25519KeysComeFromBouncyCastle() {
+ setUpBouncyCastleForSshj()
+ val key = Ed25519KeyFactory.getPublicKey(ByteArray(32) { 1 })
+ assertTrue(key.javaClass.name.startsWith("org.bouncycastle."), key.javaClass.name)
+ assertEquals(KeyType.ED25519, KeyType.fromKey(key))
+ }
}
diff --git a/app/version.properties b/app/version.properties
index 276eab40..417e286c 100644
--- a/app/version.properties
+++ b/app/version.properties
@@ -6,5 +6,5 @@
#
# This file was automatically generated by 'versioning-plugin'. DO NOT EDIT MANUALLY.
#
-versioning-plugin.versionCode=104
-versioning-plugin.versionName=0.1.4
+versioning-plugin.versionCode=105
+versioning-plugin.versionName=0.1.5