From 9c312208554a70ee82f1bdb89d46a9396a89bf89 Mon Sep 17 00:00:00 2001 From: Victor Carreras <34163765+vicajilau@users.noreply.github.com> Date: Tue, 30 Jun 2026 17:06:59 +0200 Subject: [PATCH] fix: fix connection drop with aes-gcm ciphers Fixes #168 --- CHANGELOG.md | 1 + lib/src/ssh_transport.dart | 10 ++- test/src/ssh_transport_aead_test.dart | 98 ++++++++++++++------------- 3 files changed, 61 insertions(+), 48 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e37ce4f..8159d65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ - Added chunked transfer-encoding decoding for HTTP response bodies according to RFC 7230 ยง4.1, improving interoperability with HTTP/1.1 servers [#171]. Thanks [@GT-610]. - Added support for OpenSSH's `posix-rename@openssh.com` SFTP extension to perform atomic renames with POSIX semantics (replace destination if it exists) when advertised by the server [#172]. Thanks [@GT-610]. - Added `SftpFile.downloadToRandomAccess` to download a remote file directly into a `dart:io` `RandomAccessFile` using out-of-order pipelined writes, maximizing download performance on high-latency links [#173]. Thanks [@GT-610]. +- Fixed a connection drop bug during AEAD (AES-GCM) decryption caused by incorrect padding length validation offset calculation [#168]. Thanks [@nuclear06]. ## [2.18.0] - 2026-05-18 - Fixed AES-GCM cipher encryption and decryption sequence number/nonce counter resetting during key exchanges [#165]. Thanks [@vicajilau]. diff --git a/lib/src/ssh_transport.dart b/lib/src/ssh_transport.dart index cf1accb..d05a769 100644 --- a/lib/src/ssh_transport.dart +++ b/lib/src/ssh_transport.dart @@ -758,7 +758,15 @@ class SSHTransport { final paddingLength = plaintext[0]; final payloadLength = packetLength - paddingLength - 1; - _verifyPacketPadding(payloadLength, paddingLength); + + final minPaddingLength = + _alignedPaddingLength(payloadLength, cipherType.blockSize); + if (paddingLength < minPaddingLength) { + throw SSHPacketError( + 'Invalid padding length: $paddingLength, expected: $minPaddingLength', + ); + } + return Uint8List.sublistView(plaintext, 1, 1 + payloadLength); } diff --git a/test/src/ssh_transport_aead_test.dart b/test/src/ssh_transport_aead_test.dart index 6718738..a3b9bc3 100644 --- a/test/src/ssh_transport_aead_test.dart +++ b/test/src/ssh_transport_aead_test.dart @@ -39,53 +39,57 @@ void main() { iv[i] = i + 16; } - final senderSocket = _CaptureSSHSocket(); - final sender = SSHTransport( - senderSocket, - algorithms: const SSHAlgorithms( - cipher: [SSHCipherType.aes128gcm], - ), - ); - - setPrivate(sender, '_clientCipherType', SSHCipherType.aes128gcm); - setPrivate(sender, '_localCipherKey', key); - setPrivate(sender, '_localIV', iv); - setPrivate(sender, '_kexInProgress', false); - setSequenceValue(sender, '_localPacketSN', 0); - - final payload = Uint8List.fromList([250, 1, 2, 3, 4, 5]); - sender.sendPacket(payload); - - final encryptedPacket = senderSocket.packets.last; - - final receiverSocket = _CaptureSSHSocket(); - final receivedPacket = Completer(); - final receiver = SSHTransport( - receiverSocket, - algorithms: const SSHAlgorithms( - cipher: [SSHCipherType.aes128gcm], - ), - onPacket: (packet) { - if (!receivedPacket.isCompleted) { - receivedPacket.complete(packet); - } - }, - ); - - setPrivate(receiver, '_remoteVersion', 'SSH-2.0-test'); - setPrivate(receiver, '_serverCipherType', SSHCipherType.aes128gcm); - setPrivate(receiver, '_remoteCipherKey', key); - setPrivate(receiver, '_remoteIV', iv); - setSequenceValue(receiver, '_remotePacketSN', 0); - - receiverSocket.addIncomingBytes(encryptedPacket); - - final received = - await receivedPacket.future.timeout(const Duration(seconds: 2)); - expect(received, payload); - - sender.close(); - receiver.close(); + for (final payloadLength in [1, 5, 6, 10, 11, 15, 16, 20, 31, 32]) { + final senderSocket = _CaptureSSHSocket(); + final sender = SSHTransport( + senderSocket, + algorithms: const SSHAlgorithms( + cipher: [SSHCipherType.aes128gcm], + ), + ); + + setPrivate(sender, '_clientCipherType', SSHCipherType.aes128gcm); + setPrivate(sender, '_localCipherKey', key); + setPrivate(sender, '_localIV', iv); + setPrivate(sender, '_kexInProgress', false); + setSequenceValue(sender, '_localPacketSN', 0); + + final payload = Uint8List.fromList( + List.generate(payloadLength, (index) => index % 256), + ); + sender.sendPacket(payload); + + final encryptedPacket = senderSocket.packets.last; + + final receiverSocket = _CaptureSSHSocket(); + final receivedPacket = Completer(); + final receiver = SSHTransport( + receiverSocket, + algorithms: const SSHAlgorithms( + cipher: [SSHCipherType.aes128gcm], + ), + onPacket: (packet) { + if (!receivedPacket.isCompleted) { + receivedPacket.complete(packet); + } + }, + ); + + setPrivate(receiver, '_remoteVersion', 'SSH-2.0-test'); + setPrivate(receiver, '_serverCipherType', SSHCipherType.aes128gcm); + setPrivate(receiver, '_remoteCipherKey', key); + setPrivate(receiver, '_remoteIV', iv); + setSequenceValue(receiver, '_remotePacketSN', 0); + + receiverSocket.addIncomingBytes(encryptedPacket); + + final received = + await receivedPacket.future.timeout(const Duration(seconds: 2)); + expect(received, payload); + + sender.close(); + receiver.close(); + } }); test('reports AEAD authentication failure when packet is tampered',