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
5 changes: 5 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/

import net.researchgate.release.ReleaseExtension
import org.gradle.api.tasks.testing.Test

plugins {
alias(libs.plugins.kotlin.jvm) apply false
Expand All @@ -34,6 +35,10 @@ allprojects {
google()
mavenCentral()
}

tasks.withType<Test>().configureEach {
systemProperty("junit.platform.discovery.issue.severity.critical", "WARNING")
}
}

dependencies {
Expand Down
2 changes: 1 addition & 1 deletion protocol/src/main/resources/kaitai/ascii_string.ksy
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ seq:
- id: len
type: u4
valid:
expr: _ <= 65535
expr: _ <= 65535 and _ <= _io.size - _io.pos
- id: value
type: str
size: len
Expand Down
2 changes: 2 additions & 0 deletions protocol/src/main/resources/kaitai/byte_string.ksy
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,7 @@ doc: >
seq:
- id: len_data
type: u4
valid:
expr: _ <= _io.size - _io.pos
- id: data
size: len_data
2 changes: 2 additions & 0 deletions protocol/src/main/resources/kaitai/ecdsa_signature_blob.ksy
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ doc-ref: RFC 5656 section 3.1.2
seq:
- id: len_blob
type: u4
valid:
expr: _ <= _io.size - _io.pos
- id: blob
type: ecdsa_signature_inner
size: len_blob
Expand Down
2 changes: 2 additions & 0 deletions protocol/src/main/resources/kaitai/encrypted_packet.ksy
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ params:
seq:
- id: len_encrypted_payload
type: u4
valid:
expr: _ <= _io.size - _io.pos - len_mac
- id: encrypted_payload
size: len_encrypted_payload
- id: mac
Expand Down
2 changes: 2 additions & 0 deletions protocol/src/main/resources/kaitai/etm_mac.ksy
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,7 @@ seq:
type: u4
- id: len_encrypted_packet
type: u4
valid:
expr: _ == _io.size - _io.pos
- id: encrypted_packet
size: len_encrypted_packet
2 changes: 2 additions & 0 deletions protocol/src/main/resources/kaitai/mpint.ksy
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ doc-ref: 'https://tools.ietf.org/html/rfc4251#section-5'
seq:
- id: len_body
type: u4
valid:
expr: _ <= _io.size - _io.pos
- id: body
size: len_body
instances:
Expand Down
2 changes: 2 additions & 0 deletions protocol/src/main/resources/kaitai/name_list.ksy
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ doc-ref: 'https://tools.ietf.org/html/rfc4251#section-5'
seq:
- id: len_entries
type: u4
valid:
expr: _ <= _io.size - _io.pos
- id: entries
type: name_entry
size: len_entries
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ seq:
doc: Hostname of the previous hop (empty for origin)
- id: num_from_keyspecs
type: u4
valid:
expr: _ <= (_io.size - _io.pos) / 5
doc: Number of from host key specs
- id: from_keyspecs
type: keyspec
Expand All @@ -27,6 +29,8 @@ seq:
doc: Destination hostname
- id: num_to_hostspecs
type: u4
valid:
expr: _ <= (_io.size - _io.pos) / 5
doc: Number of destination host key specs
- id: to_hostspecs
type: keyspec
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ doc: |
seq:
- id: nkeys
type: u4
valid:
expr: _ <= (_io.size - _io.pos) / 8
doc: Number of keys in the response
- id: identities
type: identity
Expand Down
2 changes: 2 additions & 0 deletions protocol/src/main/resources/kaitai/ssh_agent_message.ksy
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ doc: |
seq:
- id: length
type: u4
valid:
expr: _ >= 1 and _ == _io.size - _io.pos
doc: Length of message_type + payload
- id: message_type
type: u1
Expand Down
2 changes: 2 additions & 0 deletions protocol/src/main/resources/kaitai/ssh_msg_ext_info.ksy
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ doc: >
seq:
- id: num_extensions
type: u4
valid:
expr: _ <= (_io.size - _io.pos) / 8
doc: Number of extension name-value pairs
- id: extensions
type: extension
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ seq:
type: byte_string
- id: num_prompts
type: u4
valid:
expr: _ <= (_io.size - _io.pos) / 5
- id: prompts
type: prompt
repeat: expr
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ doc-ref: RFC 4256 section 3.4
seq:
- id: num_responses
type: u4
valid:
expr: _ <= (_io.size - _io.pos) / 4
- id: responses
type: byte_string
repeat: expr
Expand Down
2 changes: 2 additions & 0 deletions protocol/src/main/resources/kaitai/ssh_public_key.ksy
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ doc: >
seq:
- id: algorithm_name_len
type: u4
valid:
expr: _ <= _io.size - _io.pos
- id: algorithm_name
type: str
size: algorithm_name_len
Expand Down
2 changes: 2 additions & 0 deletions protocol/src/main/resources/kaitai/ssh_signature.ksy
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ doc: >
seq:
- id: algorithm_name_len
type: u4
valid:
expr: _ <= _io.size - _io.pos
- id: algorithm_name
type: str
size: algorithm_name_len
Expand Down
4 changes: 4 additions & 0 deletions protocol/src/main/resources/kaitai/unencrypted_packet.ksy
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,12 @@ meta:
seq:
- id: len_packet
type: u4
valid:
expr: _ >= 6 and _ == _io.size - _io.pos
- id: len_random_padding
type: u1
valid:
expr: _ >= 4 and _ <= len_packet - 2
- id: payload
type: unencrypted_payload
size: len_packet - len_random_padding - 1
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ doc-ref: RFC 4462 section 3.2
seq:
- id: num_mechanisms
type: u4
valid:
expr: _ <= (_io.size - _io.pos) / 4
doc: The number of mechanism OIDs client supports
- id: mechanisms
type: byte_string
Expand Down
2 changes: 2 additions & 0 deletions protocol/src/main/resources/kaitai/utf8_string.ksy
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ doc-ref: 'https://tools.ietf.org/html/rfc4251#section-5'
seq:
- id: len
type: u4
valid:
expr: _ <= _io.size - _io.pos
- id: value
type: str
size: len
Expand Down
34 changes: 34 additions & 0 deletions sshlib/src/main/kotlin/org/connectbot/sshlib/KaitaiParsing.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* ConnectBot SSH Library
* Copyright 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.connectbot.sshlib

import io.kaitai.struct.KaitaiStream
import java.nio.BufferUnderflowException

internal fun Throwable.kaitaiParseFailureOrNull(): Throwable? {
var current: Throwable? = this
while (current != null) {
if (current is KaitaiStream.KaitaiStructError ||
current is BufferUnderflowException
) {
return current
}
current = current.cause
}
return null
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import org.connectbot.sshlib.AgentProvider
import org.connectbot.sshlib.AgentSigningContext
import org.connectbot.sshlib.DestinationConstraint
import org.connectbot.sshlib.crypto.SignatureVerifier
import org.connectbot.sshlib.kaitaiParseFailureOrNull
import org.connectbot.sshlib.protocol.SshAgentIdentitiesAnswer
import org.connectbot.sshlib.protocol.SshAgentMessage
import org.connectbot.sshlib.protocol.SshAgentSignResponse
Expand Down Expand Up @@ -157,24 +158,32 @@ internal class AgentProtocolHandler(
suspend fun handleRequest(requestBytes: ByteArray): ByteArray {
logger.debug("Handling agent request (${requestBytes.size} bytes)")

val stream = ByteBufferKaitaiStream(requestBytes)
val message = SshAgentMessage(stream)
message._read()
val message = try {
parseAgentMessage(requestBytes)
} catch (e: MalformedAgentRequestException) {
logger.warn("Malformed SSH agent request", e.cause)
return createFailureResponse()
}

val messageType = message.messageType()
logger.debug("Agent message type: $messageType")

return when (messageType) {
SSH_AGENTC_REQUEST_IDENTITIES -> handleRequestIdentities()
return try {
when (messageType) {
SSH_AGENTC_REQUEST_IDENTITIES -> handleRequestIdentities()

SSH_AGENTC_SIGN_REQUEST -> handleSignRequest(message)
SSH_AGENTC_SIGN_REQUEST -> handleSignRequest(message)

SSH_AGENTC_EXTENSION -> handleExtension(message)
SSH_AGENTC_EXTENSION -> handleExtension(message)

else -> {
logger.warn("Unknown agent message type: $messageType")
createFailureResponse()
else -> {
logger.warn("Unknown agent message type: $messageType")
createFailureResponse()
}
}
} catch (e: MalformedAgentRequestException) {
logger.warn("Malformed SSH agent request", e.cause)
return createFailureResponse()
}
}

Expand Down Expand Up @@ -340,16 +349,34 @@ internal class AgentProtocolHandler(
return createSuccessResponse()
}

private inline fun <reified T : KaitaiStruct.ReadWrite> parsePayload(message: SshAgentMessage): T {
private fun parseAgentMessage(requestBytes: ByteArray): SshAgentMessage = try {
val stream = ByteBufferKaitaiStream(requestBytes)
val message = SshAgentMessage(stream)
message._read()
message
} catch (e: RuntimeException) {
throwMalformedAgentRequest(e)
}

private inline fun <reified T : KaitaiStruct.ReadWrite> parsePayload(message: SshAgentMessage): T = try {
val stream = ByteBufferKaitaiStream(message._raw_payload())
val payload = T::class.java.getConstructor(KaitaiStream::class.java).newInstance(stream)
payload._read()
return payload
payload
} catch (e: Exception) {
throwMalformedAgentRequest(e)
}

private fun createFailureResponse(): ByteArray = buildAgentMessage(SSH_AGENT_FAILURE, ByteArray(0))

private fun createSuccessResponse(): ByteArray = buildAgentMessage(SSH_AGENT_SUCCESS, ByteArray(0))

private fun throwMalformedAgentRequest(e: Exception): Nothing {
val parseFailure = e.kaitaiParseFailureOrNull() ?: throw e
throw MalformedAgentRequestException(parseFailure)
}

private class MalformedAgentRequestException(cause: Throwable) : Exception(cause)
}

internal data class AgentSessionInfo(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ import org.connectbot.sshlib.crypto.SignatureEntry
import org.connectbot.sshlib.crypto.SignatureVerifier
import org.connectbot.sshlib.crypto.SshPrivateKey
import org.connectbot.sshlib.crypto.SshPublicKeyEncoder
import org.connectbot.sshlib.kaitaiParseFailureOrNull
import org.connectbot.sshlib.protocol.AsciiString
import org.connectbot.sshlib.protocol.ChannelOpenDirectTcpip
import org.connectbot.sshlib.protocol.ChannelOpenForwardedTcpip
Expand Down Expand Up @@ -130,6 +131,7 @@ import org.connectbot.sshlib.protocol.createUtf8String
import org.connectbot.sshlib.protocol.toByteArray
import org.connectbot.sshlib.transport.PacketIO
import org.connectbot.sshlib.transport.Transport
import org.connectbot.sshlib.transport.TransportException
import org.slf4j.LoggerFactory
import java.math.BigInteger
import java.nio.ByteBuffer
Expand Down Expand Up @@ -2545,15 +2547,21 @@ class SshConnection(
} catch (_: CancellationException) {
logger.debug("Packet loop cancelled")
} catch (e: Exception) {
pendingConnect?.completeExceptionally(e)
val packetFailure = e.kaitaiParseFailureOrNull()
val loopFailure = when {
e is SshException -> e
packetFailure != null -> TransportException("Malformed SSH packet", packetFailure)
else -> e
}
pendingConnect?.completeExceptionally(loopFailure)
pendingConnect = null
val allClosed = channels.values.all { !it.isOpen }
if (allClosed) {
logger.debug("Packet loop ended (all channels closed)")
} else {
logger.warn("Packet loop ended unexpectedly", e)
logger.warn("Packet loop ended unexpectedly", loopFailure)
}
loopException = e
loopException = loopFailure
} finally {
for (ch in channels.values) {
ch.onClose()
Expand Down
30 changes: 18 additions & 12 deletions sshlib/src/main/kotlin/org/connectbot/sshlib/transport/PacketIO.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import org.connectbot.sshlib.crypto.PacketAead
import org.connectbot.sshlib.crypto.PacketCipher
import org.connectbot.sshlib.crypto.PacketCompressor
import org.connectbot.sshlib.crypto.PacketMac
import org.connectbot.sshlib.kaitaiParseFailureOrNull
import org.connectbot.sshlib.protocol.IdBanner
import org.connectbot.sshlib.protocol.UnencryptedPacket
import org.slf4j.LoggerFactory
Expand Down Expand Up @@ -216,18 +217,23 @@ internal class PacketIO(private val transport: Transport) {
* can parse them with proper parent context for body length calculation.
*/
private fun parsePayloadBytes(payloadBytes: ByteArray): UnencryptedPacket.UnencryptedPayload {
val paddingLength = 4
val packetLength = 1 + payloadBytes.size + paddingLength
val buffer = ByteArrayOutputStream()
buffer.write(ByteBuffer.allocate(4).putInt(packetLength).array())
buffer.write(paddingLength)
buffer.write(payloadBytes)
buffer.write(ByteArray(paddingLength))
val fullPacket = buffer.toByteArray()
val stream = ByteBufferKaitaiStream(fullPacket)
val packet = UnencryptedPacket(stream)
packet._read()
return packet.payload()
try {
val paddingLength = 4
val packetLength = 1 + payloadBytes.size + paddingLength
val buffer = ByteArrayOutputStream()
buffer.write(ByteBuffer.allocate(4).putInt(packetLength).array())
buffer.write(paddingLength)
buffer.write(payloadBytes)
buffer.write(ByteArray(paddingLength))
val fullPacket = buffer.toByteArray()
val stream = ByteBufferKaitaiStream(fullPacket)
val packet = UnencryptedPacket(stream)
packet._read()
return packet.payload()
} catch (e: RuntimeException) {
val parseFailure = e.kaitaiParseFailureOrNull() ?: throw e
throw TransportException("Malformed SSH packet payload", parseFailure)
}
}

/**
Expand Down
Loading
Loading