aboutsummaryrefslogtreecommitdiffstats
path: root/app
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-11-03 09:48:36 +0100
committerDanilo M. <danix@danix.xyz>2026-11-03 09:48:36 +0100
commitad3c63f665e6264ed1db42d6ae2dfc0947e33393 (patch)
treec91a67164da38b993961a97f2f8cfe01839512c6 /app
parentfd3909e16d9f7b2d0cc93ba1d640a361e37748be (diff)
downloadHardenedPass-ad3c63f665e6264ed1db42d6ae2dfc0947e33393.tar.gz
HardenedPass-ad3c63f665e6264ed1db42d6ae2dfc0947e33393.zip
fix(git): get sshj's Ed25519 keys from Bouncy Castle
Recent Conscrypt (Android 14 mainline updates) registers a KeyFactory for Ed25519. With sshj leaving provider selection to the JCA, its Ed25519KeyFactory now gets Conscrypt's key class, whose algorithm name sshj does not map to ssh-ed25519: KeyType.fromKey returns UNKNOWN and every Ed25519 host key fails with "Don't know how to encode key", so clone and sync over SSH break. 0.1.4 is affected since the sshj 0.41.2 update. Register a provider at position 1 that serves only KeyFactory.Ed25519 from Bouncy Castle (kept by the existing R8 rule). Everything else, including signatures with Android Keystore keys, still goes through normal JCA provider selection. Verified on an Android 14 tablet: clone over SSH failed before and works with the fix. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'app')
-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
2 files changed, 32 insertions, 0 deletions
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))
+ }
}