Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion sshlib/api.txt
Original file line number Diff line number Diff line change
Expand Up @@ -704,7 +704,7 @@ package org.connectbot.sshlib {
method public KeyPair decodePemPrivateKey(java.lang.String pem, optional java.lang.String? password);
method public java.lang.String encodeOpenSshPrivateKey(KeyPair keyPair, optional java.lang.String? password);
method public java.lang.String encodePemPrivateKey(KeyPair keyPair, optional java.lang.String? password);
method public void ensureEd25519Support();
method @Deprecated public void ensureEd25519Support();
field public static final org.connectbot.sshlib.SshKeys INSTANCE;
}

Expand Down
21 changes: 4 additions & 17 deletions sshlib/src/main/kotlin/org/connectbot/sshlib/SshKeys.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,7 @@ package org.connectbot.sshlib
import org.connectbot.sshlib.crypto.OpenSshKeyWriter
import org.connectbot.sshlib.crypto.PemKeyWriter
import org.connectbot.sshlib.crypto.PrivateKeyReader
import org.connectbot.sshlib.crypto.ed25519.Ed25519Provider
import java.security.KeyFactory
import java.security.KeyPair
import java.security.NoSuchAlgorithmException

/**
* Key management utilities for SSH private keys.
Expand Down Expand Up @@ -75,19 +72,9 @@ object SshKeys {
fun encodeOpenSshPrivateKey(keyPair: KeyPair, password: String? = null): String = OpenSshKeyWriter.write(keyPair, password)

/**
* Ensure Ed25519 JCA support is available.
*
* On platforms that don't natively support Ed25519 (e.g., older Android versions),
* this registers a Tink-backed JCA provider. On platforms with native support,
* this is a no-op.
*
* Call this early in application startup if you need Ed25519 key support.
* Ed25519 support is now selected automatically without changing the global
* JCE provider list.
*/
fun ensureEd25519Support() {
try {
KeyFactory.getInstance("Ed25519")
} catch (_: NoSuchAlgorithmException) {
Ed25519Provider.insertIfNeeded()
}
}
@Deprecated("Ed25519 support is selected automatically")
fun ensureEd25519Support() = Unit
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ package org.connectbot.sshlib.crypto
import org.connectbot.sshlib.SshException
import java.math.BigInteger
import java.security.AlgorithmParameters
import java.security.KeyFactory
import java.security.KeyPair
import java.security.KeyPairGenerator
import java.security.interfaces.ECPublicKey
Expand Down Expand Up @@ -89,7 +88,7 @@ internal class EcdhKeyExchange(private val curveName: String) : KexAlgorithm {
try {
val serverPoint = decodeEcPoint(serverPublicKey)
val serverPubKeySpec = ECPublicKeySpec(serverPoint, ecParameterSpec)
val serverPubKey = KeyFactory.getInstance("EC").generatePublic(serverPubKeySpec)
val serverPubKey = RawKeyFactory.generatePublic("EC", serverPubKeySpec)

val agreement = KeyAgreement.getInstance("ECDH")
agreement.init(kp.private)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/*
* ConnectBot SSH Library
* Copyright 2025 Kenny Root
* Copyright 2025-2026 Kenny Root
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -24,7 +24,6 @@ import org.connectbot.sshlib.protocol.SshPublicKey
import org.connectbot.sshlib.protocol.SshSignature
import java.math.BigInteger
import java.security.AlgorithmParameters
import java.security.KeyFactory
import java.security.PrivateKey
import java.security.Signature
import java.security.interfaces.ECKey
Expand Down Expand Up @@ -52,7 +51,7 @@ internal object EcdsaSignatureAlgorithm : SshSignatureAlgorithm {

val point = decodeEcPoint(qBytes, paramSpec)
val keySpec = ECPublicKeySpec(point, paramSpec)
val jcaKey = KeyFactory.getInstance("EC").generatePublic(keySpec)
val jcaKey = RawKeyFactory.generatePublic("EC", keySpec)

val sigBlob = sig.signatureBlob() as EcdsaSignatureBlob
val r = BigInteger(1, sigBlob.blob().r().body())
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/*
* ConnectBot SSH Library
* Copyright 2025 Kenny Root
* Copyright 2025-2026 Kenny Root
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -21,7 +21,6 @@ import org.connectbot.sshlib.protocol.SshEd25519PublicKeyBlob
import org.connectbot.sshlib.protocol.SshEd25519SignatureBlob
import org.connectbot.sshlib.protocol.SshPublicKey
import org.connectbot.sshlib.protocol.SshSignature
import java.security.KeyFactory
import java.security.PrivateKey
import java.security.Signature
import java.security.spec.X509EncodedKeySpec
Expand All @@ -42,7 +41,7 @@ internal object Ed25519SignatureAlgorithm : SshSignatureAlgorithm {
}
}
val keySpec = X509EncodedKeySpec(x509Key)
val jcaKey = KeyFactory.getInstance("Ed25519").generatePublic(keySpec)
val jcaKey = RawKeyFactory.generatePublic("Ed25519", keySpec)

val sigBlob = sig.signatureBlob() as SshEd25519SignatureBlob
val verifier = Signature.getInstance("Ed25519")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/*
* ConnectBot SSH Library
* Copyright 2025 Kenny Root
* Copyright 2025-2026 Kenny Root
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -21,7 +21,6 @@ import org.connectbot.sshlib.protocol.SshEd448PublicKeyBlob
import org.connectbot.sshlib.protocol.SshEd448SignatureBlob
import org.connectbot.sshlib.protocol.SshPublicKey
import org.connectbot.sshlib.protocol.SshSignature
import java.security.KeyFactory
import java.security.PrivateKey
import java.security.Signature
import java.security.spec.X509EncodedKeySpec
Expand All @@ -42,7 +41,7 @@ internal object Ed448SignatureAlgorithm : SshSignatureAlgorithm {
}
}
val keySpec = X509EncodedKeySpec(x509Key)
val jcaKey = KeyFactory.getInstance("Ed448").generatePublic(keySpec)
val jcaKey = RawKeyFactory.generatePublic("Ed448", keySpec)

val sigBlob = sig.signatureBlob() as SshEd448SignatureBlob
val verifier = Signature.getInstance("Ed448")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/*
* ConnectBot SSH Library
* Copyright 2025 Kenny Root
* Copyright 2025-2026 Kenny Root
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -24,7 +24,6 @@ import org.connectbot.sshlib.protocol.readString
import java.math.BigInteger
import java.nio.ByteBuffer
import java.security.AlgorithmParameters
import java.security.KeyFactory
import java.security.KeyPair
import java.security.spec.ECGenParameterSpec
import java.security.spec.ECParameterSpec
Expand Down Expand Up @@ -105,9 +104,6 @@ internal object OpenSshKeyReader {
octetString(encodeDer { octetString(seed) })
}
}
val privKey = KeyFactory.getInstance("Ed25519")
.generatePrivate(PKCS8EncodedKeySpec(pkcs8))

val pubKeyBytes = privateBytes.copyOfRange(32, 64)
val x509 = encodeDer {
sequence {
Expand All @@ -117,10 +113,11 @@ internal object OpenSshKeyReader {
bitString(pubKeyBytes)
}
}
val pubKey = KeyFactory.getInstance("Ed25519")
.generatePublic(X509EncodedKeySpec(x509))

keyPair = KeyPair(pubKey, privKey)
keyPair = RawKeyFactory.generateKeyPair(
"Ed25519",
X509EncodedKeySpec(x509),
PKCS8EncodedKeySpec(pkcs8),
)
sigAlgorithm = "ssh-ed25519"
}

Expand All @@ -144,8 +141,7 @@ internal object OpenSshKeyReader {
val pubKeySpec = ECPublicKeySpec(point, paramSpec)
val privKeySpec = ECPrivateKeySpec(privateScalar, paramSpec)

val kf = KeyFactory.getInstance("EC")
keyPair = KeyPair(kf.generatePublic(pubKeySpec), kf.generatePrivate(privKeySpec))
keyPair = RawKeyFactory.generateKeyPair("EC", pubKeySpec, privKeySpec)
sigAlgorithm = sshAlg
}

Expand All @@ -163,8 +159,7 @@ internal object OpenSshKeyReader {
val privKeySpec = RSAPrivateCrtKeySpec(n, e, d, p, q, dP, dQ, iqmp)
val pubKeySpec = RSAPublicKeySpec(n, e)

val kf = KeyFactory.getInstance("RSA")
keyPair = KeyPair(kf.generatePublic(pubKeySpec), kf.generatePrivate(privKeySpec))
keyPair = RawKeyFactory.generateKeyPair("RSA", pubKeySpec, privKeySpec)
sigAlgorithm = "rsa-sha2-512"
}

Expand Down
51 changes: 16 additions & 35 deletions sshlib/src/main/kotlin/org/connectbot/sshlib/crypto/PemKeyReader.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,26 +17,24 @@

package org.connectbot.sshlib.crypto

import com.google.crypto.tink.subtle.Ed25519Sign
import org.connectbot.sshlib.SshException
import org.connectbot.sshlib.crypto.ed25519.Ed25519PrivateKey
import java.math.BigInteger
import java.security.AlgorithmParameters
import java.security.KeyFactory
import java.security.KeyPair
import java.security.KeyPairGenerator
import java.security.PublicKey
import java.security.SecureRandom
import java.security.interfaces.ECPrivateKey
import java.security.interfaces.EdECPrivateKey
import java.security.interfaces.RSAPrivateCrtKey
import java.security.spec.ECGenParameterSpec
import java.security.spec.ECParameterSpec
import java.security.spec.ECPrivateKeySpec
import java.security.spec.ECPublicKeySpec
import java.security.spec.InvalidKeySpecException
import java.security.spec.NamedParameterSpec
import java.security.spec.PKCS8EncodedKeySpec
import java.security.spec.RSAPrivateCrtKeySpec
import java.security.spec.RSAPublicKeySpec
import java.security.spec.X509EncodedKeySpec

internal object PemKeyReader {

Expand Down Expand Up @@ -178,8 +176,7 @@ internal object PemKeyReader {

val privSpec = RSAPrivateCrtKeySpec(n, e, d, p, q, dP, dQ, qInv)
val pubSpec = RSAPublicKeySpec(n, e)
val kf = KeyFactory.getInstance("RSA")
val keyPair = KeyPair(kf.generatePublic(pubSpec), kf.generatePrivate(privSpec))
val keyPair = RawKeyFactory.generateKeyPair("RSA", pubSpec, privSpec)

SshPrivateKey("ssh-rsa", keyPair, "rsa-sha2-512")
}
Expand Down Expand Up @@ -218,26 +215,23 @@ internal object PemKeyReader {
val privKeySpec = ECPrivateKeySpec(BigInteger(1, privateBytes), paramSpec)
val pubKeySpec = ECPublicKeySpec(point, paramSpec)

val kf = KeyFactory.getInstance("EC")
val keyPair = KeyPair(kf.generatePublic(pubKeySpec), kf.generatePrivate(privKeySpec))
val keyPair = RawKeyFactory.generateKeyPair("EC", pubKeySpec, privKeySpec)
SshPrivateKey(sshAlg, keyPair, sshAlg)
}
}

private fun readPkcs8(data: ByteArray): SshPrivateKey {
// Use JCA's built-in PKCS#8 parsing — try each algorithm
try {
val kf = KeyFactory.getInstance("Ed25519")
val privKey = kf.generatePrivate(PKCS8EncodedKeySpec(data))
val edPriv = privKey as EdECPrivateKey
val seed = edPriv.bytes.orElseThrow { SshException("Cannot extract Ed25519 seed") }
val keySpec = PKCS8EncodedKeySpec(data)
val privKey = RawKeyFactory.generatePrivate("Ed25519", keySpec)
val seed = Ed25519PrivateKey(keySpec).getSeed()
val pubKey = ed25519PublicKeyFromSeed(seed)
return SshPrivateKey("ssh-ed25519", KeyPair(pubKey, privKey), "ssh-ed25519")
} catch (_: InvalidKeySpecException) {}

try {
val kf = KeyFactory.getInstance("EC")
val privKey = kf.generatePrivate(PKCS8EncodedKeySpec(data)) as ECPrivateKey
val privKey = RawKeyFactory.generatePrivate("EC", PKCS8EncodedKeySpec(data)) as ECPrivateKey
val fieldSize = (privKey.params.order.bitLength() + 7) / 8
val sshAlg = when (fieldSize) {
32 -> "ecdsa-sha2-nistp256"
Expand All @@ -250,39 +244,26 @@ internal object PemKeyReader {
} catch (_: InvalidKeySpecException) {}

try {
val kf = KeyFactory.getInstance("RSA")
val privKey = kf.generatePrivate(PKCS8EncodedKeySpec(data)) as RSAPrivateCrtKey
val privKey = RawKeyFactory.generatePrivate("RSA", PKCS8EncodedKeySpec(data)) as RSAPrivateCrtKey
val pubSpec = RSAPublicKeySpec(privKey.modulus, privKey.publicExponent)
val pubKey = kf.generatePublic(pubSpec)
val pubKey = RawKeyFactory.generatePublic("RSA", pubSpec)
return SshPrivateKey("ssh-rsa", KeyPair(pubKey, privKey), "rsa-sha2-512")
} catch (_: InvalidKeySpecException) {}

throw SshException("Unable to parse PKCS#8 key: unsupported algorithm")
}

internal fun ed25519PublicKeyFromSeed(seed: ByteArray): PublicKey {
// Build PKCS#8 from seed, create private key, then use KPG with deterministic random
val pkcs8 = encodeDer {
val rawPublicKey = Ed25519Sign.KeyPair.newKeyPairFromSeed(seed).publicKey
val x509 = encodeDer {
sequence {
integer(BigInteger.ZERO)
sequence {
objectIdentifier(byteArrayOf(0x2b, 0x65, 0x70)) // Ed25519
}
octetString(encodeDer { octetString(seed) })
bitString(rawPublicKey)
}
}
val privKey = KeyFactory.getInstance("Ed25519")
.generatePrivate(PKCS8EncodedKeySpec(pkcs8))

// Use a deterministic SecureRandom that returns our seed
val deterministicRandom = object : SecureRandom() {
override fun nextBytes(bytes: ByteArray) {
System.arraycopy(seed, 0, bytes, 0, minOf(seed.size, bytes.size))
}
}
val kpg = KeyPairGenerator.getInstance("Ed25519")
kpg.initialize(NamedParameterSpec.ED25519, deterministicRandom)
return kpg.generateKeyPair().public
return RawKeyFactory.generatePublic("Ed25519", X509EncodedKeySpec(x509))
}

private fun ecPublicKeyFromPkcs8(privKey: ECPrivateKey): PublicKey {
Expand All @@ -307,7 +288,7 @@ internal object PemKeyReader {
EcdsaSignatureAlgorithm.decodeEcPoint(pubPoint, privKey.params),
privKey.params,
)
return@readSequence KeyFactory.getInstance("EC").generatePublic(pubKeySpec)
return@readSequence RawKeyFactory.generatePublic("EC", pubKeySpec)
}

else -> seq.skipTag()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/*
* ConnectBot SSH Library
* Copyright 2025 Kenny Root
* Copyright 2025-2026 Kenny Root
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -21,13 +21,14 @@ import java.security.InvalidKeyException
import java.security.KeyFactory
import java.security.KeyPairGenerator
import java.security.PrivateKey
import java.security.Provider
import java.security.PublicKey
import java.security.spec.PKCS8EncodedKeySpec
import java.security.spec.X509EncodedKeySpec
import java.security.spec.XECPrivateKeySpec
import javax.crypto.KeyAgreement

internal class PlatformX25519Provider : X25519Provider {
internal class PlatformX25519Provider(provider: Provider) : X25519Provider {
companion object {
private const val ALGORITHM = "X25519"

Expand All @@ -50,8 +51,11 @@ internal class PlatformX25519Provider : X25519Provider {
private val BASE_POINT = ByteArray(X25519Provider.KEY_SIZE).apply { this[0] = 9 }
}

private val keyPairGenerator = KeyPairGenerator.getInstance(ALGORITHM)
private val keyFactory = KeyFactory.getInstance(ALGORITHM)
internal val providerName: String = provider.name

private val keyPairGenerator = KeyPairGenerator.getInstance(ALGORITHM, provider)
private val keyFactory = KeyFactory.getInstance(ALGORITHM, provider)
private val keyAgreementProvider = provider

override fun generatePrivateKey(): ByteArray {
val keyPair = keyPairGenerator.generateKeyPair()
Expand All @@ -60,7 +64,7 @@ internal class PlatformX25519Provider : X25519Provider {

override fun publicFromPrivate(privateKey: ByteArray): ByteArray {
val privKey = createPrivateKey(privateKey)
val ka = KeyAgreement.getInstance(ALGORITHM)
val ka = KeyAgreement.getInstance(ALGORITHM, keyAgreementProvider)
ka.init(privKey)
ka.doPhase(createPublicKey(BASE_POINT), true)
return ka.generateSecret()
Expand All @@ -69,7 +73,7 @@ internal class PlatformX25519Provider : X25519Provider {
override fun computeSharedSecret(privateKey: ByteArray, publicKey: ByteArray): ByteArray {
val privKey = createPrivateKey(privateKey)
val pubKey = createPublicKey(publicKey)
val ka = KeyAgreement.getInstance(ALGORITHM)
val ka = KeyAgreement.getInstance(ALGORITHM, keyAgreementProvider)
ka.init(privKey)
ka.doPhase(pubKey, true)
return ka.generateSecret()
Expand Down
Loading
Loading