From 6707ebb4350b35daf75d29f2e93c640c058eef09 Mon Sep 17 00:00:00 2001 From: Kenny Root Date: Wed, 20 May 2026 18:24:03 -0700 Subject: [PATCH 1/2] chore: respond to sonarqube maint issues Extract constants, add comments about intent in empty methods, set cognitive complexity limits for special files --- .github/workflows/ci.yml | 2 +- build.gradle.kts | 29 ++++++++ protocol/build.gradle.kts | 1 + sshlib/build.gradle.kts | 10 --- .../org/connectbot/sshlib/AuthHandler.kt | 8 +- .../org/connectbot/sshlib/HostKeyVerifier.kt | 11 ++- .../kotlin/org/connectbot/sshlib/SshClient.kt | 42 ++++++----- .../connectbot/sshlib/client/SshConnection.kt | 74 ++++++++++--------- .../connectbot/sshlib/crypto/Algorithms.kt | 38 +++++----- .../crypto/DiffieHellmanGroupExchange.kt | 13 ++-- .../sshlib/crypto/JavaMlKemProvider.kt | 16 ++-- .../connectbot/sshlib/crypto/KeyDecryption.kt | 23 +++--- .../connectbot/sshlib/crypto/KeyEncryption.kt | 19 +++-- .../org/connectbot/sshlib/crypto/KeyTypes.kt | 10 ++- .../connectbot/sshlib/crypto/PacketAead.kt | 7 +- .../connectbot/sshlib/crypto/PacketCipher.kt | 7 +- .../org/connectbot/sshlib/crypto/PacketMac.kt | 7 +- .../sshlib/crypto/PrivateKeyReader.kt | 13 ++-- 18 files changed, 199 insertions(+), 131 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 37030e3..6b4c7af 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -63,4 +63,4 @@ jobs: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} if: "${{ matrix.java == '25' && env.SONAR_TOKEN != '' }}" - run: ./gradlew :sshlib:sonar -Dsonar.projectVersion=${{ github.sha }} + run: ./gradlew sonar -Dsonar.projectVersion=${{ github.sha }} diff --git a/build.gradle.kts b/build.gradle.kts index 5da3a85..004dfd5 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -22,6 +22,7 @@ plugins { alias(libs.plugins.publish) apply false alias(libs.plugins.kover) alias(libs.plugins.cyclonedx) + alias(libs.plugins.sonarqube) } allprojects { @@ -34,9 +35,37 @@ allprojects { } dependencies { + kover(project(":protocol")) kover(project(":sshlib")) } +sonar { + properties { + property("sonar.projectName", "ConnectBot SSH Library") + property("sonar.projectKey", "connectbot_cbssh") + property("sonar.organization", "connectbot") + property("sonar.host.url", "https://sonarcloud.io") + property("sonar.coverage.jacoco.xmlReportPaths", "build/reports/kover/report.xml") + property("sonar.exclusions", "**/build/generated/**") + property("sonar.issue.ignore.multicriteria", "cognitiveComplexityConnection,cognitiveComplexitySftp,cognitiveComplexityTransport") + property("sonar.issue.ignore.multicriteria.cognitiveComplexityConnection.ruleKey", "kotlin:S3776") + property( + "sonar.issue.ignore.multicriteria.cognitiveComplexityConnection.resourceKey", + "**/src/main/kotlin/org/connectbot/sshlib/client/SshConnection.kt", + ) + property("sonar.issue.ignore.multicriteria.cognitiveComplexitySftp.ruleKey", "kotlin:S3776") + property( + "sonar.issue.ignore.multicriteria.cognitiveComplexitySftp.resourceKey", + "**/src/main/kotlin/org/connectbot/sshlib/client/sftp/SftpDispatcher.kt", + ) + property("sonar.issue.ignore.multicriteria.cognitiveComplexityTransport.ruleKey", "kotlin:S3776") + property( + "sonar.issue.ignore.multicriteria.cognitiveComplexityTransport.resourceKey", + "**/src/main/kotlin/org/connectbot/sshlib/transport/KtorTcpTransport.kt", + ) + } +} + spotless { ratchetFrom = "origin/main" diff --git a/protocol/build.gradle.kts b/protocol/build.gradle.kts index cf75aec..0e9741d 100644 --- a/protocol/build.gradle.kts +++ b/protocol/build.gradle.kts @@ -21,6 +21,7 @@ plugins { alias(libs.plugins.kotlin.jvm) alias(libs.plugins.publish) alias(libs.plugins.dokka) + alias(libs.plugins.kover) alias(libs.plugins.cyclonedx) `java-library` } diff --git a/sshlib/build.gradle.kts b/sshlib/build.gradle.kts index 3405f48..02e7205 100644 --- a/sshlib/build.gradle.kts +++ b/sshlib/build.gradle.kts @@ -24,7 +24,6 @@ plugins { alias(libs.plugins.metalava) alias(libs.plugins.kover) alias(libs.plugins.cyclonedx) - alias(libs.plugins.sonarqube) `java-library` } @@ -147,12 +146,3 @@ mavenPublishing { } } } - -sonar { - properties { - property("sonar.projectKey", "connectbot_cbssh") - property("sonar.organization", "connectbot") - property("sonar.host.url", "https://sonarcloud.io") - property("sonar.coverage.jacoco.xmlReportPaths", "build/reports/kover/report.xml") - } -} diff --git a/sshlib/src/main/kotlin/org/connectbot/sshlib/AuthHandler.kt b/sshlib/src/main/kotlin/org/connectbot/sshlib/AuthHandler.kt index 21d8031..dbe398d 100644 --- a/sshlib/src/main/kotlin/org/connectbot/sshlib/AuthHandler.kt +++ b/sshlib/src/main/kotlin/org/connectbot/sshlib/AuthHandler.kt @@ -29,7 +29,9 @@ interface AuthHandler { * Called when the server reports which authentication methods are available. * Override to observe or log. */ - suspend fun onAuthMethodsAvailable(methods: Set) {} + suspend fun onAuthMethodsAvailable(methods: Set) { + // Optional notification hook; default handlers do not need to observe it. + } /** * Return public keys to probe. Empty list skips public key auth. @@ -76,7 +78,9 @@ interface AuthHandler { * Called when the server sends an authentication banner (SSH_MSG_USERAUTH_BANNER). * This is often used for out-of-band authentication instructions (e.g., a URL to visit). */ - suspend fun onBanner(message: String) {} + suspend fun onBanner(message: String) { + // Optional notification hook; default handlers may ignore banners. + } } /** diff --git a/sshlib/src/main/kotlin/org/connectbot/sshlib/HostKeyVerifier.kt b/sshlib/src/main/kotlin/org/connectbot/sshlib/HostKeyVerifier.kt index 3327f94..205df47 100644 --- a/sshlib/src/main/kotlin/org/connectbot/sshlib/HostKeyVerifier.kt +++ b/sshlib/src/main/kotlin/org/connectbot/sshlib/HostKeyVerifier.kt @@ -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. @@ -35,8 +35,13 @@ interface HostKeyVerifier { */ suspend fun verify(key: PublicKey): Boolean - suspend fun addKeys(keys: List) {} - suspend fun removeKeys(keys: List) {} + suspend fun addKeys(keys: List) { + // Optional persistence hook; read-only verifiers have nothing to store. + } + + suspend fun removeKeys(keys: List) { + // Optional persistence hook; read-only verifiers have nothing to remove. + } } /** diff --git a/sshlib/src/main/kotlin/org/connectbot/sshlib/SshClient.kt b/sshlib/src/main/kotlin/org/connectbot/sshlib/SshClient.kt index e77e759..3fa47d5 100644 --- a/sshlib/src/main/kotlin/org/connectbot/sshlib/SshClient.kt +++ b/sshlib/src/main/kotlin/org/connectbot/sshlib/SshClient.kt @@ -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. @@ -78,6 +78,10 @@ class SshClient private constructor( ) { companion object { private val logger = LoggerFactory.getLogger(SshClient::class.java) + private const val ERROR_NOT_CONNECTED = "Not connected" + private const val ERROR_NOT_CONNECTED_CONNECT_FIRST = "Not connected - call connect() first" + private const val ERROR_NOT_AUTHENTICATED = "Not authenticated" + private const val LOCALHOST = "127.0.0.1" /** * Create an SshClient for TCP connection to the specified host. @@ -198,8 +202,8 @@ class SshClient private constructor( suspend fun authenticatePassword(username: String, password: String): AuthResult { val conn = connection if (conn == null) { - logger.error("Not connected - call connect() first") - return AuthResult.Error("Not connected") + logger.error(ERROR_NOT_CONNECTED_CONNECT_FIRST) + return AuthResult.Error(ERROR_NOT_CONNECTED) } return try { @@ -234,8 +238,8 @@ class SshClient private constructor( ): AuthResult { val conn = connection if (conn == null) { - logger.error("Not connected - call connect() first") - return AuthResult.Error("Not connected") + logger.error(ERROR_NOT_CONNECTED_CONNECT_FIRST) + return AuthResult.Error(ERROR_NOT_CONNECTED) } return try { @@ -287,8 +291,8 @@ class SshClient private constructor( ): AuthResult { val conn = connection if (conn == null) { - logger.error("Not connected - call connect() first") - return AuthResult.Error("Not connected") + logger.error(ERROR_NOT_CONNECTED_CONNECT_FIRST) + return AuthResult.Error(ERROR_NOT_CONNECTED) } return try { @@ -325,8 +329,8 @@ class SshClient private constructor( suspend fun authenticate(username: String, handler: AuthHandler): AuthResult { val conn = connection if (conn == null) { - logger.error("Not connected - call connect() first") - return AuthResult.Error("Not connected") + logger.error(ERROR_NOT_CONNECTED_CONNECT_FIRST) + return AuthResult.Error(ERROR_NOT_CONNECTED) } return try { @@ -427,7 +431,7 @@ class SshClient private constructor( val conn = connection if (conn == null || !authenticated) { logger.error("Not authenticated - call connect() and authenticate first") - return SftpResult.IoError(IllegalStateException("Not authenticated")) + return SftpResult.IoError(IllegalStateException(ERROR_NOT_AUTHENTICATED)) } return try { @@ -463,7 +467,7 @@ class SshClient private constructor( ): PortForwarder? { val conn = connection if (conn == null || !authenticated) { - logger.error("Not authenticated") + logger.error(ERROR_NOT_AUTHENTICATED) return null } @@ -487,7 +491,7 @@ class SshClient private constructor( bindPort: Int, remoteHost: String, remotePort: Int, - ): PortForwarder? = localPortForward(InetSocketAddress("127.0.0.1", bindPort), remoteHost, remotePort) + ): PortForwarder? = localPortForward(InetSocketAddress(LOCALHOST, bindPort), remoteHost, remotePort) /** * Start remote port forwarding (RFC 4254 section 7.1). @@ -509,7 +513,7 @@ class SshClient private constructor( ): PortForwarder? { val conn = connection if (conn == null || !authenticated) { - logger.error("Not authenticated") + logger.error(ERROR_NOT_AUTHENTICATED) return null } @@ -537,7 +541,7 @@ class SshClient private constructor( ): PortForwarder? { val conn = connection if (conn == null || !authenticated) { - logger.error("Not authenticated") + logger.error(ERROR_NOT_AUTHENTICATED) return null } @@ -559,7 +563,7 @@ class SshClient private constructor( suspend fun dynamicPortForward( bindPort: Int, authenticator: Socks5Authenticator? = null, - ): PortForwarder? = dynamicPortForward(InetSocketAddress("127.0.0.1", bindPort), authenticator) + ): PortForwarder? = dynamicPortForward(InetSocketAddress(LOCALHOST, bindPort), authenticator) /** * Forward a pair of streams through an SSH direct-tcpip channel. @@ -580,12 +584,12 @@ class SshClient private constructor( writeChannel: ByteWriteChannel, remoteHost: String, remotePort: Int, - originAddr: String = "127.0.0.1", + originAddr: String = LOCALHOST, originPort: Int = 0, ): StreamForwarder? { val conn = connection if (conn == null || !authenticated) { - logger.error("Not authenticated") + logger.error(ERROR_NOT_AUTHENTICATED) return null } @@ -635,12 +639,12 @@ class SshClient private constructor( fun openDirectTcpipTransport( remoteHost: String, remotePort: Int, - originAddr: String = "127.0.0.1", + originAddr: String = LOCALHOST, originPort: Int = 0, ): TransportFactory? { val conn = connection if (conn == null || !authenticated) { - logger.error("Not authenticated") + logger.error(ERROR_NOT_AUTHENTICATED) return null } diff --git a/sshlib/src/main/kotlin/org/connectbot/sshlib/client/SshConnection.kt b/sshlib/src/main/kotlin/org/connectbot/sshlib/client/SshConnection.kt index dfe29a3..77fc3e0 100644 --- a/sshlib/src/main/kotlin/org/connectbot/sshlib/client/SshConnection.kt +++ b/sshlib/src/main/kotlin/org/connectbot/sshlib/client/SshConnection.kt @@ -168,17 +168,25 @@ class SshConnection( companion object { private val logger = LoggerFactory.getLogger(SshConnection::class.java) + private const val KEX_EXT_INFO_C = "ext-info-c" + private const val SERVICE_SSH_CONNECTION = "ssh-connection" + private const val KEY_TYPE_SSH_RSA = "ssh-rsa" + private const val METHOD_PUBLICKEY_HOSTBOUND = "publickey-hostbound-v00@openssh.com" + private const val CHANNEL_FORWARDED_TCPIP = "forwarded-tcpip" + private const val ERROR_SESSION_ID_NOT_ESTABLISHED = "Session ID not established" + private const val ERROR_CLIENT_PUBLIC_KEY_NOT_GENERATED = "Client public key not generated" + private const val ERROR_NO_KEX_ALGORITHM_INITIALIZED = "No KEX algorithm initialized" private fun stripExtInfoC(kexAlgorithms: String): String = kexAlgorithms.split(",") - .filter { it.isNotEmpty() && it != "ext-info-c" } + .filter { it.isNotEmpty() && it != KEX_EXT_INFO_C } .joinToString(",") private fun appendExtInfoC(kexAlgorithms: String): String { val algorithms = kexAlgorithms.split(",").filter { it.isNotEmpty() } - return if ("ext-info-c" in algorithms) { + return if (KEX_EXT_INFO_C in algorithms) { kexAlgorithms } else { - (algorithms + "ext-info-c").joinToString(",") + (algorithms + KEX_EXT_INFO_C).joinToString(",") } } @@ -448,7 +456,7 @@ class SshConnection( try { val req = SshMsgUserauthRequest().apply { setUserName(createAsciiString(username)) - setServiceName(createAsciiString("ssh-connection")) + setServiceName(createAsciiString(SERVICE_SSH_CONNECTION)) setMethodName(createAsciiString("password")) val passAuth = UserauthRequestPassword().apply { @@ -498,7 +506,7 @@ class SshConnection( try { val req = SshMsgUserauthRequest().apply { setUserName(createAsciiString(username)) - setServiceName(createAsciiString("ssh-connection")) + setServiceName(createAsciiString(SERVICE_SSH_CONNECTION)) setMethodName(createAsciiString("keyboard-interactive")) val kbdInteractive = UserauthRequestKeyboardInteractive().apply { @@ -584,11 +592,11 @@ class SshConnection( */ internal suspend fun authenticatePublicKey(username: String, privateKey: SshPrivateKey): PublicAuthResult { try { - val sid = sessionId ?: throw SshException("Session ID not established") + val sid = sessionId ?: throw SshException(ERROR_SESSION_ID_NOT_ESTABLISHED) val publicKeyBlob = SshPublicKeyEncoder.encode(privateKey.jcaKeyPair, privateKey.keyType) - val sigAlgorithmName = if (privateKey.keyType == "ssh-rsa") { + val sigAlgorithmName = if (privateKey.keyType == KEY_TYPE_SSH_RSA) { negotiateRsaAlgorithm() } else { privateKey.signatureAlgorithm @@ -600,9 +608,9 @@ class SshConnection( val useHostBound = serverAdvertisesHostBound && hostKeyBlob != null val signatureData = if (useHostBound && hostKeyBlob != null) { - buildHostBoundSignatureData(sid, username, "ssh-connection", sigAlgorithmName, publicKeyBlob, hostKeyBlob) + buildHostBoundSignatureData(sid, username, SERVICE_SSH_CONNECTION, sigAlgorithmName, publicKeyBlob, hostKeyBlob) } else { - buildSignatureData(sid, username, "ssh-connection", sigAlgorithmName, publicKeyBlob) + buildSignatureData(sid, username, SERVICE_SSH_CONNECTION, sigAlgorithmName, publicKeyBlob) } val signature = sigEntry.algorithm.sign( @@ -613,10 +621,10 @@ class SshConnection( val req = SshMsgUserauthRequest().apply { setUserName(createAsciiString(username)) - setServiceName(createAsciiString("ssh-connection")) + setServiceName(createAsciiString(SERVICE_SSH_CONNECTION)) if (useHostBound && hostKeyBlob != null) { - setMethodName(createAsciiString("publickey-hostbound-v00@openssh.com")) + setMethodName(createAsciiString(METHOD_PUBLICKEY_HOSTBOUND)) val pubkeyAuth = UserauthRequestPublickeyHostbound().apply { setHasSignature(1) setPublicKeyAlgorithmName(createAsciiString(sigAlgorithmName)) @@ -698,7 +706,7 @@ class SshConnection( setMessageType(byteArrayOf(50)) setUserName(createByteString(username.toByteArray(Charsets.UTF_8))) setServiceName(createByteString(serviceName.toByteArray(Charsets.US_ASCII))) - setMethodName(createByteString("publickey-hostbound-v00@openssh.com".toByteArray(Charsets.US_ASCII))) + setMethodName(createByteString(METHOD_PUBLICKEY_HOSTBOUND.toByteArray(Charsets.US_ASCII))) setHasSignature(byteArrayOf(1)) setPublicKeyAlgorithmName(createByteString(algorithmName.toByteArray(Charsets.US_ASCII))) setPublicKeyBlob(createByteString(publicKeyBlob)) @@ -805,7 +813,7 @@ class SshConnection( handler: AuthHandler, channel: Channel, ): InternalAuthResult { - val effectiveAlgorithmName = if (keyBlobAlgorithmName(key.publicKeyBlob) == "ssh-rsa") { + val effectiveAlgorithmName = if (keyBlobAlgorithmName(key.publicKeyBlob) == KEY_TYPE_SSH_RSA) { negotiateRsaAlgorithm() } else { key.algorithmName @@ -828,27 +836,27 @@ class SshConnection( handler: AuthHandler, channel: Channel, ): Boolean { - val sid = sessionId ?: throw SshException("Session ID not established") + val sid = sessionId ?: throw SshException(ERROR_SESSION_ID_NOT_ESTABLISHED) val hostKeyBlob = serverHostKeyBlob val useHostBound = serverAdvertisesHostBound && hostKeyBlob != null - val effectiveAlgorithmName = if (keyBlobAlgorithmName(key.publicKeyBlob) == "ssh-rsa") { + val effectiveAlgorithmName = if (keyBlobAlgorithmName(key.publicKeyBlob) == KEY_TYPE_SSH_RSA) { negotiateRsaAlgorithm() } else { key.algorithmName } val signatureData = if (useHostBound && hostKeyBlob != null) { - buildHostBoundSignatureData(sid, username, "ssh-connection", effectiveAlgorithmName, key.publicKeyBlob, hostKeyBlob) + buildHostBoundSignatureData(sid, username, SERVICE_SSH_CONNECTION, effectiveAlgorithmName, key.publicKeyBlob, hostKeyBlob) } else { - buildSignatureData(sid, username, "ssh-connection", effectiveAlgorithmName, key.publicKeyBlob) + buildSignatureData(sid, username, SERVICE_SSH_CONNECTION, effectiveAlgorithmName, key.publicKeyBlob) } val signingKey = if (effectiveAlgorithmName != key.algorithmName) key.copy(algorithmName = effectiveAlgorithmName) else key val signature = handler.onSignatureRequest(signingKey, signatureData) ?: return false if (useHostBound && hostKeyBlob != null) { - sendAuthRequest(username, "publickey-hostbound-v00@openssh.com") { + sendAuthRequest(username, METHOD_PUBLICKEY_HOSTBOUND) { val pubkeyAuth = UserauthRequestPublickeyHostbound().apply { setHasSignature(1) setPublicKeyAlgorithmName(createAsciiString(effectiveAlgorithmName)) @@ -984,7 +992,7 @@ class SshConnection( ) { val req = SshMsgUserauthRequest().apply { setUserName(createAsciiString(username)) - setServiceName(createAsciiString("ssh-connection")) + setServiceName(createAsciiString(SERVICE_SSH_CONNECTION)) setMethodName(createAsciiString(method)) configure() _check() @@ -1151,7 +1159,7 @@ class SshConnection( clientPublicKey = dh.generateClientKeys() val pubKey = clientPublicKey - ?: throw SshException("Client public key not generated") + ?: throw SshException(ERROR_CLIENT_PUBLIC_KEY_NOT_GENERATED) val msg = SshMsgKexdhInit().apply { setE(createMpint(pubKey)) @@ -1169,7 +1177,7 @@ class SshConnection( clientPublicKey = ecdh.generateClientKeys() val pubKey = clientPublicKey - ?: throw SshException("Client public key not generated") + ?: throw SshException(ERROR_CLIENT_PUBLIC_KEY_NOT_GENERATED) val msg = SshMsgKexEcdhInit().apply { setQC(createByteString(pubKey)) @@ -1267,11 +1275,11 @@ class SshConnection( } private suspend fun completeKex(serverHostKey: ByteArray, serverPublicKey: ByteArray, signature: ByteArray) { - val kexAlg = kex ?: throw SshException("No KEX algorithm initialized") + val kexAlg = kex ?: throw SshException(ERROR_NO_KEX_ALGORITHM_INITIALIZED) val sv = serverVersion ?: throw SshException("Server version not received") val cki = clientKexInit ?: throw SshException("Client KEX_INIT not sent") val ski = serverKexInit ?: throw SshException("Server KEX_INIT not received") - val cpk = clientPublicKey ?: throw SshException("Client public key not generated") + val cpk = clientPublicKey ?: throw SshException(ERROR_CLIENT_PUBLIC_KEY_NOT_GENERATED) sharedSecret = kexAlg.computeSharedSecret(serverPublicKey) @@ -1493,8 +1501,8 @@ class SshConnection( val secret = sharedSecret ?: throw SshException("Shared secret not computed") val hash = exchangeHash ?: throw SshException("Exchange hash not computed") - val sid = sessionId ?: throw SshException("Session ID not established") - val kexAlg = kex ?: throw SshException("No KEX algorithm initialized") + val sid = sessionId ?: throw SshException(ERROR_SESSION_ID_NOT_ESTABLISHED) + val kexAlg = kex ?: throw SshException(ERROR_NO_KEX_ALGORITHM_INITIALIZED) val keyDerivation = KeyDerivation( secret, @@ -1552,8 +1560,8 @@ class SshConnection( val secret = sharedSecret ?: throw SshException("Shared secret not computed") val hash = exchangeHash ?: throw SshException("Exchange hash not computed") - val sid = sessionId ?: throw SshException("Session ID not established") - val kexAlg = kex ?: throw SshException("No KEX algorithm initialized") + val sid = sessionId ?: throw SshException(ERROR_SESSION_ID_NOT_ESTABLISHED) + val kexAlg = kex ?: throw SshException(ERROR_NO_KEX_ALGORITHM_INITIALIZED) val keyDerivation = KeyDerivation( secret, @@ -1732,7 +1740,7 @@ class SshConnection( logger.info("Accepted agent channel: local=$localChannelNumber, remote=$senderChannel") } - "forwarded-tcpip" -> { + CHANNEL_FORWARDED_TCPIP -> { handleForwardedTcpip(msg, senderChannel, initialWindow, maxPacketSize) } @@ -1754,8 +1762,8 @@ class SshConnection( try { val channelData = msg.channelSpecificData() if (channelData !is ChannelOpenForwardedTcpip) { - logger.warn("Failed to parse forwarded-tcpip channel data") - rejectChannelOpen(senderChannel, "forwarded-tcpip") + logger.warn("Failed to parse $CHANNEL_FORWARDED_TCPIP channel data") + rejectChannelOpen(senderChannel, CHANNEL_FORWARDED_TCPIP) return } @@ -1768,14 +1776,14 @@ class SshConnection( val handler = remoteForwarders[key] if (handler == null) { logger.warn("No remote forwarder registered for $key") - rejectChannelOpen(senderChannel, "forwarded-tcpip") + rejectChannelOpen(senderChannel, CHANNEL_FORWARDED_TCPIP) return } handler(connectedAddr, connectedPort, originAddr, originPort, senderChannel, initialWindow, maxPacketSize) } catch (e: Exception) { - logger.error("Failed to handle forwarded-tcpip", e) - rejectChannelOpen(senderChannel, "forwarded-tcpip") + logger.error("Failed to handle $CHANNEL_FORWARDED_TCPIP", e) + rejectChannelOpen(senderChannel, CHANNEL_FORWARDED_TCPIP) } } diff --git a/sshlib/src/main/kotlin/org/connectbot/sshlib/crypto/Algorithms.kt b/sshlib/src/main/kotlin/org/connectbot/sshlib/crypto/Algorithms.kt index 548ba16..d7c8c0b 100644 --- a/sshlib/src/main/kotlin/org/connectbot/sshlib/crypto/Algorithms.kt +++ b/sshlib/src/main/kotlin/org/connectbot/sshlib/crypto/Algorithms.kt @@ -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. @@ -17,6 +17,10 @@ package org.connectbot.sshlib.crypto +private const val HASH_SHA256 = "SHA-256" +private const val HASH_SHA512 = "SHA-512" +private const val KEY_TYPE_SSH_RSA = "ssh-rsa" + internal sealed interface EncryptionInstance { data class Cipher(val cipher: PacketCipher) : EncryptionInstance data class Aead(val aead: PacketAead) : EncryptionInstance @@ -180,21 +184,21 @@ internal enum class KexEntry( ) { MLKEM768X25519_SHA256( "mlkem768x25519-sha256", - "SHA-256", + HASH_SHA256, KexType.ECDH, true, { MlKemHybridKeyExchange() }, ), CURVE25519_SHA256( "curve25519-sha256", - "SHA-256", + HASH_SHA256, KexType.ECDH, false, { Curve25519KeyExchange() }, ), ECDH_SHA2_NISTP256( "ecdh-sha2-nistp256", - "SHA-256", + HASH_SHA256, KexType.ECDH, false, { EcdhKeyExchange("nistp256") }, @@ -208,38 +212,38 @@ internal enum class KexEntry( ), ECDH_SHA2_NISTP521( "ecdh-sha2-nistp521", - "SHA-512", + HASH_SHA512, KexType.ECDH, false, { EcdhKeyExchange("nistp521") }, ), DH_GROUP18_SHA512( "diffie-hellman-group18-sha512", - "SHA-512", + HASH_SHA512, KexType.DH, false, - { DiffieHellman("SHA-512", DhGroups.GROUP18_P, DhGroups.GENERATOR) }, + { DiffieHellman(HASH_SHA512, DhGroups.GROUP18_P, DhGroups.GENERATOR) }, ), DH_GROUP16_SHA512( "diffie-hellman-group16-sha512", - "SHA-512", + HASH_SHA512, KexType.DH, false, - { DiffieHellman("SHA-512", DhGroups.GROUP16_P, DhGroups.GENERATOR) }, + { DiffieHellman(HASH_SHA512, DhGroups.GROUP16_P, DhGroups.GENERATOR) }, ), DH_GROUP_EXCHANGE_SHA256( "diffie-hellman-group-exchange-sha256", - "SHA-256", + HASH_SHA256, KexType.DH_GEX, false, - { DiffieHellmanGroupExchange("SHA-256") }, + { DiffieHellmanGroupExchange(HASH_SHA256) }, ), DH_GROUP14_SHA256( "diffie-hellman-group14-sha256", - "SHA-256", + HASH_SHA256, KexType.DH, false, - { DiffieHellman("SHA-256", DhGroups.GROUP14_P, DhGroups.GENERATOR) }, + { DiffieHellman(HASH_SHA256, DhGroups.GROUP14_P, DhGroups.GENERATOR) }, ), DH_GROUP14_SHA1( "diffie-hellman-group14-sha1", @@ -287,7 +291,7 @@ internal enum class SignatureEntry( ECDSA_SHA2_NISTP521("ecdsa-sha2-nistp521", EcdsaSignatureAlgorithm), RSA_SHA2_256("rsa-sha2-256", RsaSignatureAlgorithm), RSA_SHA2_512("rsa-sha2-512", RsaSignatureAlgorithm), - SSH_RSA("ssh-rsa", RsaSignatureAlgorithm), + SSH_RSA(KEY_TYPE_SSH_RSA, RsaSignatureAlgorithm), ; companion object { @@ -297,7 +301,7 @@ internal enum class SignatureEntry( fun fromSshName(name: String): SignatureEntry? = entries.firstOrNull { it.sshName == name } - private val rsaPreferenceOrder = listOf("rsa-sha2-512", "rsa-sha2-256", "ssh-rsa") + private val rsaPreferenceOrder = listOf("rsa-sha2-512", "rsa-sha2-256", KEY_TYPE_SSH_RSA) /** * Picks the best RSA signing algorithm given the server's advertised list. @@ -305,8 +309,8 @@ internal enum class SignatureEntry( * or if no supported RSA algorithms were advertised. */ fun negotiateRsaAlgorithm(serverSigAlgs: Set?): String { - if (serverSigAlgs == null) return "ssh-rsa" - return rsaPreferenceOrder.firstOrNull { it in serverSigAlgs } ?: "ssh-rsa" + if (serverSigAlgs == null) return KEY_TYPE_SSH_RSA + return rsaPreferenceOrder.firstOrNull { it in serverSigAlgs } ?: KEY_TYPE_SSH_RSA } } } diff --git a/sshlib/src/main/kotlin/org/connectbot/sshlib/crypto/DiffieHellmanGroupExchange.kt b/sshlib/src/main/kotlin/org/connectbot/sshlib/crypto/DiffieHellmanGroupExchange.kt index c4711ba..10053e2 100644 --- a/sshlib/src/main/kotlin/org/connectbot/sshlib/crypto/DiffieHellmanGroupExchange.kt +++ b/sshlib/src/main/kotlin/org/connectbot/sshlib/crypto/DiffieHellmanGroupExchange.kt @@ -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. @@ -32,6 +32,7 @@ import java.security.SecureRandom internal class DiffieHellmanGroupExchange(override val hashAlgorithm: String) : KexAlgorithm { companion object { private val secureRandom = SecureRandom() + private const val ERROR_GROUP_NOT_SET = "DH group not set; call setGroup() first" } val min = 2048 @@ -48,8 +49,8 @@ internal class DiffieHellmanGroupExchange(override val hashAlgorithm: String) : } override fun generateClientKeys(): ByteArray { - val p = this.p ?: throw SshException("DH group not set; call setGroup() first") - val g = this.g ?: throw SshException("DH group not set; call setGroup() first") + val p = this.p ?: throw SshException(ERROR_GROUP_NOT_SET) + val g = this.g ?: throw SshException(ERROR_GROUP_NOT_SET) val x = BigInteger(p.bitLength(), secureRandom).mod(p - BigInteger.ONE) + BigInteger.ONE privateKey = x @@ -60,7 +61,7 @@ internal class DiffieHellmanGroupExchange(override val hashAlgorithm: String) : override fun computeSharedSecret(serverPublicKey: ByteArray): ByteArray { val x = privateKey ?: throw SshException("Client keys not generated; call generateClientKeys() first") - val p = this.p ?: throw SshException("DH group not set; call setGroup() first") + val p = this.p ?: throw SshException(ERROR_GROUP_NOT_SET) val f = BigInteger(1, serverPublicKey) if (f <= BigInteger.ONE || f >= p - BigInteger.ONE) { @@ -89,8 +90,8 @@ internal class DiffieHellmanGroupExchange(override val hashAlgorithm: String) : serverPublicKey: ByteArray, sharedSecret: ByteArray, ): ByteArray { - val p = this.p ?: throw SshException("DH group not set; call setGroup() first") - val g = this.g ?: throw SshException("DH group not set; call setGroup() first") + val p = this.p ?: throw SshException(ERROR_GROUP_NOT_SET) + val g = this.g ?: throw SshException(ERROR_GROUP_NOT_SET) val transcript = ByteArrayOutputStream() diff --git a/sshlib/src/main/kotlin/org/connectbot/sshlib/crypto/JavaMlKemProvider.kt b/sshlib/src/main/kotlin/org/connectbot/sshlib/crypto/JavaMlKemProvider.kt index f46e589..596e61c 100644 --- a/sshlib/src/main/kotlin/org/connectbot/sshlib/crypto/JavaMlKemProvider.kt +++ b/sshlib/src/main/kotlin/org/connectbot/sshlib/crypto/JavaMlKemProvider.kt @@ -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. @@ -32,6 +32,8 @@ import javax.crypto.SecretKey internal class JavaMlKemProvider : MlKemProvider { companion object { private const val MLKEM768_PUBLIC_KEY_SIZE = 1184 + private const val KEM_CLASS_NAME = "javax.crypto.KEM" + private const val ML_KEM_ALGORITHM = "ML-KEM" // X.509 wrapper for ML-KEM-768 public keys private val X509_PREFIX = byteArrayOf( @@ -72,9 +74,9 @@ internal class JavaMlKemProvider : MlKemProvider { init { try { - val kemClass = Class.forName("javax.crypto.KEM") + val kemClass = Class.forName(KEM_CLASS_NAME) val getInstance = kemClass.getMethod("getInstance", String::class.java) - kemInstance = getInstance.invoke(null, "ML-KEM") + kemInstance = getInstance.invoke(null, ML_KEM_ALGORITHM) } catch (e: Exception) { throw IOException("Failed to initialize Java KEM API", e) } @@ -98,10 +100,10 @@ internal class JavaMlKemProvider : MlKemProvider { override fun encapsulate(publicKey: ByteArray): MlKemEncapsulationResult { try { val x509Encoded = wrapRawMlKemPublicKey(publicKey) - val kf = KeyFactory.getInstance("ML-KEM") + val kf = KeyFactory.getInstance(ML_KEM_ALGORITHM) val pubKey = kf.generatePublic(X509EncodedKeySpec(x509Encoded)) - val kemClass = Class.forName("javax.crypto.KEM") + val kemClass = Class.forName(KEM_CLASS_NAME) val newEncapsulator = kemClass.getMethod("newEncapsulator", PublicKey::class.java) val encapsulator = newEncapsulator.invoke(kemInstance, pubKey) @@ -125,10 +127,10 @@ internal class JavaMlKemProvider : MlKemProvider { override fun decapsulate(privateKey: ByteArray, ciphertext: ByteArray): ByteArray { try { - val kf = KeyFactory.getInstance("ML-KEM") + val kf = KeyFactory.getInstance(ML_KEM_ALGORITHM) val privKey: PrivateKey = kf.generatePrivate(PKCS8EncodedKeySpec(privateKey)) - val kemClass = Class.forName("javax.crypto.KEM") + val kemClass = Class.forName(KEM_CLASS_NAME) val newDecapsulator = kemClass.getMethod("newDecapsulator", PrivateKey::class.java) val decapsulator = newDecapsulator.invoke(kemInstance, privKey) diff --git a/sshlib/src/main/kotlin/org/connectbot/sshlib/crypto/KeyDecryption.kt b/sshlib/src/main/kotlin/org/connectbot/sshlib/crypto/KeyDecryption.kt index 36ee328..cf267f6 100644 --- a/sshlib/src/main/kotlin/org/connectbot/sshlib/crypto/KeyDecryption.kt +++ b/sshlib/src/main/kotlin/org/connectbot/sshlib/crypto/KeyDecryption.kt @@ -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. @@ -24,6 +24,9 @@ import javax.crypto.Cipher import javax.crypto.spec.IvParameterSpec import javax.crypto.spec.SecretKeySpec +private const val AES_CBC_NO_PADDING = "AES/CBC/NoPadding" +private const val AES_CTR_NO_PADDING = "AES/CTR/NoPadding" + internal object KeyDecryption { fun decryptOpenSsh( @@ -34,12 +37,12 @@ internal object KeyDecryption { cipherName: String, ): ByteArray { val (jcaCipher, keySize, ivSize) = when (cipherName.lowercase()) { - "aes256-ctr" -> Triple("AES/CTR/NoPadding", 32, 16) - "aes256-cbc" -> Triple("AES/CBC/NoPadding", 32, 16) - "aes128-ctr" -> Triple("AES/CTR/NoPadding", 16, 16) - "aes128-cbc" -> Triple("AES/CBC/NoPadding", 16, 16) - "aes192-ctr" -> Triple("AES/CTR/NoPadding", 24, 16) - "aes192-cbc" -> Triple("AES/CBC/NoPadding", 24, 16) + "aes256-ctr" -> Triple(AES_CTR_NO_PADDING, 32, 16) + "aes256-cbc" -> Triple(AES_CBC_NO_PADDING, 32, 16) + "aes128-ctr" -> Triple(AES_CTR_NO_PADDING, 16, 16) + "aes128-cbc" -> Triple(AES_CBC_NO_PADDING, 16, 16) + "aes192-ctr" -> Triple(AES_CTR_NO_PADDING, 24, 16) + "aes192-cbc" -> Triple(AES_CBC_NO_PADDING, 24, 16) else -> throw SshException("Unsupported OpenSSH cipher: $cipherName") } @@ -63,9 +66,9 @@ internal object KeyDecryption { val (jcaCipher, keySize) = when (cipherName.uppercase()) { "DES-EDE3-CBC" -> "DESede/CBC/NoPadding" to 24 "DES-CBC" -> "DES/CBC/NoPadding" to 8 - "AES-128-CBC" -> "AES/CBC/NoPadding" to 16 - "AES-192-CBC" -> "AES/CBC/NoPadding" to 24 - "AES-256-CBC" -> "AES/CBC/NoPadding" to 32 + "AES-128-CBC" -> AES_CBC_NO_PADDING to 16 + "AES-192-CBC" -> AES_CBC_NO_PADDING to 24 + "AES-256-CBC" -> AES_CBC_NO_PADDING to 32 else -> throw SshException("Unsupported PEM cipher: $cipherName") } diff --git a/sshlib/src/main/kotlin/org/connectbot/sshlib/crypto/KeyEncryption.kt b/sshlib/src/main/kotlin/org/connectbot/sshlib/crypto/KeyEncryption.kt index 8f87ad9..fa7f195 100644 --- a/sshlib/src/main/kotlin/org/connectbot/sshlib/crypto/KeyEncryption.kt +++ b/sshlib/src/main/kotlin/org/connectbot/sshlib/crypto/KeyEncryption.kt @@ -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. @@ -23,6 +23,9 @@ import javax.crypto.Cipher import javax.crypto.spec.IvParameterSpec import javax.crypto.spec.SecretKeySpec +private const val AES_CBC_NO_PADDING = "AES/CBC/NoPadding" +private const val AES_CTR_NO_PADDING = "AES/CTR/NoPadding" + internal object KeyEncryption { fun encryptPem( @@ -34,9 +37,9 @@ internal object KeyEncryption { val (jcaCipher, keySize) = when (cipherName.uppercase()) { "DES-EDE3-CBC" -> "DESede/CBC/NoPadding" to 24 "DES-CBC" -> "DES/CBC/NoPadding" to 8 - "AES-128-CBC" -> "AES/CBC/NoPadding" to 16 - "AES-192-CBC" -> "AES/CBC/NoPadding" to 24 - "AES-256-CBC" -> "AES/CBC/NoPadding" to 32 + "AES-128-CBC" -> AES_CBC_NO_PADDING to 16 + "AES-192-CBC" -> AES_CBC_NO_PADDING to 24 + "AES-256-CBC" -> AES_CBC_NO_PADDING to 32 else -> throw SshException("Unsupported PEM cipher: $cipherName") } @@ -66,10 +69,10 @@ internal object KeyEncryption { cipherName: String, ): ByteArray { val (jcaCipher, keySize, ivSize) = when (cipherName.lowercase()) { - "aes256-ctr" -> Triple("AES/CTR/NoPadding", 32, 16) - "aes256-cbc" -> Triple("AES/CBC/NoPadding", 32, 16) - "aes128-ctr" -> Triple("AES/CTR/NoPadding", 16, 16) - "aes128-cbc" -> Triple("AES/CBC/NoPadding", 16, 16) + "aes256-ctr" -> Triple(AES_CTR_NO_PADDING, 32, 16) + "aes256-cbc" -> Triple(AES_CBC_NO_PADDING, 32, 16) + "aes128-ctr" -> Triple(AES_CTR_NO_PADDING, 16, 16) + "aes128-cbc" -> Triple(AES_CBC_NO_PADDING, 16, 16) else -> throw SshException("Unsupported OpenSSH cipher: $cipherName") } diff --git a/sshlib/src/main/kotlin/org/connectbot/sshlib/crypto/KeyTypes.kt b/sshlib/src/main/kotlin/org/connectbot/sshlib/crypto/KeyTypes.kt index ff9d551..8902b4a 100644 --- a/sshlib/src/main/kotlin/org/connectbot/sshlib/crypto/KeyTypes.kt +++ b/sshlib/src/main/kotlin/org/connectbot/sshlib/crypto/KeyTypes.kt @@ -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. @@ -22,13 +22,15 @@ import java.security.KeyPair import java.security.PublicKey import java.security.interfaces.ECPublicKey +private const val KEY_TYPE_ED25519 = "ssh-ed25519" + internal fun inferKeyType(publicKey: PublicKey): String = when (publicKey.algorithm) { - "Ed25519" -> "ssh-ed25519" + "Ed25519" -> KEY_TYPE_ED25519 "Ed448" -> "ssh-ed448" "EdDSA" -> { - if (publicKey.encoded.size <= 44) "ssh-ed25519" else "ssh-ed448" + if (publicKey.encoded.size <= 44) KEY_TYPE_ED25519 else "ssh-ed448" } "EC", "ECDSA" -> { @@ -45,7 +47,7 @@ internal fun inferKeyType(publicKey: PublicKey): String = when (publicKey.algori else -> { if (isEd25519Key(publicKey)) { - "ssh-ed25519" + KEY_TYPE_ED25519 } else { throw SshException("Unsupported key type: ${publicKey.algorithm}") } diff --git a/sshlib/src/main/kotlin/org/connectbot/sshlib/crypto/PacketAead.kt b/sshlib/src/main/kotlin/org/connectbot/sshlib/crypto/PacketAead.kt index 2984214..7bdbee8 100644 --- a/sshlib/src/main/kotlin/org/connectbot/sshlib/crypto/PacketAead.kt +++ b/sshlib/src/main/kotlin/org/connectbot/sshlib/crypto/PacketAead.kt @@ -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. @@ -29,7 +29,10 @@ internal data class AeadResult(val ciphertext: ByteArray, val tag: ByteArray) * as AAD (authenticated but not encrypted). */ internal interface PacketAead : Destroyable { - override fun destroy() {} + override fun destroy() { + // Stateless implementations have no key material to clear. + } + override fun isDestroyed() = false val tagLength: Int diff --git a/sshlib/src/main/kotlin/org/connectbot/sshlib/crypto/PacketCipher.kt b/sshlib/src/main/kotlin/org/connectbot/sshlib/crypto/PacketCipher.kt index bec664c..5ec0643 100644 --- a/sshlib/src/main/kotlin/org/connectbot/sshlib/crypto/PacketCipher.kt +++ b/sshlib/src/main/kotlin/org/connectbot/sshlib/crypto/PacketCipher.kt @@ -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. @@ -23,7 +23,10 @@ import javax.security.auth.Destroyable * Interface for SSH packet encryption/decryption. */ internal interface PacketCipher : Destroyable { - override fun destroy() {} + override fun destroy() { + // Stateless implementations have no key material to clear. + } + override fun isDestroyed() = false /** diff --git a/sshlib/src/main/kotlin/org/connectbot/sshlib/crypto/PacketMac.kt b/sshlib/src/main/kotlin/org/connectbot/sshlib/crypto/PacketMac.kt index 3d8ce0c..6de80c7 100644 --- a/sshlib/src/main/kotlin/org/connectbot/sshlib/crypto/PacketMac.kt +++ b/sshlib/src/main/kotlin/org/connectbot/sshlib/crypto/PacketMac.kt @@ -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. @@ -23,7 +23,10 @@ import javax.security.auth.Destroyable * Interface for SSH packet message authentication codes (MAC). */ internal interface PacketMac : Destroyable { - override fun destroy() {} + override fun destroy() { + // Stateless implementations have no key material to clear. + } + override fun isDestroyed() = false /** diff --git a/sshlib/src/main/kotlin/org/connectbot/sshlib/crypto/PrivateKeyReader.kt b/sshlib/src/main/kotlin/org/connectbot/sshlib/crypto/PrivateKeyReader.kt index 3933891..9153480 100644 --- a/sshlib/src/main/kotlin/org/connectbot/sshlib/crypto/PrivateKeyReader.kt +++ b/sshlib/src/main/kotlin/org/connectbot/sshlib/crypto/PrivateKeyReader.kt @@ -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. @@ -19,6 +19,9 @@ package org.connectbot.sshlib.crypto import org.connectbot.sshlib.SshException +private const val OPENSSH_PRIVATE_KEY_BEGIN = "-----BEGIN OPENSSH PRIVATE KEY-----" +private const val OPENSSH_PRIVATE_KEY_END = "-----END OPENSSH PRIVATE KEY-----" + internal object PrivateKeyReader { fun read(keyData: ByteArray, passphrase: String? = null): SshPrivateKey = read(String(keyData, Charsets.UTF_8), passphrase) @@ -26,8 +29,8 @@ internal object PrivateKeyReader { fun read(keyData: String, passphrase: String? = null): SshPrivateKey { val trimmed = keyData.trim() return when { - trimmed.startsWith("-----BEGIN OPENSSH PRIVATE KEY-----") -> { - val base64 = extractBase64(trimmed, "-----BEGIN OPENSSH PRIVATE KEY-----", "-----END OPENSSH PRIVATE KEY-----") + trimmed.startsWith(OPENSSH_PRIVATE_KEY_BEGIN) -> { + val base64 = extractBase64(trimmed, OPENSSH_PRIVATE_KEY_BEGIN, OPENSSH_PRIVATE_KEY_END) val data = Base64Compat.decode(base64) OpenSshKeyReader.read(data, passphrase) } @@ -47,8 +50,8 @@ internal object PrivateKeyReader { fun isEncrypted(keyData: String): Boolean { val trimmed = keyData.trim() return when { - trimmed.startsWith("-----BEGIN OPENSSH PRIVATE KEY-----") -> { - val base64 = extractBase64(trimmed, "-----BEGIN OPENSSH PRIVATE KEY-----", "-----END OPENSSH PRIVATE KEY-----") + trimmed.startsWith(OPENSSH_PRIVATE_KEY_BEGIN) -> { + val base64 = extractBase64(trimmed, OPENSSH_PRIVATE_KEY_BEGIN, OPENSSH_PRIVATE_KEY_END) val data = Base64Compat.decode(base64) OpenSshKeyReader.isEncrypted(data) } From cd864ad7fae26b96609da1d0fa2422da5be001cb Mon Sep 17 00:00:00 2001 From: Kenny Root Date: Wed, 20 May 2026 18:39:20 -0700 Subject: [PATCH 2/2] chore: fix kotlin compilation warnings --- .../connectbot/sshlib/client/SshConnection.kt | 22 +++++++++---------- .../sshlib/crypto/ed25519/Ed25519Provider.kt | 22 +++++++++++++++++-- 2 files changed, 30 insertions(+), 14 deletions(-) diff --git a/sshlib/src/main/kotlin/org/connectbot/sshlib/client/SshConnection.kt b/sshlib/src/main/kotlin/org/connectbot/sshlib/client/SshConnection.kt index 77fc3e0..43dcdeb 100644 --- a/sshlib/src/main/kotlin/org/connectbot/sshlib/client/SshConnection.kt +++ b/sshlib/src/main/kotlin/org/connectbot/sshlib/client/SshConnection.kt @@ -604,11 +604,10 @@ class SshConnection( val sigEntry = SignatureEntry.fromSshName(sigAlgorithmName) ?: throw SshException("Unknown signature algorithm: $sigAlgorithmName") - val hostKeyBlob = serverHostKeyBlob - val useHostBound = serverAdvertisesHostBound && hostKeyBlob != null + val hostBoundKeyBlob = serverHostKeyBlob.takeIf { serverAdvertisesHostBound } - val signatureData = if (useHostBound && hostKeyBlob != null) { - buildHostBoundSignatureData(sid, username, SERVICE_SSH_CONNECTION, sigAlgorithmName, publicKeyBlob, hostKeyBlob) + val signatureData = if (hostBoundKeyBlob != null) { + buildHostBoundSignatureData(sid, username, SERVICE_SSH_CONNECTION, sigAlgorithmName, publicKeyBlob, hostBoundKeyBlob) } else { buildSignatureData(sid, username, SERVICE_SSH_CONNECTION, sigAlgorithmName, publicKeyBlob) } @@ -623,13 +622,13 @@ class SshConnection( setUserName(createAsciiString(username)) setServiceName(createAsciiString(SERVICE_SSH_CONNECTION)) - if (useHostBound && hostKeyBlob != null) { + if (hostBoundKeyBlob != null) { setMethodName(createAsciiString(METHOD_PUBLICKEY_HOSTBOUND)) val pubkeyAuth = UserauthRequestPublickeyHostbound().apply { setHasSignature(1) setPublicKeyAlgorithmName(createAsciiString(sigAlgorithmName)) setPublicKeyBlob(createByteString(publicKeyBlob)) - setServerHostKey(createByteString(hostKeyBlob)) + setServerHostKey(createByteString(hostBoundKeyBlob)) setSignature(createByteString(signature)) _check() } @@ -837,8 +836,7 @@ class SshConnection( channel: Channel, ): Boolean { val sid = sessionId ?: throw SshException(ERROR_SESSION_ID_NOT_ESTABLISHED) - val hostKeyBlob = serverHostKeyBlob - val useHostBound = serverAdvertisesHostBound && hostKeyBlob != null + val hostBoundKeyBlob = serverHostKeyBlob.takeIf { serverAdvertisesHostBound } val effectiveAlgorithmName = if (keyBlobAlgorithmName(key.publicKeyBlob) == KEY_TYPE_SSH_RSA) { negotiateRsaAlgorithm() @@ -846,8 +844,8 @@ class SshConnection( key.algorithmName } - val signatureData = if (useHostBound && hostKeyBlob != null) { - buildHostBoundSignatureData(sid, username, SERVICE_SSH_CONNECTION, effectiveAlgorithmName, key.publicKeyBlob, hostKeyBlob) + val signatureData = if (hostBoundKeyBlob != null) { + buildHostBoundSignatureData(sid, username, SERVICE_SSH_CONNECTION, effectiveAlgorithmName, key.publicKeyBlob, hostBoundKeyBlob) } else { buildSignatureData(sid, username, SERVICE_SSH_CONNECTION, effectiveAlgorithmName, key.publicKeyBlob) } @@ -855,13 +853,13 @@ class SshConnection( val signingKey = if (effectiveAlgorithmName != key.algorithmName) key.copy(algorithmName = effectiveAlgorithmName) else key val signature = handler.onSignatureRequest(signingKey, signatureData) ?: return false - if (useHostBound && hostKeyBlob != null) { + if (hostBoundKeyBlob != null) { sendAuthRequest(username, METHOD_PUBLICKEY_HOSTBOUND) { val pubkeyAuth = UserauthRequestPublickeyHostbound().apply { setHasSignature(1) setPublicKeyAlgorithmName(createAsciiString(effectiveAlgorithmName)) setPublicKeyBlob(createByteString(key.publicKeyBlob)) - setServerHostKey(createByteString(hostKeyBlob)) + setServerHostKey(createByteString(hostBoundKeyBlob)) setSignature(createByteString(signature)) _check() } diff --git a/sshlib/src/main/kotlin/org/connectbot/sshlib/crypto/ed25519/Ed25519Provider.kt b/sshlib/src/main/kotlin/org/connectbot/sshlib/crypto/ed25519/Ed25519Provider.kt index eaefe87..eec071d 100644 --- a/sshlib/src/main/kotlin/org/connectbot/sshlib/crypto/ed25519/Ed25519Provider.kt +++ b/sshlib/src/main/kotlin/org/connectbot/sshlib/crypto/ed25519/Ed25519Provider.kt @@ -1,3 +1,22 @@ +/* + * 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. + */ + +@file:Suppress("DEPRECATION", "removal") + /* * ConnectBot SSH Library * Copyright 2025 Kenny Root @@ -22,10 +41,9 @@ import java.security.PrivilegedAction import java.security.Provider import java.security.Security -@Suppress("DEPRECATION", "removal") // Android only has the Provider(String, double, String) constructor +// Android only has the Provider(String, double, String) constructor. internal class Ed25519Provider : Provider(NAME, 1.0, "ConnectBot Ed25519 JCA Provider") { init { - @Suppress("DEPRECATION", "removal") AccessController.doPrivileged( PrivilegedAction { setup()