Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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].
Expand Down
10 changes: 9 additions & 1 deletion lib/src/ssh_transport.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
98 changes: 51 additions & 47 deletions test/src/ssh_transport_aead_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<Uint8List>();
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<Uint8List>();
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',
Expand Down
Loading