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 .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
timeout-minutes: 30
strategy:
matrix:
java: [17, 21]
java: [17, 21, 25]
steps:
- name: Checkout (with history)
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,13 @@ package org.connectbot.sshlib.protocol

import io.kaitai.struct.ByteBufferKaitaiStream
import io.kaitai.struct.KaitaiStruct
import java.nio.BufferOverflowException

/**
* Serialize a Kaitai struct to a byte array.
*
* Kaitai's [ByteBufferKaitaiStream] is fixed-capacity, so the underlying
* `ByteBuffer.put` throws [java.nio.BufferOverflowException] if the
* `ByteBuffer.put` throws [BufferOverflowException] if the
* pre-allocated buffer is too small. We don't have a cheap way to know
* the encoded size up front, so start at 16 KiB and double on overflow
* until the message fits or we cross [MAX_BUFFER]. Most SSH messages
Expand All @@ -40,7 +41,7 @@ fun KaitaiStruct.ReadWrite.toByteArray(): ByteArray {
val size = io.pos()
io.seek(0)
return io.readBytes(size.toLong())
} catch (_: java.nio.BufferOverflowException) {
} catch (_: BufferOverflowException) {
if (capacity >= MAX_BUFFER) throw IllegalStateException("Kaitai message exceeds $MAX_BUFFER byte serialization limit")
capacity = minOf(capacity * 2, MAX_BUFFER)
}
Expand Down
3 changes: 2 additions & 1 deletion sshlib/src/main/kotlin/org/connectbot/sshlib/SshKeys.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ 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 @@ -83,7 +84,7 @@ object SshKeys {
fun ensureEd25519Support() {
try {
KeyFactory.getInstance("Ed25519")
} catch (_: java.security.NoSuchAlgorithmException) {
} catch (_: NoSuchAlgorithmException) {
Ed25519Provider.insertIfNeeded()
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package org.connectbot.sshlib.client

import io.kaitai.struct.ByteBufferKaitaiStream
import io.kaitai.struct.KaitaiStream
import io.kaitai.struct.KaitaiStruct
import org.connectbot.sshlib.AgentIdentity
import org.connectbot.sshlib.AgentKeySpec
Expand Down Expand Up @@ -340,7 +341,7 @@ internal class AgentProtocolHandler(

private inline fun <reified T : KaitaiStruct.ReadWrite> parsePayload(message: SshAgentMessage): T {
val stream = ByteBufferKaitaiStream(message._raw_payload())
val payload = T::class.java.getConstructor(io.kaitai.struct.KaitaiStream::class.java).newInstance(stream)
val payload = T::class.java.getConstructor(KaitaiStream::class.java).newInstance(stream)
payload._read()
return payload
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ internal class DynamicPortForwarder(
): DynamicPortForwarder {
val selectorManager = SelectorManager(Dispatchers.IO)
val serverSocket = aSocket(selectorManager).tcp().bind(bindAddress.hostString, bindAddress.port)
val actualAddress = serverSocket.localAddress.toJavaAddress() as java.net.InetSocketAddress
val actualAddress = serverSocket.localAddress.toJavaAddress() as InetSocketAddress

val handler = Socks5Handler(authenticator)
val forwarder = DynamicPortForwarder(
Expand Down Expand Up @@ -108,7 +108,7 @@ internal class DynamicPortForwarder(
return
}

val remoteAddr = socket.remoteAddress.toJavaAddress() as? java.net.InetSocketAddress
val remoteAddr = socket.remoteAddress.toJavaAddress() as? InetSocketAddress
val originAddr = remoteAddr?.hostString ?: "127.0.0.1"
val originPort = remoteAddr?.port ?: 0

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ internal class LocalPortForwarder(
): LocalPortForwarder {
val selectorManager = SelectorManager(Dispatchers.IO)
val serverSocket = aSocket(selectorManager).tcp().bind(bindAddress.hostString, bindAddress.port)
val actualAddress = serverSocket.localAddress.toJavaAddress() as java.net.InetSocketAddress
val actualAddress = serverSocket.localAddress.toJavaAddress() as InetSocketAddress

val forwarder = LocalPortForwarder(
scope,
Expand Down Expand Up @@ -92,7 +92,7 @@ internal class LocalPortForwarder(
}

private suspend fun handleConnection(socket: Socket) {
val remoteAddr = socket.remoteAddress.toJavaAddress() as? java.net.InetSocketAddress
val remoteAddr = socket.remoteAddress.toJavaAddress() as? InetSocketAddress
val originAddr = remoteAddr?.hostString ?: "127.0.0.1"
val originPort = remoteAddr?.port ?: 0

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package org.connectbot.sshlib.client

import io.kaitai.struct.ByteBufferKaitaiStream
import io.kaitai.struct.KaitaiStream
import io.kaitai.struct.KaitaiStruct
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CloseableCoroutineDispatcher
Expand Down Expand Up @@ -2399,7 +2400,7 @@ class SshConnection(
private inline fun <reified T : KaitaiStruct.ReadWrite> parseBody(packet: UnencryptedPacket.UnencryptedPayload): T {
val rawBody = packet._raw_body()
val stream = ByteBufferKaitaiStream(rawBody)
val msg = T::class.java.getConstructor(io.kaitai.struct.KaitaiStream::class.java).newInstance(stream)
val msg = T::class.java.getConstructor(KaitaiStream::class.java).newInstance(stream)
msg._read()
return msg
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,14 @@

package org.connectbot.sshlib.crypto

import java.util.Base64

/**
* Base64 compatibility adapter supporting both JVM and Android API 24+.
*
* On Android below API 26, `java.util.Base64` is unavailable. This object detects
* On Android below API 26, `Base64` is unavailable. This object detects
* `android.util.Base64` at runtime via reflection and delegates to it; otherwise
* falls back to `java.util.Base64`.
* falls back to `Base64`.
*/
internal object Base64Compat {

Expand All @@ -45,9 +47,9 @@ internal object Base64Compat {
}

private class JvmDelegate : Delegate {
private val encoder = java.util.Base64.getEncoder()
private val encoderNoPad = java.util.Base64.getEncoder().withoutPadding()
private val decoder = java.util.Base64.getDecoder()
private val encoder = Base64.getEncoder()
private val encoderNoPad = Base64.getEncoder().withoutPadding()
private val decoder = Base64.getDecoder()

override fun encode(data: ByteArray): String = encoderNoPad.encodeToString(data)
override fun encodeWithPadding(data: ByteArray): String = encoder.encodeToString(data)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ 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
import java.security.spec.ECGenParameterSpec
Expand All @@ -42,7 +43,7 @@ internal class EcdhKeyExchange(private val curveName: String) : KexAlgorithm {
private val fieldSize: Int
override val hashAlgorithm: String

private var clientKeyPair: java.security.KeyPair? = null
private var clientKeyPair: KeyPair? = null

init {
val (jcaName, hash) = when (curveName) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ 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
import java.security.spec.ECGenParameterSpec
Expand Down Expand Up @@ -89,7 +90,7 @@ internal object EcdsaSignatureAlgorithm : SshSignatureAlgorithm {
}
}

override fun sign(algorithmName: String, privateKey: java.security.PrivateKey, data: ByteArray): ByteArray {
override fun sign(algorithmName: String, privateKey: PrivateKey, data: ByteArray): ByteArray {
val ecKey = privateKey as ECKey
val fieldSize = (ecKey.params.order.bitLength() + 7) / 8

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ 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 Down Expand Up @@ -49,7 +50,7 @@ internal object Ed25519SignatureAlgorithm : SshSignatureAlgorithm {
return verifier.verify(sigBlob.signature().data())
}

override fun sign(algorithmName: String, privateKey: java.security.PrivateKey, data: ByteArray): ByteArray {
override fun sign(algorithmName: String, privateKey: PrivateKey, data: ByteArray): ByteArray {
val signer = Signature.getInstance("Ed25519")
signer.initSign(privateKey)
signer.update(data)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ 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 Down Expand Up @@ -49,7 +50,7 @@ internal object Ed448SignatureAlgorithm : SshSignatureAlgorithm {
return verifier.verify(sigBlob.signature().data())
}

override fun sign(algorithmName: String, privateKey: java.security.PrivateKey, data: ByteArray): ByteArray {
override fun sign(algorithmName: String, privateKey: PrivateKey, data: ByteArray): ByteArray {
val signer = Signature.getInstance("Ed448")
signer.initSign(privateKey)
signer.update(data)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@ import java.io.IOException
import java.security.KeyFactory
import java.security.KeyPairGenerator
import java.security.PrivateKey
import java.security.PublicKey
import java.security.spec.PKCS8EncodedKeySpec
import java.security.spec.X509EncodedKeySpec
import javax.crypto.SecretKey

/**
* ML-KEM provider using Java 23+ native javax.crypto.KEM API via reflection.
Expand All @@ -39,6 +41,30 @@ internal class JavaMlKemProvider : MlKemProvider {
0x03, 0x82.toByte(), 0x04, 0xa1.toByte(), // BIT STRING
0x00, // no unused bits
)

internal fun extractRawMlKemPublicKey(x509Encoded: ByteArray): ByteArray {
if (x509Encoded.size < X509_PREFIX.size) throw IOException("X.509 encoded ML-KEM public key too short")
if (x509Encoded[0] != 0x30.toByte()) throw IOException("Invalid X.509 encoding: expected SEQUENCE tag")
if (x509Encoded[17] != 0x03.toByte()) throw IOException("Invalid X.509 encoding: BIT STRING not found")
if (x509Encoded[21] != 0x00.toByte()) throw IOException("Invalid X.509 encoding: unexpected unused bits")
if (x509Encoded.size < X509_PREFIX.size + MLKEM768_PUBLIC_KEY_SIZE) {
throw IOException("X.509 encoded ML-KEM public key missing raw key bytes")
}

val rawKey = ByteArray(MLKEM768_PUBLIC_KEY_SIZE)
System.arraycopy(x509Encoded, X509_PREFIX.size, rawKey, 0, MLKEM768_PUBLIC_KEY_SIZE)
return rawKey
}

internal fun wrapRawMlKemPublicKey(rawKey: ByteArray): ByteArray {
if (rawKey.size != MLKEM768_PUBLIC_KEY_SIZE) {
throw IOException("Invalid raw ML-KEM public key size: ${rawKey.size}")
}
val x509 = ByteArray(X509_PREFIX.size + MLKEM768_PUBLIC_KEY_SIZE)
System.arraycopy(X509_PREFIX, 0, x509, 0, X509_PREFIX.size)
System.arraycopy(rawKey, 0, x509, X509_PREFIX.size, MLKEM768_PUBLIC_KEY_SIZE)
return x509
}
}

private val kemInstance: Any
Expand Down Expand Up @@ -75,7 +101,7 @@ internal class JavaMlKemProvider : MlKemProvider {
val pubKey = kf.generatePublic(X509EncodedKeySpec(x509Encoded))

val kemClass = Class.forName("javax.crypto.KEM")
val newEncapsulator = kemClass.getMethod("newEncapsulator", java.security.PublicKey::class.java)
val newEncapsulator = kemClass.getMethod("newEncapsulator", PublicKey::class.java)
val encapsulator = newEncapsulator.invoke(kemInstance, pubKey)

val encapsulatorClass = Class.forName("javax.crypto.KEM\$Encapsulator")
Expand All @@ -87,7 +113,7 @@ internal class JavaMlKemProvider : MlKemProvider {
val ciphertext = encapsulationMethod.invoke(encapsulated) as ByteArray

val keyMethod = encapsulatedClass.getMethod("key")
val secretKey = keyMethod.invoke(encapsulated) as javax.crypto.SecretKey
val secretKey = keyMethod.invoke(encapsulated) as SecretKey
val sharedSecret = secretKey.encoded

return MlKemEncapsulationResult(ciphertext, sharedSecret)
Expand All @@ -107,32 +133,11 @@ internal class JavaMlKemProvider : MlKemProvider {

val decapsulatorClass = Class.forName("javax.crypto.KEM\$Decapsulator")
val decapsulateMethod = decapsulatorClass.getMethod("decapsulate", ByteArray::class.java)
val secretKey = decapsulateMethod.invoke(decapsulator, ciphertext) as javax.crypto.SecretKey
val secretKey = decapsulateMethod.invoke(decapsulator, ciphertext) as SecretKey

return secretKey.encoded
} catch (e: Exception) {
throw IOException("ML-KEM decapsulation failed", e)
}
}

private fun extractRawMlKemPublicKey(x509Encoded: ByteArray): ByteArray {
if (x509Encoded.size < 22) throw IOException("X.509 encoded ML-KEM public key too short")
if (x509Encoded[0] != 0x30.toByte()) throw IOException("Invalid X.509 encoding: expected SEQUENCE tag")
if (x509Encoded[17] != 0x03.toByte()) throw IOException("Invalid X.509 encoding: BIT STRING not found")
if (x509Encoded[21] != 0x00.toByte()) throw IOException("Invalid X.509 encoding: unexpected unused bits")

val rawKey = ByteArray(MLKEM768_PUBLIC_KEY_SIZE)
System.arraycopy(x509Encoded, 22, rawKey, 0, MLKEM768_PUBLIC_KEY_SIZE)
return rawKey
}

private fun wrapRawMlKemPublicKey(rawKey: ByteArray): ByteArray {
if (rawKey.size != MLKEM768_PUBLIC_KEY_SIZE) {
throw IOException("Invalid raw ML-KEM public key size: ${rawKey.size}")
}
val x509 = ByteArray(X509_PREFIX.size + MLKEM768_PUBLIC_KEY_SIZE)
System.arraycopy(X509_PREFIX, 0, x509, 0, X509_PREFIX.size)
System.arraycopy(rawKey, 0, x509, X509_PREFIX.size, MLKEM768_PUBLIC_KEY_SIZE)
return x509
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import java.security.KeyPair
import java.security.SecureRandom
import java.security.interfaces.ECPrivateKey
import java.security.interfaces.ECPublicKey
import java.security.interfaces.EdECPrivateKey
import java.security.interfaces.RSAPrivateCrtKey
import java.security.interfaces.RSAPublicKey

Expand Down Expand Up @@ -144,7 +145,7 @@ internal object OpenSshKeyWriter {
private fun extractEd25519Seed(keyPair: KeyPair): ByteArray {
val privKey = keyPair.private
return when {
privKey is java.security.interfaces.EdECPrivateKey -> {
privKey is EdECPrivateKey -> {
privKey.bytes.orElseThrow { SshException("Cannot extract Ed25519 seed") }
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ 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
Expand Down Expand Up @@ -209,15 +215,15 @@ internal object PemKeyReader {
try {
val kf = KeyFactory.getInstance("Ed25519")
val privKey = kf.generatePrivate(PKCS8EncodedKeySpec(data))
val edPriv = privKey as java.security.interfaces.EdECPrivateKey
val edPriv = privKey as EdECPrivateKey
val seed = edPriv.bytes.orElseThrow { SshException("Cannot extract Ed25519 seed") }
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 java.security.interfaces.ECPrivateKey
val privKey = kf.generatePrivate(PKCS8EncodedKeySpec(data)) as ECPrivateKey
val fieldSize = (privKey.params.order.bitLength() + 7) / 8
val sshAlg = when (fieldSize) {
32 -> "ecdsa-sha2-nistp256"
Expand All @@ -231,7 +237,7 @@ internal object PemKeyReader {

try {
val kf = KeyFactory.getInstance("RSA")
val privKey = kf.generatePrivate(PKCS8EncodedKeySpec(data)) as java.security.interfaces.RSAPrivateCrtKey
val privKey = kf.generatePrivate(PKCS8EncodedKeySpec(data)) as RSAPrivateCrtKey
val pubSpec = RSAPublicKeySpec(privKey.modulus, privKey.publicExponent)
val pubKey = kf.generatePublic(pubSpec)
return SshPrivateKey("ssh-rsa", KeyPair(pubKey, privKey), "rsa-sha2-512")
Expand All @@ -240,7 +246,7 @@ internal object PemKeyReader {
throw SshException("Unable to parse PKCS#8 key: unsupported algorithm")
}

internal fun ed25519PublicKeyFromSeed(seed: ByteArray): java.security.PublicKey {
internal fun ed25519PublicKeyFromSeed(seed: ByteArray): PublicKey {
// Build PKCS#8 from seed, create private key, then use KPG with deterministic random
val pkcs8 = encodeDer {
sequence {
Expand All @@ -255,17 +261,17 @@ internal object PemKeyReader {
.generatePrivate(PKCS8EncodedKeySpec(pkcs8))

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

private fun ecPublicKeyFromPkcs8(privKey: java.security.interfaces.ECPrivateKey): java.security.PublicKey {
private fun ecPublicKeyFromPkcs8(privKey: ECPrivateKey): PublicKey {
// Parse the PKCS#8 encoding to extract the embedded public key
val encoded = privKey.encoded
val reader = DerReader(encoded)
Expand Down
Loading
Loading