diff options
| -rw-r--r-- | AGENTS.md | 6 | ||||
| -rw-r--r-- | CHANGELOG.md | 7 | ||||
| -rw-r--r-- | app/src/main/java/app/passwordstore/util/git/sshj/SshjConfig.kt | 18 | ||||
| -rw-r--r-- | app/src/test/java/app/passwordstore/util/git/sshj/SshjConfigTest.kt | 14 | ||||
| -rw-r--r-- | app/version.properties | 4 |
5 files changed, 46 insertions, 3 deletions
@@ -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 |
