From 88e36fc3319fc6a5e412e1820a4421ec22e7d760 Mon Sep 17 00:00:00 2001 From: Alexander Irion Date: Wed, 12 Mar 2025 14:04:00 +0100 Subject: [PATCH 01/32] Add parameter disableHostkeyVerification When disableHostkeyVerification is set, the verification which is time consuming in debug mode is omitted. --- lib/src/ssh_client.dart | 5 +++++ lib/src/ssh_transport.dart | 18 ++++++++++++------ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/lib/src/ssh_client.dart b/lib/src/ssh_client.dart index d3f8b74..9290a1f 100644 --- a/lib/src/ssh_client.dart +++ b/lib/src/ssh_client.dart @@ -130,6 +130,9 @@ class SSHClient { /// extension. May not be called if the server does not support the extension. // final SSHHostKeysHandler? onHostKeys; + /// Allow to disable hostkey verification, which can be slow in debug mode. + final bool disableHostkeyVerification; + /// A [Future] that completes when the transport is closed, or when an error /// occurs. After this [Future] completes, [isClosed] will be true and no more /// data can be sent or received. @@ -152,6 +155,7 @@ class SSHClient { this.onUserauthBanner, this.onAuthenticated, this.keepAliveInterval = const Duration(seconds: 10), + this.disableHostkeyVerification = false, }) { _transport = SSHTransport( socket, @@ -162,6 +166,7 @@ class SSHClient { onVerifyHostKey: onVerifyHostKey, onReady: _handleTransportReady, onPacket: _handlePacket, + disableHostkeyVerification: disableHostkeyVerification, ); _transport.done.then( diff --git a/lib/src/ssh_transport.dart b/lib/src/ssh_transport.dart index 03540b6..3da0e79 100644 --- a/lib/src/ssh_transport.dart +++ b/lib/src/ssh_transport.dart @@ -72,6 +72,8 @@ class SSHTransport { /// Function called when a packet is received. final SSHPacketHandler? onPacket; + final bool disableHostkeyVerification; + /// A [Future] that completes when the transport is closed, or when an error /// occurs. After this [Future] completes, [isClosed] will be true and no /// more data can be sent or received. @@ -94,6 +96,7 @@ class SSHTransport { this.onVerifyHostKey, this.onReady, this.onPacket, + this.disableHostkeyVerification = false, }) { _initSocket(); _startHandshake(); @@ -803,12 +806,15 @@ class SSHTransport { sharedSecret: sharedSecret, ); - final verified = _verifyHostkey( - keyBytes: hostkey, - signatureBytes: hostSignature, - exchangeHash: exchangeHash, - ); - if (!verified) throw SSHHostkeyError('Signature verification failed'); + if (!disableHostkeyVerification) + { + final verified = _verifyHostkey( + keyBytes: hostkey, + signatureBytes: hostSignature, + exchangeHash: exchangeHash, + ); + if (!verified) throw SSHHostkeyError('Signature verification failed'); + } _exchangeHash = exchangeHash; _sessionId ??= exchangeHash; From e32b1326cdbbb35bfcf3b7185fdcc258a55516e9 Mon Sep 17 00:00:00 2001 From: Alexander Irion Date: Wed, 26 Mar 2025 16:28:26 +0100 Subject: [PATCH 02/32] Catch exceptions when sshd terminated When a sftp session is created with Client:sftp and on client side the sshd is killed it caused unhandled exceptions. --- lib/src/ssh_channel.dart | 10 +++++++--- lib/src/ssh_client.dart | 7 ++++++- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/lib/src/ssh_channel.dart b/lib/src/ssh_channel.dart index fb5daa7..f5defbf 100644 --- a/lib/src/ssh_channel.dart +++ b/lib/src/ssh_channel.dart @@ -297,7 +297,12 @@ class SSHChannelController { if (_done.isCompleted) return; if (_hasSentClose) return; _hasSentClose = true; - sendMessage(SSH_Message_Channel_Close(recipientChannel: remoteId)); + + try { + sendMessage(SSH_Message_Channel_Close(recipientChannel: remoteId)); + } catch (e) { + printDebug?.call('SSHChannelController._sendCloseIfNeeded - error: $e'); + } } void _sendRequestSuccess() { @@ -455,8 +460,7 @@ class SSHChannelExtendedDataType { static const stderr = 1; } -class SSHChannelDataSplitter - extends StreamTransformerBase { +class SSHChannelDataSplitter extends StreamTransformerBase { SSHChannelDataSplitter(this.maxSize); final int maxSize; diff --git a/lib/src/ssh_client.dart b/lib/src/ssh_client.dart index 9290a1f..ee841f5 100644 --- a/lib/src/ssh_client.dart +++ b/lib/src/ssh_client.dart @@ -481,7 +481,12 @@ class SSHClient { ); } _keepAlive?.stop(); - _closeChannels(); + + try { + _closeChannels(); + } catch (e) { + printDebug?.call("SSHClient::_handleTransportClosed - error: $e"); + } } void _handlePacket(Uint8List payload) { From b0fdd018bcb4ee98bd2182977a50ab5264003ef5 Mon Sep 17 00:00:00 2001 From: Bar Mazuz Date: Thu, 15 May 2025 20:12:23 +0300 Subject: [PATCH 03/32] Implemented support for server rekey (by default every 1 GB or 1 hour) --- lib/src/ssh_transport.dart | 47 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/lib/src/ssh_transport.dart b/lib/src/ssh_transport.dart index 03540b6..aaad760 100644 --- a/lib/src/ssh_transport.dart +++ b/lib/src/ssh_transport.dart @@ -174,6 +174,13 @@ class SSHTransport { final _remotePacketSN = SSHPacketSN.fromZero(); + /// Whether a key exchange is currently in progress (initial or re-key). + bool _kexInProgress = false; + + /// Whether we have already sent our SSH_MSG_KEXINIT for the ongoing key + /// exchange round. This is reset when the exchange finishes. + bool _sentKexInit = false; + void sendPacket(Uint8List data) { if (isClosed) { throw SSHStateError('Transport is closed'); @@ -553,6 +560,16 @@ class SSHTransport { void _sendKexInit() { printDebug?.call('SSHTransport._sendKexInit'); + // Don't start a new key exchange when one is already in progress + if (_kexInProgress && _sentKexInit) { + printDebug?.call('Key exchange already in progress, ignoring'); + return; + } + + // Mark that a new key-exchange round has started from our side. + _kexInProgress = true; + _sentKexInit = true; + final message = SSH_Message_KexInit( kexAlgorithms: algorithms.kex.toNameList(), // kexAlgorithms: ['curve25519-sha256'], @@ -646,6 +663,18 @@ class SSHTransport { void _handleMessageKexInit(Uint8List payload) { printDebug?.call('SSHTransport._handleMessageKexInit'); + // If this message initiates a new key-exchange round from the remote + // side, we MUST respond with our own KEXINIT (RFC 4253 §7.1). + if (!_kexInProgress) { + // Start a new exchange initiated by the peer. + _kexInProgress = true; + } + + if (!_sentKexInit) { + // We have not sent our KEXINIT for this round yet, do it now. + _sendKexInit(); + } + final message = SSH_Message_KexInit.decode(payload); printTrace?.call('<- $socket: $message'); _remoteKexInit = payload; @@ -857,6 +886,24 @@ class SSHTransport { void _handleMessageNewKeys(Uint8List message) { printDebug?.call('SSHTransport._handleMessageNewKeys'); printTrace?.call('<- $socket: SSH_Message_NewKeys'); + _applyRemoteKeys(); + + // Key exchange round finished. + _kexInProgress = false; + _sentKexInit = false; + _kex = null; + } + + /// Initiates a client-side re-key operation. This can be called + /// by client code to refresh session keys when needed. + void rekey() { + printDebug?.call('SSHTransport.rekey'); + if (_kexInProgress) { + printDebug + ?.call('Key exchange already in progress, ignoring rekey request'); + return; + } + _sendKexInit(); } } From 86805cb31af61cebd67ecf2977e2341b04a3ec32 Mon Sep 17 00:00:00 2001 From: Bar Mazuz Date: Thu, 15 May 2025 20:31:42 +0300 Subject: [PATCH 04/32] According to RFC, we shouldn't send anything except KEX stuff when doing rekey --- lib/src/ssh_transport.dart | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/lib/src/ssh_transport.dart b/lib/src/ssh_transport.dart index aaad760..a418d65 100644 --- a/lib/src/ssh_transport.dart +++ b/lib/src/ssh_transport.dart @@ -181,10 +181,19 @@ class SSHTransport { /// exchange round. This is reset when the exchange finishes. bool _sentKexInit = false; + /// Packets queued during key exchange that will be sent after NEW_KEYS + final List _rekeyPendingPackets = []; + void sendPacket(Uint8List data) { if (isClosed) { throw SSHStateError('Transport is closed'); } + + if (_kexInProgress && !_isKexPacket(data)) { + _rekeyPendingPackets.add(Uint8List.fromList(data)); + return; + } + final packetAlign = _encryptCipher == null ? SSHPacket.minAlign : max(SSHPacket.minAlign, _encryptCipher!.blockSize); @@ -893,6 +902,13 @@ class SSHTransport { _kexInProgress = false; _sentKexInit = false; _kex = null; + + // Flush any pending packets + final pending = List.from(_rekeyPendingPackets); + _rekeyPendingPackets.clear(); + for (final packet in pending) { + sendPacket(packet); + } } /// Initiates a client-side re-key operation. This can be called @@ -906,4 +922,20 @@ class SSHTransport { } _sendKexInit(); } + + bool _isKexPacket(Uint8List data) { + if (data.isEmpty) return false; + + // All KEX message IDs + const kexMsgIds = [ + SSH_Message_KexInit.messageId, + SSH_Message_NewKeys.messageId, + SSH_Message_KexDH_Init.messageId, + SSH_Message_KexDH_Reply.messageId, + SSH_Message_KexDH_GexRequest.messageId, + SSH_Message_KexDH_GexGroup.messageId, + SSH_Message_KexDH_GexInit.messageId, + ]; + return kexMsgIds.contains(data[0]); + } } From dcce7af7a2605d6665d9d9d2e07b11d0d7fa7780 Mon Sep 17 00:00:00 2001 From: Bar Mazuz Date: Thu, 15 May 2025 20:52:10 +0300 Subject: [PATCH 05/32] Improved function to allow critical ssh messages through as well changed logs --- lib/src/ssh_transport.dart | 41 ++++++++++++++++++++++++++------------ 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/lib/src/ssh_transport.dart b/lib/src/ssh_transport.dart index a418d65..fd3933d 100644 --- a/lib/src/ssh_transport.dart +++ b/lib/src/ssh_transport.dart @@ -189,7 +189,7 @@ class SSHTransport { throw SSHStateError('Transport is closed'); } - if (_kexInProgress && !_isKexPacket(data)) { + if (_kexInProgress && !_shouldBypassRekeyBuffer(data)) { _rekeyPendingPackets.add(Uint8List.fromList(data)); return; } @@ -923,19 +923,34 @@ class SSHTransport { _sendKexInit(); } - bool _isKexPacket(Uint8List data) { + /// Determines if a packet should bypass the rekey buffer. + /// + /// During key exchange, most packets should be buffered until the exchange + /// is complete. However, key exchange packets themselves and transport layer + /// control messages (like disconnect) need to be sent immediately. + /// + /// Per RFC 4253, the following message types bypass the buffer: + /// + /// /// Critical transport messages (1-4): + /// - 1: [SSH_Message_Disconnect] + /// - 2: [SSH_Message_Ignore] + /// - 3: [SSH_Message_Unimplemented] + /// - 4: [SSH_Message_Debug] + /// + /// Key exchange messages (20-49): + /// - 20: [SSH_Message_KexInit] + /// - 21: [SSH_Message_NewKeys] + /// - 30: [SSH_Message_KexDH_Init]/[SSH_Message_KexECDH_Init] + /// - 31: [SSH_Message_KexDH_Reply]/[SSH_Message_KexECDH_Reply]/[SSH_Message_KexDH_GexGroup] + /// - 32: [SSH_Message_KexDH_GexInit] + /// - 33: [SSH_Message_KexDH_GexReply] + /// - 34: [SSH_Message_KexDH_GexRequest] + /// + /// + bool _shouldBypassRekeyBuffer(Uint8List data) { if (data.isEmpty) return false; - // All KEX message IDs - const kexMsgIds = [ - SSH_Message_KexInit.messageId, - SSH_Message_NewKeys.messageId, - SSH_Message_KexDH_Init.messageId, - SSH_Message_KexDH_Reply.messageId, - SSH_Message_KexDH_GexRequest.messageId, - SSH_Message_KexDH_GexGroup.messageId, - SSH_Message_KexDH_GexInit.messageId, - ]; - return kexMsgIds.contains(data[0]); + final messageId = data[0]; + return (messageId >= 20 && messageId <= 49) || messageId <= 4; } } From bead8a869708f8cdccf9ba6e78f17f80dfc011eb Mon Sep 17 00:00:00 2001 From: brein Date: Wed, 18 Jun 2025 16:50:33 +0200 Subject: [PATCH 06/32] Add support for new MAC mac-sha2-256-96", "hmac-sha2-512-96", "hmac-sha2-256-etm@openssh.com", "hmac-sha2-512-etm@openssh.com" algorithms --- lib/src/algorithm/ssh_mac_type.dart | 39 +++++++++++++-- lib/src/ssh_algorithm.dart | 6 +++ lib/src/utils/truncated_HMac.dart | 26 ++++++++++ test/src/algorithm/ssh_cipher_type_test.dart | 4 ++ test/src/algorithm/ssh_mac_type_test.dart | 50 ++++++++++++++++++++ 5 files changed, 122 insertions(+), 3 deletions(-) create mode 100644 lib/src/utils/truncated_HMac.dart diff --git a/lib/src/algorithm/ssh_mac_type.dart b/lib/src/algorithm/ssh_mac_type.dart index 0d1f59b..645a093 100644 --- a/lib/src/algorithm/ssh_mac_type.dart +++ b/lib/src/algorithm/ssh_mac_type.dart @@ -3,6 +3,8 @@ import 'dart:typed_data'; import 'package:dartssh2/src/ssh_algorithm.dart'; import 'package:pointycastle/export.dart'; +import '../utils/truncated_HMac.dart'; + class SSHMacType with SSHAlgorithm { static const hmacMd5 = SSHMacType._( name: 'hmac-md5', @@ -27,19 +29,41 @@ class SSHMacType with SSHAlgorithm { keySize: 64, macFactory: _hmacSha512Factory, ); + + // added by Rein + static const hmacSha256_96 = SSHMacType._( + name: 'hmac-sha2-256-96', + keySize: 32, + macFactory: _hmacSha256_96Factory, + ); + + static const hmacSha512_96 = SSHMacType._( + name: 'hmac-sha2-512-96', + keySize: 64, + macFactory: _hmacSha512_96Factory, + ); + + static const hmacSha256Etm = SSHMacType._( + name: 'hmac-sha2-256-etm@openssh.com', + keySize: 32, + macFactory: _hmacSha256Factory, + ); + static const hmacSha512Etm = SSHMacType._( + name: 'hmac-sha2-512-etm@openssh.com', + keySize: 64, + macFactory: _hmacSha512Factory, + ); + // end added by Rein const SSHMacType._({ required this.name, required this.keySize, required this.macFactory, }); - /// The name of the algorithm. For example, `"aes256-ctr`"`. @override final String name; - /// The length of the key in bytes. This is the same as the length of the - /// output of the MAC algorithm. final int keySize; final Mac Function() macFactory; @@ -70,3 +94,12 @@ Mac _hmacSha256Factory() { Mac _hmacSha512Factory() { return HMac(SHA512Digest(), 128); } +// added by Rein +Mac _hmacSha256_96Factory() { + return TruncatedHMac(SHA256Digest(), 64, 12); +} + +Mac _hmacSha512_96Factory() { + return TruncatedHMac(SHA512Digest(), 128, 12); +} +//end added by Rein \ No newline at end of file diff --git a/lib/src/ssh_algorithm.dart b/lib/src/ssh_algorithm.dart index 35d3c7d..e0ef706 100644 --- a/lib/src/ssh_algorithm.dart +++ b/lib/src/ssh_algorithm.dart @@ -69,6 +69,12 @@ class SSHAlgorithms { SSHCipherType.aes256cbc, ], this.mac = const [ + // added by Rein + SSHMacType.hmacSha256_96, + SSHMacType.hmacSha512_96, + SSHMacType.hmacSha256Etm, + SSHMacType.hmacSha512Etm, + // end added by Rein SSHMacType.hmacSha1, SSHMacType.hmacSha256, SSHMacType.hmacSha512, diff --git a/lib/src/utils/truncated_HMac.dart b/lib/src/utils/truncated_HMac.dart new file mode 100644 index 0000000..0565255 --- /dev/null +++ b/lib/src/utils/truncated_HMac.dart @@ -0,0 +1,26 @@ +import 'dart:typed_data'; + +import 'package:pointycastle/macs/hmac.dart'; + +/// Custom HMac implementation that truncates output to 96 bits (12 bytes) +class TruncatedHMac extends HMac { + final int _truncatedSize; + + TruncatedHMac(super.digest, super.blockSize, this._truncatedSize); + + @override + int get macSize => _truncatedSize; // 12 bytes instead of 32/64 + + @override + int doFinal(Uint8List out, int outOff) { + // Call the original doFinal to get the full MAC + final fullMacSize = super.macSize; + final tempBuffer = Uint8List(fullMacSize); + super.doFinal(tempBuffer, 0); + + // Copy only the first 12 bytes to the output + out.setRange(outOff, outOff + _truncatedSize, tempBuffer); + + return _truncatedSize; + } +} \ No newline at end of file diff --git a/test/src/algorithm/ssh_cipher_type_test.dart b/test/src/algorithm/ssh_cipher_type_test.dart index 15ae2fd..45019c3 100644 --- a/test/src/algorithm/ssh_cipher_type_test.dart +++ b/test/src/algorithm/ssh_cipher_type_test.dart @@ -81,6 +81,10 @@ void main() { expect( algorithms.mac, equals([ + SSHMacType.hmacSha256_96, + SSHMacType.hmacSha512_96, + SSHMacType.hmacSha256Etm, + SSHMacType.hmacSha512Etm, SSHMacType.hmacSha1, SSHMacType.hmacSha256, SSHMacType.hmacSha512, diff --git a/test/src/algorithm/ssh_mac_type_test.dart b/test/src/algorithm/ssh_mac_type_test.dart index a1b14c0..b75ca58 100644 --- a/test/src/algorithm/ssh_mac_type_test.dart +++ b/test/src/algorithm/ssh_mac_type_test.dart @@ -11,11 +11,21 @@ void main() { expect(SSHMacType.hmacSha1.name, equals('hmac-sha1')); expect(SSHMacType.hmacSha256.name, equals('hmac-sha2-256')); expect(SSHMacType.hmacSha512.name, equals('hmac-sha2-512')); + // Added new algorithms + expect(SSHMacType.hmacSha256_96.name, equals('hmac-sha2-256-96')); + expect(SSHMacType.hmacSha512_96.name, equals('hmac-sha2-512-96')); + expect(SSHMacType.hmacSha256Etm.name, equals('hmac-sha2-256-etm@openssh.com')); + expect(SSHMacType.hmacSha512Etm.name, equals('hmac-sha2-512-etm@openssh.com')); expect(SSHMacType.hmacMd5.keySize, equals(16)); expect(SSHMacType.hmacSha1.keySize, equals(20)); expect(SSHMacType.hmacSha256.keySize, equals(32)); expect(SSHMacType.hmacSha512.keySize, equals(64)); + // Added new algorithm key sizes + expect(SSHMacType.hmacSha256_96.keySize, equals(32)); + expect(SSHMacType.hmacSha512_96.keySize, equals(64)); + expect(SSHMacType.hmacSha256Etm.keySize, equals(32)); + expect(SSHMacType.hmacSha512Etm.keySize, equals(64)); }); test('createMac() returns correct Mac instance', () { @@ -49,5 +59,45 @@ void main() { final mac = SSHMacType.hmacSha512.createMac(key); expect(mac, isA()); }); + + test('createMac() for new algorithm types returns correct instances', () { + final sha256Key = Uint8List(32); // 32 bytes for SHA-256 based algorithms + final sha512Key = Uint8List(64); // 64 bytes for SHA-512 based algorithms + + final macSha256_96 = SSHMacType.hmacSha256_96.createMac(sha256Key); + final macSha512_96 = SSHMacType.hmacSha512_96.createMac(sha512Key); + final macSha256Etm = SSHMacType.hmacSha256Etm.createMac(sha256Key); + final macSha512Etm = SSHMacType.hmacSha512Etm.createMac(sha512Key); + + expect(macSha256_96, isNotNull); + expect(macSha512_96, isNotNull); + expect(macSha256Etm, isA()); + expect(macSha512Etm, isA()); + }); + + test('createMac() throws for new algorithms with incorrect key length', () { + final shortSha256Key = Uint8List(31); // One byte too short for SHA-256 based algorithms + final shortSha512Key = Uint8List(63); // One byte too short for SHA-512 based algorithms + + expect( + () => SSHMacType.hmacSha256_96.createMac(shortSha256Key), + throwsArgumentError, + ); + + expect( + () => SSHMacType.hmacSha512_96.createMac(shortSha512Key), + throwsArgumentError, + ); + + expect( + () => SSHMacType.hmacSha256Etm.createMac(shortSha256Key), + throwsArgumentError, + ); + + expect( + () => SSHMacType.hmacSha512Etm.createMac(shortSha512Key), + throwsArgumentError, + ); + }); }); } From 81c6905f4305e7a0c3be4a6a3d2cde6033d77c1e Mon Sep 17 00:00:00 2001 From: brein Date: Fri, 20 Jun 2025 01:26:34 +0200 Subject: [PATCH 07/32] Corrected the ETM encryptions --- lib/src/utils/{truncated_HMac.dart => truncated_hmac.dart} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename lib/src/utils/{truncated_HMac.dart => truncated_hmac.dart} (100%) diff --git a/lib/src/utils/truncated_HMac.dart b/lib/src/utils/truncated_hmac.dart similarity index 100% rename from lib/src/utils/truncated_HMac.dart rename to lib/src/utils/truncated_hmac.dart From c55e2692ad65fad49c86e2b6483c2c1ccede38d9 Mon Sep 17 00:00:00 2001 From: brein Date: Fri, 20 Jun 2025 01:27:12 +0200 Subject: [PATCH 08/32] Corrected the ETM encryptions --- lib/src/algorithm/ssh_cipher_type.dart | 2 +- lib/src/algorithm/ssh_hostkey_type.dart | 2 +- lib/src/algorithm/ssh_kex_type.dart | 2 +- lib/src/algorithm/ssh_mac_type.dart | 15 +- lib/src/ssh_algorithm.dart | 4 +- lib/src/ssh_transport.dart | 231 ++++++++++++++++++---- test/src/algorithm/ssh_mac_type_test.dart | 56 ++---- 7 files changed, 225 insertions(+), 87 deletions(-) diff --git a/lib/src/algorithm/ssh_cipher_type.dart b/lib/src/algorithm/ssh_cipher_type.dart index 2553ca6..c59db33 100644 --- a/lib/src/algorithm/ssh_cipher_type.dart +++ b/lib/src/algorithm/ssh_cipher_type.dart @@ -3,7 +3,7 @@ import 'dart:typed_data'; import 'package:dartssh2/src/ssh_algorithm.dart'; import 'package:pointycastle/export.dart'; -class SSHCipherType with SSHAlgorithm { +class SSHCipherType extends SSHAlgorithm { static const values = [ aes128cbc, aes192cbc, diff --git a/lib/src/algorithm/ssh_hostkey_type.dart b/lib/src/algorithm/ssh_hostkey_type.dart index 307ef7d..513c800 100644 --- a/lib/src/algorithm/ssh_hostkey_type.dart +++ b/lib/src/algorithm/ssh_hostkey_type.dart @@ -1,6 +1,6 @@ import 'package:dartssh2/src/ssh_algorithm.dart'; -class SSHHostkeyType with SSHAlgorithm { +class SSHHostkeyType extends SSHAlgorithm { static const rsaSha1 = SSHHostkeyType._('ssh-rsa'); static const rsaSha256 = SSHHostkeyType._('rsa-sha2-256'); static const rsaSha512 = SSHHostkeyType._('rsa-sha2-512'); diff --git a/lib/src/algorithm/ssh_kex_type.dart b/lib/src/algorithm/ssh_kex_type.dart index 9110230..2866319 100644 --- a/lib/src/algorithm/ssh_kex_type.dart +++ b/lib/src/algorithm/ssh_kex_type.dart @@ -1,7 +1,7 @@ import 'package:dartssh2/src/ssh_algorithm.dart'; import 'package:pointycastle/export.dart'; -class SSHKexType with SSHAlgorithm { +class SSHKexType extends SSHAlgorithm { static const x25519 = SSHKexType._( name: 'curve25519-sha256@libssh.org', digestFactory: digestSha256, diff --git a/lib/src/algorithm/ssh_mac_type.dart b/lib/src/algorithm/ssh_mac_type.dart index 645a093..6e00d75 100644 --- a/lib/src/algorithm/ssh_mac_type.dart +++ b/lib/src/algorithm/ssh_mac_type.dart @@ -3,9 +3,9 @@ import 'dart:typed_data'; import 'package:dartssh2/src/ssh_algorithm.dart'; import 'package:pointycastle/export.dart'; -import '../utils/truncated_HMac.dart'; +import '../utils/truncated_hmac.dart'; -class SSHMacType with SSHAlgorithm { +class SSHMacType extends SSHAlgorithm { static const hmacMd5 = SSHMacType._( name: 'hmac-md5', keySize: 16, @@ -29,7 +29,7 @@ class SSHMacType with SSHAlgorithm { keySize: 64, macFactory: _hmacSha512Factory, ); - + // added by Rein static const hmacSha256_96 = SSHMacType._( name: 'hmac-sha2-256-96', @@ -47,18 +47,21 @@ class SSHMacType with SSHAlgorithm { name: 'hmac-sha2-256-etm@openssh.com', keySize: 32, macFactory: _hmacSha256Factory, + isEtm: true, ); static const hmacSha512Etm = SSHMacType._( name: 'hmac-sha2-512-etm@openssh.com', keySize: 64, macFactory: _hmacSha512Factory, + isEtm: true, ); // end added by Rein const SSHMacType._({ required this.name, required this.keySize, required this.macFactory, + this.isEtm = false, }); @override @@ -68,6 +71,9 @@ class SSHMacType with SSHAlgorithm { final Mac Function() macFactory; + /// Whether this MAC algorithm is an ETM (Encrypt-Then-MAC) variant. + final bool isEtm; + Mac createMac(Uint8List key) { if (key.length != keySize) { throw ArgumentError.value(key, 'key', 'Key must be $keySize bytes long'); @@ -94,7 +100,7 @@ Mac _hmacSha256Factory() { Mac _hmacSha512Factory() { return HMac(SHA512Digest(), 128); } -// added by Rein + Mac _hmacSha256_96Factory() { return TruncatedHMac(SHA256Digest(), 64, 12); } @@ -102,4 +108,3 @@ Mac _hmacSha256_96Factory() { Mac _hmacSha512_96Factory() { return TruncatedHMac(SHA512Digest(), 128, 12); } -//end added by Rein \ No newline at end of file diff --git a/lib/src/ssh_algorithm.dart b/lib/src/ssh_algorithm.dart index e0ef706..fc57995 100644 --- a/lib/src/ssh_algorithm.dart +++ b/lib/src/ssh_algorithm.dart @@ -7,6 +7,8 @@ abstract class SSHAlgorithm { /// The name of the algorithm. String get name; + const SSHAlgorithm(); + @override String toString() { return '$runtimeType($name)'; @@ -69,12 +71,10 @@ class SSHAlgorithms { SSHCipherType.aes256cbc, ], this.mac = const [ - // added by Rein SSHMacType.hmacSha256_96, SSHMacType.hmacSha512_96, SSHMacType.hmacSha256Etm, SSHMacType.hmacSha512Etm, - // end added by Rein SSHMacType.hmacSha1, SSHMacType.hmacSha256, SSHMacType.hmacSha512, diff --git a/lib/src/ssh_transport.dart b/lib/src/ssh_transport.dart index 03540b6..3589a1d 100644 --- a/lib/src/ssh_transport.dart +++ b/lib/src/ssh_transport.dart @@ -178,24 +178,95 @@ class SSHTransport { if (isClosed) { throw SSHStateError('Transport is closed'); } - final packetAlign = _encryptCipher == null - ? SSHPacket.minAlign - : max(SSHPacket.minAlign, _encryptCipher!.blockSize); - final packet = SSHPacket.pack(data, align: packetAlign); + // Check if encryption is enabled and if we have MAC types initialized + final clientMacType = _clientMacType; + final serverMacType = _serverMacType; + final macType = isClient ? clientMacType : serverMacType; + final isEtm = _encryptCipher != null && macType != null && macType.isEtm; + + // For ETM, we need to handle the packet differently + if (isEtm) { + // For ETM (Encrypt-Then-MAC): + // 1. Keep the packet length in plaintext + // 2. Encrypt only the payload (padding length, payload, padding) + + // Calculate the block size for alignment + final blockSize = _encryptCipher!.blockSize; + + // Create a custom packet structure for ETM mode + // We need to ensure that the payload we're encrypting is a multiple of the block size + + // Calculate the padding length to ensure the total length is a multiple of the block size + // We need to account for the 1 byte padding length field + final paddingLength = blockSize - ((data.length + 1) % blockSize); + // Ensure padding is at least 4 bytes as per SSH spec + final adjustedPaddingLength = paddingLength < 4 ? paddingLength + blockSize : paddingLength; + + // Calculate the total packet length (excluding the length field itself) + final packetLength = 1 + data.length + adjustedPaddingLength; + + // Create the packet length field (4 bytes) + final packetLengthBytes = Uint8List(4); + packetLengthBytes.buffer.asByteData().setUint32(0, packetLength); + + // Create the payload to be encrypted (padding length + payload + padding) + final payloadToEncrypt = Uint8List(packetLength); + payloadToEncrypt[0] = adjustedPaddingLength; // Set padding length + payloadToEncrypt.setRange(1, 1 + data.length, data); // Copy data + + // Add random padding + for (var i = 0; i < adjustedPaddingLength; i++) { + payloadToEncrypt[1 + data.length + i] = (DateTime.now().microsecondsSinceEpoch + i) & 0xFF; + } - if (_encryptCipher == null) { - socket.sink.add(packet); - } else { + // Verify that the payload length is a multiple of the block size + if (payloadToEncrypt.length % blockSize != 0) { + throw StateError('Payload length ${payloadToEncrypt.length} is not a multiple of block size $blockSize'); + } + + // Encrypt the payload + final encryptedPayload = _encryptCipher!.processAll(payloadToEncrypt); + + // Calculate MAC on the packet length and encrypted payload final mac = _localMac!; mac.updateAll(_localPacketSN.value.toUint32()); - mac.updateAll(packet); + mac.updateAll(packetLengthBytes); + mac.updateAll(encryptedPayload); + final macBytes = mac.finish(); + // Build the final packet: length + encrypted payload + MAC final buffer = BytesBuilder(copy: false); - buffer.add(_encryptCipher!.processAll(packet)); - buffer.add(mac.finish()); + buffer.add(packetLengthBytes); + buffer.add(encryptedPayload); + buffer.add(macBytes); socket.sink.add(buffer.takeBytes()); + } else { + // For standard encryption or no encryption: + // Use the original packet packing logic + final packetAlign = _encryptCipher == null + ? SSHPacket.minAlign + : max(SSHPacket.minAlign, _encryptCipher!.blockSize); + + final packet = SSHPacket.pack(data, align: packetAlign); + + if (_encryptCipher == null) { + socket.sink.add(packet); + } else { + final mac = _localMac!; + final encryptedPacket = _encryptCipher!.processAll(packet); + + final buffer = BytesBuilder(copy: false); + buffer.add(encryptedPacket); + + // Calculate MAC on the unencrypted packet + mac.updateAll(_localPacketSN.value.toUint32()); + mac.updateAll(packet); + buffer.add(mac.finish()); + + socket.sink.add(buffer.takeBytes()); + } } _localPacketSN.increase(); @@ -357,33 +428,109 @@ class SSHTransport { return null; } - if (_decryptBuffer.isEmpty) { - final firstBlock = _buffer.consume(blockSize); - _decryptBuffer.add(_decryptCipher!.process(firstBlock)); - } + final macType = isClient ? _serverMacType! : _clientMacType!; + final isEtm = macType.isEtm; + final macLength = _remoteMac!.macSize; - final packetLength = SSHPacket.readPacketLength(_decryptBuffer.data); - _verifyPacketLength(packetLength); + if (isEtm) { + // For ETM (Encrypt-Then-MAC) algorithms, the packet length is in plaintext + // followed by the encrypted payload and then the MAC - final macLength = _remoteMac!.macSize; - if (_buffer.length + _decryptBuffer.length < 4 + packetLength + macLength) { - return null; - } + // We need at least 4 bytes to read the packet length + if (_buffer.length < 4) { + return null; + } - while (_decryptBuffer.length < 4 + packetLength) { - final block = _buffer.consume(blockSize); - _decryptBuffer.add(_decryptCipher!.process(block)); - } + // Read the packet length from the plaintext data + final packetLength = SSHPacket.readPacketLength(_buffer.data); + _verifyPacketLength(packetLength); - final packet = _decryptBuffer.consume(packetLength + 4); - final paddingLength = SSHPacket.readPaddingLength(packet); - final payloadLength = packetLength - paddingLength - 1; - _verifyPacketPadding(payloadLength, paddingLength); + // Make sure we have enough data for the entire packet and MAC + if (_buffer.length < 4 + packetLength + macLength) { + return null; + } - final mac = _buffer.consume(macLength); - _verifyPacketMac(packet, mac); + // Get the packet length bytes + final packetLengthBytes = _buffer.view(0, 4); - return Uint8List.sublistView(packet, 5, packet.length - paddingLength); + // Get the encrypted payload and MAC + final encryptedPayload = _buffer.view(4, packetLength); + final mac = _buffer.view(4 + packetLength, macLength); + + // Verify the MAC on the packet length and encrypted payload + final packetForMac = Uint8List(4 + packetLength); + packetForMac.setRange(0, 4, packetLengthBytes); + packetForMac.setRange(4, 4 + packetLength, encryptedPayload); + _verifyPacketMac(packetForMac, mac, isEncrypted: true); + + // Consume the packet and MAC from the buffer + _buffer.consume(4 + packetLength + macLength); + + // Ensure the encrypted payload length is a multiple of the block size + if (encryptedPayload.length % blockSize != 0) { + throw SSHPacketError( + 'Encrypted payload length ${encryptedPayload.length} is not a multiple of block size $blockSize', + ); + } + + // Decrypt the payload + final decryptedPayload = _decryptCipher!.processAll(encryptedPayload); + + // Process the decrypted payload + final paddingLength = decryptedPayload[0]; + + // Verify that the padding length is valid + if (paddingLength < 4) { + throw SSHPacketError( + 'Padding length too small: $paddingLength (minimum is 4)', + ); + } + + if (paddingLength >= packetLength) { + throw SSHPacketError( + 'Padding length too large: $paddingLength (packet length is $packetLength)', + ); + } + + final payloadLength = packetLength - paddingLength - 1; + if (payloadLength < 0) { + throw SSHPacketError( + 'Invalid payload length: $payloadLength (packet length: $packetLength, padding length: $paddingLength)', + ); + } + + // Skip the padding length byte and extract the payload + return Uint8List.sublistView(decryptedPayload, 1, 1 + payloadLength); + } else { + // For standard MAC algorithms, decrypt the packet first, then verify the MAC + + if (_decryptBuffer.isEmpty) { + final firstBlock = _buffer.consume(blockSize); + _decryptBuffer.add(_decryptCipher!.process(firstBlock)); + } + + final packetLength = SSHPacket.readPacketLength(_decryptBuffer.data); + _verifyPacketLength(packetLength); + + if (_buffer.length + _decryptBuffer.length < 4 + packetLength + macLength) { + return null; + } + + while (_decryptBuffer.length < 4 + packetLength) { + final block = _buffer.consume(blockSize); + _decryptBuffer.add(_decryptCipher!.process(block)); + } + + final packet = _decryptBuffer.consume(packetLength + 4); + final paddingLength = SSHPacket.readPaddingLength(packet); + final payloadLength = packetLength - paddingLength - 1; + _verifyPacketPadding(payloadLength, paddingLength); + + final mac = _buffer.consume(macLength); + _verifyPacketMac(packet, mac, isEncrypted: false); + + return Uint8List.sublistView(packet, 5, packet.length - paddingLength); + } } void _verifyPacketLength(int packetLength) { @@ -413,14 +560,32 @@ class SSHTransport { /// Verifies that the MAC of the packet is correct. Throws [SSHPacketError] /// if the MAC is incorrect. - void _verifyPacketMac(Uint8List payload, Uint8List actualMac) { + /// + /// For ETM (Encrypt-Then-MAC) algorithms, the MAC is calculated on the packet length and encrypted payload. + /// For standard MAC algorithms, the MAC is calculated on the unencrypted packet. + void _verifyPacketMac(Uint8List payload, Uint8List actualMac, {bool isEncrypted = false}) { final macSize = _remoteMac!.macSize; if (actualMac.length != macSize) { throw ArgumentError.value(actualMac, 'mac', 'Invalid MAC size'); } + final macType = isClient ? _serverMacType! : _clientMacType!; + final isEtm = macType.isEtm; + _remoteMac!.updateAll(_remotePacketSN.value.toUint32()); - _remoteMac!.updateAll(payload); + + // For ETM algorithms, the MAC is calculated on the packet length and encrypted payload + // For standard MAC algorithms, the MAC is calculated on the unencrypted packet + if (isEtm && isEncrypted) { + _remoteMac!.updateAll(payload); + } else if (!isEtm && !isEncrypted) { + _remoteMac!.updateAll(payload); + } else { + throw SSHPacketError( + 'MAC algorithm mismatch: isEtm=$isEtm, isEncrypted=$isEncrypted', + ); + } + final expectedMac = _remoteMac!.finish(); if (!expectedMac.equals(actualMac)) { diff --git a/test/src/algorithm/ssh_mac_type_test.dart b/test/src/algorithm/ssh_mac_type_test.dart index b75ca58..a9c69ca 100644 --- a/test/src/algorithm/ssh_mac_type_test.dart +++ b/test/src/algorithm/ssh_mac_type_test.dart @@ -11,9 +11,6 @@ void main() { expect(SSHMacType.hmacSha1.name, equals('hmac-sha1')); expect(SSHMacType.hmacSha256.name, equals('hmac-sha2-256')); expect(SSHMacType.hmacSha512.name, equals('hmac-sha2-512')); - // Added new algorithms - expect(SSHMacType.hmacSha256_96.name, equals('hmac-sha2-256-96')); - expect(SSHMacType.hmacSha512_96.name, equals('hmac-sha2-512-96')); expect(SSHMacType.hmacSha256Etm.name, equals('hmac-sha2-256-etm@openssh.com')); expect(SSHMacType.hmacSha512Etm.name, equals('hmac-sha2-512-etm@openssh.com')); @@ -21,9 +18,6 @@ void main() { expect(SSHMacType.hmacSha1.keySize, equals(20)); expect(SSHMacType.hmacSha256.keySize, equals(32)); expect(SSHMacType.hmacSha512.keySize, equals(64)); - // Added new algorithm key sizes - expect(SSHMacType.hmacSha256_96.keySize, equals(32)); - expect(SSHMacType.hmacSha512_96.keySize, equals(64)); expect(SSHMacType.hmacSha256Etm.keySize, equals(32)); expect(SSHMacType.hmacSha512Etm.keySize, equals(64)); }); @@ -60,44 +54,18 @@ void main() { expect(mac, isA()); }); - test('createMac() for new algorithm types returns correct instances', () { - final sha256Key = Uint8List(32); // 32 bytes for SHA-256 based algorithms - final sha512Key = Uint8List(64); // 64 bytes for SHA-512 based algorithms - - final macSha256_96 = SSHMacType.hmacSha256_96.createMac(sha256Key); - final macSha512_96 = SSHMacType.hmacSha512_96.createMac(sha512Key); - final macSha256Etm = SSHMacType.hmacSha256Etm.createMac(sha256Key); - final macSha512Etm = SSHMacType.hmacSha512Etm.createMac(sha512Key); - - expect(macSha256_96, isNotNull); - expect(macSha512_96, isNotNull); - expect(macSha256Etm, isA()); - expect(macSha512Etm, isA()); - }); - - test('createMac() throws for new algorithms with incorrect key length', () { - final shortSha256Key = Uint8List(31); // One byte too short for SHA-256 based algorithms - final shortSha512Key = Uint8List(63); // One byte too short for SHA-512 based algorithms - - expect( - () => SSHMacType.hmacSha256_96.createMac(shortSha256Key), - throwsArgumentError, - ); - - expect( - () => SSHMacType.hmacSha512_96.createMac(shortSha512Key), - throwsArgumentError, - ); - - expect( - () => SSHMacType.hmacSha256Etm.createMac(shortSha256Key), - throwsArgumentError, - ); - - expect( - () => SSHMacType.hmacSha512Etm.createMac(shortSha512Key), - throwsArgumentError, - ); + test('isEtm property is set correctly', () { + // Non-ETM algorithms + expect(SSHMacType.hmacMd5.isEtm, isFalse); + expect(SSHMacType.hmacSha1.isEtm, isFalse); + expect(SSHMacType.hmacSha256.isEtm, isFalse); + expect(SSHMacType.hmacSha512.isEtm, isFalse); + expect(SSHMacType.hmacSha256_96.isEtm, isFalse); + expect(SSHMacType.hmacSha512_96.isEtm, isFalse); + + // ETM algorithms + expect(SSHMacType.hmacSha256Etm.isEtm, isTrue); + expect(SSHMacType.hmacSha512Etm.isEtm, isTrue); }); }); } From cd3bc7737d05474e96798fae5dc829de19d5c79b Mon Sep 17 00:00:00 2001 From: xuty Date: Sun, 22 Jun 2025 10:27:57 +0800 Subject: [PATCH 09/32] Format code --- lib/src/ssh_channel.dart | 3 ++- lib/src/ssh_transport.dart | 3 +-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/src/ssh_channel.dart b/lib/src/ssh_channel.dart index f5defbf..20f0fa8 100644 --- a/lib/src/ssh_channel.dart +++ b/lib/src/ssh_channel.dart @@ -460,7 +460,8 @@ class SSHChannelExtendedDataType { static const stderr = 1; } -class SSHChannelDataSplitter extends StreamTransformerBase { +class SSHChannelDataSplitter + extends StreamTransformerBase { SSHChannelDataSplitter(this.maxSize); final int maxSize; diff --git a/lib/src/ssh_transport.dart b/lib/src/ssh_transport.dart index 3da0e79..b50ccf3 100644 --- a/lib/src/ssh_transport.dart +++ b/lib/src/ssh_transport.dart @@ -806,8 +806,7 @@ class SSHTransport { sharedSecret: sharedSecret, ); - if (!disableHostkeyVerification) - { + if (!disableHostkeyVerification) { final verified = _verifyHostkey( keyBytes: hostkey, signatureBytes: hostSignature, From 364856d3834001797b5b2fe04fabca465fc696d2 Mon Sep 17 00:00:00 2001 From: xuty Date: Sun, 22 Jun 2025 12:01:24 +0800 Subject: [PATCH 10/32] Add more tests --- test/src/ssh_client_test.dart | 32 ++++++++++++++++++++++++++++++++ test/test_utils.dart | 5 ++++- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/test/src/ssh_client_test.dart b/test/src/ssh_client_test.dart index 8a6abd8..ecd9189 100644 --- a/test/src/ssh_client_test.dart +++ b/test/src/ssh_client_test.dart @@ -26,6 +26,38 @@ void main() { // client.close(); // }); + // test('hmacSha256_96 mac works', () async { + // var client = await getHoneypotClient( + // algorithms: SSHAlgorithms(mac: [SSHMacType.hmacSha256_96]), + // ); + // await client.authenticated; + // client.close(); + // }); + + // test('hmacSha512_96 mac works', () async { + // var client = await getHoneypotClient( + // algorithms: SSHAlgorithms(mac: [SSHMacType.hmacSha512_96]), + // ); + // await client.authenticated; + // client.close(); + // }); + + test('hmacSha256Etm mac works', () async { + var client = await getHoneypotClient( + algorithms: SSHAlgorithms(mac: [SSHMacType.hmacSha256Etm]), + ); + await client.authenticated; + client.close(); + }); + + test('hmacSha512Etm mac works', () async { + var client = await getHoneypotClient( + algorithms: SSHAlgorithms(mac: [SSHMacType.hmacSha512Etm]), + ); + await client.authenticated; + client.close(); + }); + test('throws SSHAuthFailError when public key is wrong', () async { var client = SSHClient( await SSHSocket.connect('test.rebex.net', 22), diff --git a/test/test_utils.dart b/test/test_utils.dart index f4c96c0..328fe96 100644 --- a/test/test_utils.dart +++ b/test/test_utils.dart @@ -5,11 +5,14 @@ import 'package:dartssh2/dartssh2.dart'; import 'package:dartssh2/src/message/msg_channel.dart'; /// A honeypot that accepts all passwords and public-keys -Future getHoneypotClient() async { +Future getHoneypotClient({ + SSHAlgorithms algorithms = const SSHAlgorithms(), +}) async { return SSHClient( await SSHSocket.connect('test.rebex.net', 22), username: 'demo', onPasswordRequest: () => 'password', + algorithms: algorithms, ); } From 81cf1c543928f85fa5a70761ae3c93265506b711 Mon Sep 17 00:00:00 2001 From: xuty Date: Sun, 22 Jun 2025 14:43:59 +0800 Subject: [PATCH 11/32] Format code --- lib/src/ssh_transport.dart | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/lib/src/ssh_transport.dart b/lib/src/ssh_transport.dart index 3589a1d..0e6e6ee 100644 --- a/lib/src/ssh_transport.dart +++ b/lib/src/ssh_transport.dart @@ -201,7 +201,8 @@ class SSHTransport { // We need to account for the 1 byte padding length field final paddingLength = blockSize - ((data.length + 1) % blockSize); // Ensure padding is at least 4 bytes as per SSH spec - final adjustedPaddingLength = paddingLength < 4 ? paddingLength + blockSize : paddingLength; + final adjustedPaddingLength = + paddingLength < 4 ? paddingLength + blockSize : paddingLength; // Calculate the total packet length (excluding the length field itself) final packetLength = 1 + data.length + adjustedPaddingLength; @@ -217,12 +218,14 @@ class SSHTransport { // Add random padding for (var i = 0; i < adjustedPaddingLength; i++) { - payloadToEncrypt[1 + data.length + i] = (DateTime.now().microsecondsSinceEpoch + i) & 0xFF; + payloadToEncrypt[1 + data.length + i] = + (DateTime.now().microsecondsSinceEpoch + i) & 0xFF; } // Verify that the payload length is a multiple of the block size if (payloadToEncrypt.length % blockSize != 0) { - throw StateError('Payload length ${payloadToEncrypt.length} is not a multiple of block size $blockSize'); + throw StateError( + 'Payload length ${payloadToEncrypt.length} is not a multiple of block size $blockSize'); } // Encrypt the payload @@ -512,7 +515,8 @@ class SSHTransport { final packetLength = SSHPacket.readPacketLength(_decryptBuffer.data); _verifyPacketLength(packetLength); - if (_buffer.length + _decryptBuffer.length < 4 + packetLength + macLength) { + if (_buffer.length + _decryptBuffer.length < + 4 + packetLength + macLength) { return null; } @@ -560,10 +564,11 @@ class SSHTransport { /// Verifies that the MAC of the packet is correct. Throws [SSHPacketError] /// if the MAC is incorrect. - /// + /// /// For ETM (Encrypt-Then-MAC) algorithms, the MAC is calculated on the packet length and encrypted payload. /// For standard MAC algorithms, the MAC is calculated on the unencrypted packet. - void _verifyPacketMac(Uint8List payload, Uint8List actualMac, {bool isEncrypted = false}) { + void _verifyPacketMac(Uint8List payload, Uint8List actualMac, + {bool isEncrypted = false}) { final macSize = _remoteMac!.macSize; if (actualMac.length != macSize) { throw ArgumentError.value(actualMac, 'mac', 'Invalid MAC size'); From 25429ab84fb47d6a4722d9f646b77cbe4dd04a57 Mon Sep 17 00:00:00 2001 From: xuty Date: Sun, 22 Jun 2025 14:45:54 +0800 Subject: [PATCH 12/32] Format code --- lib/src/utils/truncated_hmac.dart | 2 +- test/src/algorithm/ssh_mac_type_test.dart | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/src/utils/truncated_hmac.dart b/lib/src/utils/truncated_hmac.dart index 0565255..00cb1e1 100644 --- a/lib/src/utils/truncated_hmac.dart +++ b/lib/src/utils/truncated_hmac.dart @@ -23,4 +23,4 @@ class TruncatedHMac extends HMac { return _truncatedSize; } -} \ No newline at end of file +} diff --git a/test/src/algorithm/ssh_mac_type_test.dart b/test/src/algorithm/ssh_mac_type_test.dart index a9c69ca..14d4e56 100644 --- a/test/src/algorithm/ssh_mac_type_test.dart +++ b/test/src/algorithm/ssh_mac_type_test.dart @@ -11,8 +11,10 @@ void main() { expect(SSHMacType.hmacSha1.name, equals('hmac-sha1')); expect(SSHMacType.hmacSha256.name, equals('hmac-sha2-256')); expect(SSHMacType.hmacSha512.name, equals('hmac-sha2-512')); - expect(SSHMacType.hmacSha256Etm.name, equals('hmac-sha2-256-etm@openssh.com')); - expect(SSHMacType.hmacSha512Etm.name, equals('hmac-sha2-512-etm@openssh.com')); + expect(SSHMacType.hmacSha256Etm.name, + equals('hmac-sha2-256-etm@openssh.com')); + expect(SSHMacType.hmacSha512Etm.name, + equals('hmac-sha2-512-etm@openssh.com')); expect(SSHMacType.hmacMd5.keySize, equals(16)); expect(SSHMacType.hmacSha1.keySize, equals(20)); From dc8607b37dae9e4196d2fa34c042d9cea8ac45e8 Mon Sep 17 00:00:00 2001 From: xuty Date: Sun, 22 Jun 2025 14:48:39 +0800 Subject: [PATCH 13/32] Update example --- example/sftp_upload.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/example/sftp_upload.dart b/example/sftp_upload.dart index 68f3c18..106d78c 100644 --- a/example/sftp_upload.dart +++ b/example/sftp_upload.dart @@ -22,6 +22,8 @@ void main(List args) async { await file.write(File('local_file.txt').openRead().cast()).done; print('done'); + await file.close(); + client.close(); await client.done; } From c7d632185fdb226c726e6033f92649d97b0fdf47 Mon Sep 17 00:00:00 2001 From: xuty Date: Sun, 22 Jun 2025 14:48:44 +0800 Subject: [PATCH 14/32] Add .vscode to .gitignore --- .gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 85fca2c..336b955 100644 --- a/.gitignore +++ b/.gitignore @@ -13,4 +13,6 @@ build/ doc/api/ .idea -.history \ No newline at end of file +.history + +.vscode \ No newline at end of file From df127d7b937d9a2b040c6bae3f1fc5bc0bba1722 Mon Sep 17 00:00:00 2001 From: xuty Date: Sun, 22 Jun 2025 14:54:10 +0800 Subject: [PATCH 15/32] Bump version --- CHANGELOG.md | 15 +++++++++++++-- pubspec.yaml | 2 +- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a3f635..7bd0f49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ -## [2.13.0] - 2025-mm-dd +## [2.13.0] - 2025-06-22 - docs: Update NoPorts naming [#115]. [@XavierChanth]. +- Add parameter disableHostkeyVerification [#123]. Thanks [@alexander-irion]. +- Add support for server initiated re-keying [#125]. Thanks [@MarBazuz]. +- Add support for new algorithms "mac-sha2-256-96", "hmac-sha2-512-96", "hmac-sha2-256-etm@openssh.com", "hmac-sha2-512-etm@openssh.com" [#126] [#127]. Thanks [@reinbeumer]. ## [2.12.0] - 2025-02-08 - Fixed streams and channel not closing after receiving SSH_Message_Channel_Close [#116]. [@cbenhagen]. @@ -168,6 +171,10 @@ - Initial release. +[#127]: https://github.com/TerminalStudio/dartssh2/pull/127 +[#126]: https://github.com/TerminalStudio/dartssh2/pull/126 +[#125]: https://github.com/TerminalStudio/dartssh2/pull/125 +[#123]: https://github.com/TerminalStudio/dartssh2/pull/123 [#101]: https://github.com/TerminalStudio/dartssh2/pull/101 [#100]: https://github.com/TerminalStudio/dartssh2/issues/100 [#80]: https://github.com/TerminalStudio/dartssh2/issues/80 @@ -182,4 +189,8 @@ [@linhanyu]: https://github.com/linhanyu [@Migarl]: https://github.com/Migarl -[@PIDAMI]: https://github.com/PIDAMI \ No newline at end of file +[@PIDAMI]: https://github.com/PIDAMI +[@XavierChanth]: https://github.com/XavierChanth +[@MarBazuz]: https://github.com/MarBazuz +[@reinbeumer]: https://github.com/reinbeumer +[@alexander-irion]: https://github.com/alexander-irion \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index 3f784b9..9d75950 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: dartssh2 -version: 2.12.0 +version: 2.13.0 description: SSH and SFTP client written in pure Dart, aiming to be feature-rich as well as easy to use. homepage: https://github.com/TerminalStudio/dartssh2 From 1d9613c9939f726bbc99c3b0b23580d8f12c7ff5 Mon Sep 17 00:00:00 2001 From: "Victor C." Date: Sun, 20 Jul 2025 08:32:08 +0200 Subject: [PATCH 16/32] Removed Dependabot on package --- .github/dependabot.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 1059862..ad8b29e 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -5,12 +5,6 @@ version: 2 updates: - - package-ecosystem: "pub" - directory: "/" - schedule: - interval: "weekly" - time: "09:00" - timezone: Europe/Madrid - package-ecosystem: "pub" directory: "/example/" schedule: From 5cba8d7cacec48a971c7663eaf801e427691e633 Mon Sep 17 00:00:00 2001 From: "Victor C." Date: Sun, 20 Jul 2025 08:43:50 +0200 Subject: [PATCH 17/32] Added publish flow --- .github/workflows/publish_pub_dev.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .github/workflows/publish_pub_dev.yml diff --git a/.github/workflows/publish_pub_dev.yml b/.github/workflows/publish_pub_dev.yml new file mode 100644 index 0000000..8105a6d --- /dev/null +++ b/.github/workflows/publish_pub_dev.yml @@ -0,0 +1,14 @@ +name: Publish to pub.dev + +on: + push: + tags: + - 'v[0-9]+.[0-9]+.[0-9]+*' + +jobs: + publish: + permissions: + id-token: write # Required for authentication using OIDC + uses: dart-lang/setup-dart/.github/workflows/publish.yml@v1 + # with: + # working-directory: path/to/package/within/repository \ No newline at end of file From 13127669c68ae65cc488b08eccd4963739da93d6 Mon Sep 17 00:00:00 2001 From: "Victor C." Date: Sun, 20 Jul 2025 08:50:21 +0200 Subject: [PATCH 18/32] Renamed file --- .github/workflows/{publish_pub_dev.yml => publish.yml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/workflows/{publish_pub_dev.yml => publish.yml} (100%) diff --git a/.github/workflows/publish_pub_dev.yml b/.github/workflows/publish.yml similarity index 100% rename from .github/workflows/publish_pub_dev.yml rename to .github/workflows/publish.yml From 3aa45a511afc373e091822102d3ce2495c67f1a9 Mon Sep 17 00:00:00 2001 From: Trivix Date: Thu, 1 Jan 2026 23:06:12 +0800 Subject: [PATCH 19/32] Add NaviTerm screenshot to README --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index ce8e698..65c6605 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,10 @@ SSH and SFTP client written in pure Dart, aiming to be feature-rich as well as e dartShell displaying terminal and session information for SSH operations + + + Screenshot of NaviTerm application built with dartssh2 + From f9d3a1d57c17264f5a24f05e20498fcaca25289a Mon Sep 17 00:00:00 2001 From: Trivix Date: Thu, 1 Jan 2026 23:24:21 +0800 Subject: [PATCH 20/32] add NaviTerm title and repository link Updated the showcase table to include the NaviTerm title and linked the name and image to the GitHub repository. --- README.md | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 65c6605..466ff7e 100644 --- a/README.md +++ b/README.md @@ -35,39 +35,37 @@ SSH and SFTP client written in pure Dart, aiming to be feature-rich as well as e - - - + - - - -
ServerBox NoPorts DartShell + Naviterm +
- ServerBox interface displaying connection management options - ServerBox user interface for server control and monitoring + ServerBox interface + ServerBox interface - NoPorts demo showcasing SSH connectivity without open ports + NoPorts demo - dartShell displaying terminal and session information for SSH operations + dartShell interface - Screenshot of NaviTerm application built with dartssh2 + + NaviTerm: Professional SSH Terminal for iOS +
From 2bf1ef21968538caeb9233a9380e667064cf2c5b Mon Sep 17 00:00:00 2001 From: Trivix Date: Thu, 1 Jan 2026 23:29:41 +0800 Subject: [PATCH 21/32] Update App Store link for NaviTerm --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 466ff7e..22af41b 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ SSH and SFTP client written in pure Dart, aiming to be feature-rich as well as e dartShell interface - + NaviTerm: Professional SSH Terminal for iOS From 416d74efb7bbaa4fc736ef528944a85b2802128a Mon Sep 17 00:00:00 2001 From: Trivix Date: Thu, 1 Jan 2026 23:54:23 +0800 Subject: [PATCH 22/32] add NaviTerm to showcase and refine table structure Updated alt text for images to improve accessibility and clarity. --- README.md | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 22af41b..83e2809 100644 --- a/README.md +++ b/README.md @@ -35,33 +35,41 @@ SSH and SFTP client written in pure Dart, aiming to be feature-rich as well as e + + + + + + + + - + From 4499ab6ddafdf785432e4d1001d15e6fe51e5571 Mon Sep 17 00:00:00 2001 From: Ivgeni Segal Date: Fri, 13 Feb 2026 07:19:21 -0800 Subject: [PATCH 24/32] Introducing domain sockets as forwarding connect destination --- lib/src/message/msg_channel.dart | 39 ++++++++++++++++++++++++++++++++ lib/src/ssh_client.dart | 30 ++++++++++++++++++++++++ 2 files changed, 69 insertions(+) diff --git a/lib/src/message/msg_channel.dart b/lib/src/message/msg_channel.dart index cab4111..5d49884 100644 --- a/lib/src/message/msg_channel.dart +++ b/lib/src/message/msg_channel.dart @@ -24,6 +24,9 @@ class SSH_Message_Channel_Open implements SSHMessage { final String? originatorIP; final int? originatorPort; + /// "direct-streamlocal@openssh.com" channel specific data. + final String? socketPath; + SSH_Message_Channel_Open({ required this.channelType, required this.senderChannel, @@ -33,6 +36,7 @@ class SSH_Message_Channel_Open implements SSHMessage { this.port, this.originatorIP, this.originatorPort, + this.socketPath, }); factory SSH_Message_Channel_Open.session({ @@ -107,6 +111,23 @@ class SSH_Message_Channel_Open implements SSHMessage { ); } + /// Opens a channel to forward data to a Unix domain socket on the remote + /// side. See OpenSSH PROTOCOL, section 2.4. + factory SSH_Message_Channel_Open.directStreamLocal({ + required int senderChannel, + required int initialWindowSize, + required int maximumPacketSize, + required String socketPath, + }) { + return SSH_Message_Channel_Open( + channelType: 'direct-streamlocal@openssh.com', + senderChannel: senderChannel, + initialWindowSize: initialWindowSize, + maximumPacketSize: maximumPacketSize, + socketPath: socketPath, + ); + } + factory SSH_Message_Channel_Open.decode(Uint8List bytes) { final reader = SSHMessageReader(bytes); reader.skip(1); @@ -159,6 +180,17 @@ class SSH_Message_Channel_Open implements SSHMessage { originatorIP: originatorIP, originatorPort: originatorPort, ); + case 'direct-streamlocal@openssh.com': + final socketPath = reader.readUtf8(); + // reserved string and uint32 per OpenSSH PROTOCOL spec + reader.readUtf8(); + reader.readUint32(); + return SSH_Message_Channel_Open.directStreamLocal( + senderChannel: senderChannel, + initialWindowSize: initialWindowSize, + maximumPacketSize: maximumPacketSize, + socketPath: socketPath, + ); default: return SSH_Message_Channel_Open( @@ -197,6 +229,11 @@ class SSH_Message_Channel_Open implements SSHMessage { writer.writeUtf8(originatorIP!); writer.writeUint32(originatorPort!); break; + case 'direct-streamlocal@openssh.com': + writer.writeUtf8(socketPath!); + writer.writeUtf8(''); // reserved + writer.writeUint32(0); // reserved + break; } return writer.takeBytes(); } @@ -212,6 +249,8 @@ class SSH_Message_Channel_Open implements SSHMessage { return 'SSH_Message_Channel_Open(channelType: $channelType, senderChannel: $senderChannel, initialWindowSize: $initialWindowSize, maximumPacketSize: $maximumPacketSize, host: $host, port: $port, originatorIP: $originatorIP, originatorPort: $originatorPort)'; case 'direct-tcpip': return 'SSH_Message_Channel_Open(channelType: $channelType, senderChannel: $senderChannel, initialWindowSize: $initialWindowSize, maximumPacketSize: $maximumPacketSize, host: $host, port: $port, originatorIP: $originatorIP, originatorPort: $originatorPort)'; + case 'direct-streamlocal@openssh.com': + return 'SSH_Message_Channel_Open(channelType: $channelType, senderChannel: $senderChannel, initialWindowSize: $initialWindowSize, maximumPacketSize: $maximumPacketSize, socketPath: $socketPath)'; default: return 'SSH_Message_Channel_Open(channelType: $channelType, senderChannel: $senderChannel, initialWindowSize: $initialWindowSize, maximumPacketSize: $maximumPacketSize)'; } diff --git a/lib/src/ssh_client.dart b/lib/src/ssh_client.dart index ee841f5..834a2c5 100644 --- a/lib/src/ssh_client.dart +++ b/lib/src/ssh_client.dart @@ -297,6 +297,20 @@ class SSHClient { return SSHForwardChannel(channelController.channel); } + /// Forward connections to a Unix domain socket at [remoteSocketPath] on the + /// remote side via a `direct-streamlocal@openssh.com` channel. + /// + /// This is the equivalent of `ssh -L localPort:remoteSocketPath`. + Future forwardLocalUnix( + String remoteSocketPath, + ) async { + await _authenticated.future; + final channelController = await _openForwardLocalUnixChannel( + remoteSocketPath, + ); + return SSHForwardChannel(channelController.channel); + } + /// Execute [command] on the remote side. Returns a [SSHChannel] that can be /// used to read and write to the remote side. Future execute( @@ -953,6 +967,22 @@ class SSHClient { return await _waitChannelOpen(localChannelId); } + Future _openForwardLocalUnixChannel( + String socketPath, + ) async { + final localChannelId = _channelIdAllocator.allocate(); + + final request = SSH_Message_Channel_Open.directStreamLocal( + senderChannel: localChannelId, + initialWindowSize: _initialWindowSize, + maximumPacketSize: _maximumPacketSize, + socketPath: socketPath, + ); + _sendMessage(request); + + return await _waitChannelOpen(localChannelId); + } + Future _waitChannelOpen( SSHChannelId localChannelId, ) async { From 81e88d7d4af89c170f06baf31fc471e1b64f1d9f Mon Sep 17 00:00:00 2001 From: Ivgeni Segal Date: Sat, 14 Feb 2026 13:02:14 -0800 Subject: [PATCH 25/32] Add example of forwarding to a remote unix domain socket --- example/forward_local_unix.dart | 33 +++++++++++++++++++++++++++++++++ lib/src/ssh_client.dart | 2 +- 2 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 example/forward_local_unix.dart diff --git a/example/forward_local_unix.dart b/example/forward_local_unix.dart new file mode 100644 index 0000000..853ef00 --- /dev/null +++ b/example/forward_local_unix.dart @@ -0,0 +1,33 @@ +import 'dart:io'; + +import 'package:dartssh2/dartssh2.dart'; + +/// Example of forwarding a local TCP port to a remote Unix domain socket using `ssh -L localPort:remoteSocketPath`. +void main(List args) async { + final socket = await SSHSocket.connect('localhost', 22); + + final client = SSHClient( + socket, + username: 'root', + onPasswordRequest: () { + stdout.write('Password: '); + stdin.echoMode = false; + return stdin.readLineSync() ?? exit(1); + }, + ); + + await client.authenticated; + + final serverSocket = await ServerSocket.bind('localhost', 8080); + + print('Listening on ${serverSocket.address.address}:${serverSocket.port}'); + + await for (final socket in serverSocket) { + final forward = await client.forwardLocalUnix('/var/run/docker.sock'); + forward.stream.cast>().pipe(socket); + socket.cast>().pipe(forward.sink); + } + + client.close(); + await client.done; +} diff --git a/lib/src/ssh_client.dart b/lib/src/ssh_client.dart index 834a2c5..723d9b3 100644 --- a/lib/src/ssh_client.dart +++ b/lib/src/ssh_client.dart @@ -297,7 +297,7 @@ class SSHClient { return SSHForwardChannel(channelController.channel); } - /// Forward connections to a Unix domain socket at [remoteSocketPath] on the + /// Forward local connections to a remote Unix domain socket at [remoteSocketPath] on the /// remote side via a `direct-streamlocal@openssh.com` channel. /// /// This is the equivalent of `ssh -L localPort:remoteSocketPath`. From 963e9f9f7012ee185a272a703af4c83f5c001d7a Mon Sep 17 00:00:00 2001 From: Ivgeni Segal Date: Sat, 14 Mar 2026 08:07:13 -0700 Subject: [PATCH 26/32] Fix potential hang when closing channel while opening --- lib/src/ssh_client.dart | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/lib/src/ssh_client.dart b/lib/src/ssh_client.dart index 723d9b3..931d7ca 100644 --- a/lib/src/ssh_client.dart +++ b/lib/src/ssh_client.dart @@ -496,6 +496,17 @@ class SSHClient { } _keepAlive?.stop(); + // Complete any pending channel-open waiters so callers (e.g. + // forwardLocalUnix) don't hang forever when the connection drops. + for (final entry in _channelOpenReplyWaiters.entries) { + if (!entry.value.isCompleted) { + entry.value.completeError( + SSHStateError('Connection closed while waiting for channel open'), + ); + } + } + _channelOpenReplyWaiters.clear(); + try { _closeChannels(); } catch (e) { From e7a38eea466ca3e74e1ccebdef6e1f190358005b Mon Sep 17 00:00:00 2001 From: "yuanyuan.liu" Date: Wed, 18 Mar 2026 21:44:59 +0800 Subject: [PATCH 27/32] register channel --- lib/src/ssh_client.dart | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/lib/src/ssh_client.dart b/lib/src/ssh_client.dart index ee841f5..4b67115 100644 --- a/lib/src/ssh_client.dart +++ b/lib/src/ssh_client.dart @@ -751,6 +751,17 @@ class SSHClient { void _handleChannelConfirmation(Uint8List payload) { final message = SSH_Message_Channel_Confirmation.decode(payload); printTrace?.call('<- $socket: $message'); + // Register the channel synchronously BEFORE completing the future. + // CHANNEL_DATA for this channel may arrive in the same TCP segment as + // the CONFIRMATION. If we defer registration to the async continuation + // of _waitChannelOpen, that data hits _handleChannelData while + // _channels[id] is still null and is silently dropped. + _acceptChannel( + localChannelId: message.recipientChannel, + remoteChannelId: message.senderChannel, + remoteInitialWindowSize: message.initialWindowSize, + remoteMaximumPacketSize: message.maximumPacketSize, + ); _dispatchChannelOpenReply(message.recipientChannel, message); } @@ -961,17 +972,8 @@ class SSHClient { throw SSHChannelOpenError(message.reasonCode, message.description); } - final reply = message as SSH_Message_Channel_Confirmation; - if (reply.recipientChannel != localChannelId) { - throw SSHStateError('Unexpected channel confirmation'); - } - - return _acceptChannel( - localChannelId: localChannelId, - remoteChannelId: reply.senderChannel, - remoteInitialWindowSize: reply.initialWindowSize, - remoteMaximumPacketSize: reply.maximumPacketSize, - ); + // Channel was already registered synchronously in _handleChannelConfirmation. + return _channels[localChannelId]!; } SSHChannelController _acceptChannel({ From 7b1a4390c9e9859285b90619ee2e1acb7903045a Mon Sep 17 00:00:00 2001 From: Victor Carreras <34163765+vicajilau@users.noreply.github.com> Date: Thu, 19 Mar 2026 18:56:12 +0100 Subject: [PATCH 28/32] Bump version to 2.14.0 and update CHANGELOG for SSH connection fix --- CHANGELOG.md | 3 +++ pubspec.yaml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7bd0f49..2a302d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## [2.14.0] - 2026-03-19 +- Fixed SSH connections through bastion hosts where the target server sends its version string immediately upon connection (which is standard behavior per RFC 4253) [#141]. Thanks @shihuili1218. + ## [2.13.0] - 2025-06-22 - docs: Update NoPorts naming [#115]. [@XavierChanth]. - Add parameter disableHostkeyVerification [#123]. Thanks [@alexander-irion]. diff --git a/pubspec.yaml b/pubspec.yaml index 9d75950..ae33b99 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: dartssh2 -version: 2.13.0 +version: 2.14.0 description: SSH and SFTP client written in pure Dart, aiming to be feature-rich as well as easy to use. homepage: https://github.com/TerminalStudio/dartssh2 From 78cf43a0061a347d6ea4742d3b21729d1213c6f5 Mon Sep 17 00:00:00 2001 From: Victor Carreras <34163765+vicajilau@users.noreply.github.com> Date: Thu, 19 Mar 2026 19:01:53 +0100 Subject: [PATCH 29/32] Update GitHub Actions workflow to include pull request triggers and modify SSHSocket test connection --- .github/workflows/dart.yml | 8 +++++++- test/src/socket/ssh_socket_io_test.dart | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/dart.yml b/.github/workflows/dart.yml index 6ee41a6..f3c339d 100644 --- a/.github/workflows/dart.yml +++ b/.github/workflows/dart.yml @@ -1,6 +1,12 @@ name: Dart -on: push +on: + push: + branches: + - master + pull_request: + branches: + - master jobs: test: diff --git a/test/src/socket/ssh_socket_io_test.dart b/test/src/socket/ssh_socket_io_test.dart index 82c4be9..626f678 100644 --- a/test/src/socket/ssh_socket_io_test.dart +++ b/test/src/socket/ssh_socket_io_test.dart @@ -4,7 +4,7 @@ import 'package:test/test.dart'; void main() { group('SSHSocket', () { test('can establish tcp connections', () async { - final socket = await SSHSocket.connect('time.nist.gov', 13); + final socket = await SSHSocket.connect('test.rebex.net', 22); final firstPacket = await socket.stream.first; expect(firstPacket, isNotEmpty); await socket.close(); From 6ae02d4a08ffa3a4e9d5bfca1843644eda7b668f Mon Sep 17 00:00:00 2001 From: Victor Carreras <34163765+vicajilau@users.noreply.github.com> Date: Thu, 19 Mar 2026 19:11:38 +0100 Subject: [PATCH 30/32] Add forwardLocalUnix() function to CHANGELOG for SSH forwarding --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a302d9..3e4322d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ ## [2.14.0] - 2026-03-19 - Fixed SSH connections through bastion hosts where the target server sends its version string immediately upon connection (which is standard behavior per RFC 4253) [#141]. Thanks @shihuili1218. +- Adds a new forwardLocalUnix() function, which is an equivalent of ssh -L localPort:remoteSocketPath [#140]. Thanks @isegal. + ## [2.13.0] - 2025-06-22 - docs: Update NoPorts naming [#115]. [@XavierChanth]. From 27a6480c8edfe5a7f5920ca330213e45b798e18e Mon Sep 17 00:00:00 2001 From: GT610 Date: Mon, 30 Mar 2026 11:01:49 +0800 Subject: [PATCH 31/32] feat(forward): Adds local Unix domain socket forwarding functionality Implements the ability to forward a local connection to a remote Unix domain socket via the `direct-streamlocal@openssh.com` channel, equivalent to the `ssh -L localPort:remoteSocketPath` command --- CHANGELOG.md | 4 ++- example/forward_local_unix.dart | 9 +++++- lib/src/algorithm/ssh_mac_type.dart | 2 +- lib/src/ssh_algorithm.dart | 9 +++--- lib/src/ssh_client.dart | 33 +++++++++++++++++++- test/src/algorithm/ssh_cipher_type_test.dart | 6 ++-- 6 files changed, 51 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e4322d..5d07cb2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ ## [2.13.0] - 2025-06-22 - docs: Update NoPorts naming [#115]. [@XavierChanth]. - Add parameter disableHostkeyVerification [#123]. Thanks [@alexander-irion]. -- Add support for server initiated re-keying [#125]. Thanks [@MarBazuz]. +- Add support for server-initiated re-keying [#125]. Thanks [@MarBazuz]. - Add support for new algorithms "mac-sha2-256-96", "hmac-sha2-512-96", "hmac-sha2-256-etm@openssh.com", "hmac-sha2-512-etm@openssh.com" [#126] [#127]. Thanks [@reinbeumer]. ## [2.12.0] - 2025-02-08 @@ -176,6 +176,8 @@ - Initial release. +[#141]: https://github.com/TerminalStudio/dartssh2/pull/141 +[#140]: https://github.com/TerminalStudio/dartssh2/pull/140 [#127]: https://github.com/TerminalStudio/dartssh2/pull/127 [#126]: https://github.com/TerminalStudio/dartssh2/pull/126 [#125]: https://github.com/TerminalStudio/dartssh2/pull/125 diff --git a/example/forward_local_unix.dart b/example/forward_local_unix.dart index 853ef00..e8791a4 100644 --- a/example/forward_local_unix.dart +++ b/example/forward_local_unix.dart @@ -12,7 +12,14 @@ void main(List args) async { onPasswordRequest: () { stdout.write('Password: '); stdin.echoMode = false; - return stdin.readLineSync() ?? exit(1); + String? password; + try { + password = stdin.readLineSync(); + } finally { + stdin.echoMode = true; + } + if (password == null) exit(1); + return password; }, ); diff --git a/lib/src/algorithm/ssh_mac_type.dart b/lib/src/algorithm/ssh_mac_type.dart index a263f30..de37ab3 100644 --- a/lib/src/algorithm/ssh_mac_type.dart +++ b/lib/src/algorithm/ssh_mac_type.dart @@ -32,7 +32,6 @@ class SSHMacType extends SSHAlgorithm { // Non-standard MAC: RFC 6668/IANA only standardizes hmac-sha2-256 and hmac-sha2-512. // These use custom names and are not IANA-registered. - // added by Rein static const hmacSha256_96 = SSHMacType._( name: 'hmac-sha2-256-96', keySize: 32, @@ -59,6 +58,7 @@ class SSHMacType extends SSHAlgorithm { isEtm: true, ); // end added by Rein + const SSHMacType._({ required this.name, required this.keySize, diff --git a/lib/src/ssh_algorithm.dart b/lib/src/ssh_algorithm.dart index 345d50c..652303f 100644 --- a/lib/src/ssh_algorithm.dart +++ b/lib/src/ssh_algorithm.dart @@ -53,7 +53,6 @@ abstract class SSHAlgorithm { return true; } - @override String toString() { assert(isValidAlgorithmName, 'Invalid algorithm name: $name'); @@ -131,17 +130,17 @@ class SSHAlgorithms { SSHCipherType.aes256cbc, SSHCipherType.aes128cbc, ], - // Prefer modern SHA-2 MACs by default; keep SHA-1 as fallback and MD5 last. + // Prefer modern SHA-2 MACs by default; full-length variants first, + // ETM variants for better security, truncated 96-bit as last-resort fallback. this.mac = const [ - SSHMacType.hmacSha256_96, - SSHMacType.hmacSha512_96, SSHMacType.hmacSha256Etm, SSHMacType.hmacSha512Etm, - SSHMacType.hmacSha1, SSHMacType.hmacSha256, SSHMacType.hmacSha512, SSHMacType.hmacSha1, SSHMacType.hmacMd5, + SSHMacType.hmacSha256_96, + SSHMacType.hmacSha512_96, ], }); } diff --git a/lib/src/ssh_client.dart b/lib/src/ssh_client.dart index 9177034..6e01249 100644 --- a/lib/src/ssh_client.dart +++ b/lib/src/ssh_client.dart @@ -357,6 +357,21 @@ class SSHClient { return SSHForwardChannel(channelController.channel); } + /// Forward local connections to a remote Unix domain socket at + /// [remoteSocketPath] on the remote side via a + /// `direct-streamlocal@openssh.com` channel. + /// + /// This is the equivalent of `ssh -L localPort:remoteSocketPath`. + Future forwardLocalUnix( + String remoteSocketPath, + ) async { + await _authenticated.future; + final channelController = await _openForwardLocalUnixChannel( + remoteSocketPath, + ); + return SSHForwardChannel(channelController.channel); + } + /// Execute [command] on the remote side. Returns a [SSHChannel] that can be /// used to read and write to the remote side. Future execute( @@ -1265,6 +1280,22 @@ class SSHClient { return await _waitChannelOpen(localChannelId); } + Future _openForwardLocalUnixChannel( + String socketPath, + ) async { + final localChannelId = _channelIdAllocator.allocate(); + + final request = SSH_Message_Channel_Open.directStreamLocal( + senderChannel: localChannelId, + initialWindowSize: _initialWindowSize, + maximumPacketSize: _maximumPacketSize, + socketPath: socketPath, + ); + _sendMessage(request); + + return await _waitChannelOpen(localChannelId); + } + Future _waitChannelOpen( SSHChannelId localChannelId, ) async { @@ -1466,4 +1497,4 @@ class SSHRemoteForward { @override String toString() => '$runtimeType($host:$port)'; -} \ No newline at end of file +} diff --git a/test/src/algorithm/ssh_cipher_type_test.dart b/test/src/algorithm/ssh_cipher_type_test.dart index 565dae2..70755d9 100644 --- a/test/src/algorithm/ssh_cipher_type_test.dart +++ b/test/src/algorithm/ssh_cipher_type_test.dart @@ -88,14 +88,14 @@ void main() { expect( algorithms.mac, equals([ - SSHMacType.hmacSha256_96, - SSHMacType.hmacSha512_96, SSHMacType.hmacSha256Etm, SSHMacType.hmacSha512Etm, - SSHMacType.hmacSha1, SSHMacType.hmacSha256, SSHMacType.hmacSha512, + SSHMacType.hmacSha1, SSHMacType.hmacMd5, + SSHMacType.hmacSha256_96, + SSHMacType.hmacSha512_96, ])); }); } From 79059a37880039319032392465f994b011c99a8c Mon Sep 17 00:00:00 2001 From: GT610 Date: Mon, 30 Mar 2026 11:35:37 +0800 Subject: [PATCH 32/32] docs (CHANGELOG): Added missing PR links #116 and #115 --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d07cb2..f984f0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -182,6 +182,8 @@ [#126]: https://github.com/TerminalStudio/dartssh2/pull/126 [#125]: https://github.com/TerminalStudio/dartssh2/pull/125 [#123]: https://github.com/TerminalStudio/dartssh2/pull/123 +[#116]: https://github.com/TerminalStudio/dartssh2/pull/116 +[#115]: https://github.com/TerminalStudio/dartssh2/pull/115 [#101]: https://github.com/TerminalStudio/dartssh2/pull/101 [#100]: https://github.com/TerminalStudio/dartssh2/issues/100 [#80]: https://github.com/TerminalStudio/dartssh2/issues/80
ServerBox NoPorts DartShell Naviterm
- ServerBox interface - ServerBox interface + ServerBox interface displaying connection management options + ServerBox user interface for server control and monitoring - NoPorts demo + NoPorts demo showcasing SSH connectivity without open ports - dartShell interface + dartShell displaying terminal and session information for SSH operations NaviTerm: Professional SSH Terminal for iOS From b2e1264ca3744a79bacd007fb80cc58fef45c06d Mon Sep 17 00:00:00 2001 From: Trivix Date: Fri, 2 Jan 2026 00:01:06 +0800 Subject: [PATCH 23/32] Update image alt text for clarity --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 83e2809..85bbaa3 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ SSH and SFTP client written in pure Dart, aiming to be feature-rich as well as e ServerBox interface displaying connection management options ServerBox user interface for server control and monitoring NoPorts demo showcasing SSH connectivity without open ports @@ -72,7 +72,7 @@ SSH and SFTP client written in pure Dart, aiming to be feature-rich as well as e - NaviTerm: Professional SSH Terminal for iOS + Your all-in-one SSH terminal, SFTP client, and port forwarding tool, built from the ground up for macOS, iPhone, and iPad.