aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--AGENTS.md4
-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
3 files changed, 36 insertions, 0 deletions
diff --git a/AGENTS.md b/AGENTS.md
index 6708cd53..d9973397 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`).
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))
+ }
}