Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
51ed7a2
fix(web): make SSHSocket import wasm-compatible
vicajilau Mar 24, 2026
1407474
chore(release): bump version to 2.17.0 and update CHANGELOG for Web/W…
vicajilau Mar 24, 2026
fcf2de7
fix(changelog): update issue reference for Web/WASM compatibility imp…
vicajilau Mar 24, 2026
c37417d
fix(changelog): correct issue reference for Web/WASM compatibility im…
vicajilau Mar 24, 2026
c310497
Merge pull request #153 from TerminalStudio/feat/wasm-web-compat-sock…
vicajilau Mar 24, 2026
66384b6
feat: add dynamic SOCKS5 forwarding API
vicajilau Mar 25, 2026
57f426a
test: cover dynamic forward socks flow
vicajilau Mar 25, 2026
10f1da8
test: add comprehensive tests for dynamic SOCKS5 forwarding behavior
vicajilau Mar 26, 2026
080d6d4
test: add unit tests for SSHClient.forwardDynamic authentication flow
vicajilau Mar 26, 2026
a2f57cf
Merge pull request #154 from TerminalStudio/feat/dynamic-forward-socks5
vicajilau Mar 26, 2026
b54f461
feat: start AEAD groundwork with AES-GCM transport support
vicajilau Mar 26, 2026
d60caf3
feat: update CHANGELOG and README to include AES-GCM AEAD groundwork …
vicajilau Mar 26, 2026
2e7887b
fix: keep AES-GCM opt-in to avoid auth regressions
vicajilau Mar 26, 2026
20bad41
feat: update README and CHANGELOG to clarify AES-GCM opt-in status an…
vicajilau Mar 26, 2026
6616eb9
feat: add tests for AES-GCM cipher resolution and SSHTransport AEAD f…
vicajilau Mar 27, 2026
9182d10
refactor: remove unused imports in SSHTransport AEAD test
vicajilau Mar 27, 2026
ea6766f
feat: add AEAD cipher tests and enhance SSHTransport functionality
vicajilau Mar 27, 2026
3b57dcd
Merge pull request #155 from TerminalStudio/feat/issue-26-aead-ground…
vicajilau Mar 27, 2026
6977b3c
docs: add note about SSHSocket.connect() limitations for Flutter Web …
vicajilau Mar 28, 2026
d41840a
chore: update release date for version 2.17.0 in CHANGELOG.md
vicajilau Mar 28, 2026
a386ef1
Merge upstream v2.17.0
GT-610 Mar 30, 2026
4db9a55
fix(transport): Fixed AEAD integrity checks and improved random padding
GT-610 Mar 30, 2026
f13cb8f
fix: Fixed an issue with resource release when a dynamic forwarding c…
GT-610 Mar 30, 2026
52f8e3e
fix: Fixed issues with SSH dynamic forwarding and connection closure
GT-610 Mar 30, 2026
f846249
refactor: Simplify connection closure logic and optimize SSH transpor…
GT-610 Mar 31, 2026
9bd5f6e
refactor: Optimize code formatting and EOF handling logic
GT-610 Mar 31, 2026
20eb905
refactor(SSHTransport): Clean up unused AEAD encryption-related code
GT-610 Mar 31, 2026
5e3db54
fix: Fixed issues with connection limits and concurrent dialing
GT-610 Mar 31, 2026
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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## [2.17.0] - 2026-03-28
- Improved Web/WASM compatibility by updating `SSHSocket` conditional imports so web runtimes consistently use the web socket shim and avoid incorrect native socket selection [#88]. Thanks [@vicajilau].
- Added local dynamic forwarding (`SSHClient.forwardDynamic`) with SOCKS5 `NO AUTH` + `CONNECT`, including configurable handshake/connect timeouts and connection limits [#153].
- Added AES-GCM (`aes128-gcm@openssh.com`, `aes256-gcm@openssh.com`) AEAD groundwork in transport and cipher negotiation; currently opt-in (not enabled by default yet). `chacha20-poly1305@openssh.com` remains pending [#26]. Thanks [@vicajilau].

## [2.16.0] - 2026-03-24
- **BREAKING**: Changed `SSHChannelController.sendEnv()` from `void` to `Future<bool>` to properly await environment variable setup responses and avoid race conditions with PTY requests [#102]. Thanks [@itzhoujun] and [@vicajilau].
- Clarified shell stdio wiring for CLI-only usage and guarded `example/shell.dart` against missing local terminal handles (for example GUI-launched Windows `.exe`) [#121]. Thanks [@bradmartin333] and [@vicajilau].
Expand Down Expand Up @@ -196,12 +201,15 @@
[#145]: https://github.com/TerminalStudio/dartssh2/pull/145
[#141]: https://github.com/TerminalStudio/dartssh2/pull/141
[#140]: https://github.com/TerminalStudio/dartssh2/pull/140
[#153]: https://github.com/TerminalStudio/dartssh2/pull/153
[#102]: https://github.com/TerminalStudio/dartssh2/issues/102
[#99]: https://github.com/TerminalStudio/dartssh2/issues/99
[#109]: https://github.com/TerminalStudio/dartssh2/issues/109
[#121]: https://github.com/TerminalStudio/dartssh2/issues/121
[#124]: https://github.com/TerminalStudio/dartssh2/issues/124
[#95]: https://github.com/TerminalStudio/dartssh2/issues/95
[#88]: https://github.com/TerminalStudio/dartssh2/issues/88
[#26]: https://github.com/TerminalStudio/dartssh2/issues/26
[#139]: https://github.com/TerminalStudio/dartssh2/pull/139
[#133]: https://github.com/TerminalStudio/dartssh2/pull/133
[#132]: https://github.com/TerminalStudio/dartssh2/pull/132
Expand Down
70 changes: 69 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ SSH and SFTP client written in pure Dart, aiming to be feature-rich as well as e
- **Pure Dart**: Working with both Dart VM and Flutter.
- **SSH Session**: Executing commands, spawning shells, setting environment variables, pseudo terminals, etc.
- **Authentication**: Supports password, private key and interactive authentication method.
- **Forwarding**: Supports local forwarding and remote forwarding.
- **Forwarding**: Supports local forwarding, remote forwarding, and dynamic forwarding (SOCKS5 CONNECT).
- **SFTP**: Supports all operations defined in [SFTPv3 protocol](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02) including upload, download, list, link, remove, rename, etc.

## 🧬 Built with dartssh2
Expand Down Expand Up @@ -117,6 +117,10 @@ void main() async {
}
```

> Note: `SSHSocket.connect()` uses native TCP sockets (`dart:io`) and is not
> available on Flutter Web / Dart Web. See [Web support](#web-support) below
> for browser-compatible transport options.

> `SSHSocket` is an interface and it's possible to implement your own `SSHSocket` if you want to use a different underlying transport rather than standard TCP socket. For example WebSocket or Unix domain socket.

### Web support
Expand Down Expand Up @@ -296,6 +300,40 @@ void main() async {
}
```

### Start a local SOCKS5 proxy through SSH (`ssh -D` style)

```dart
void main() async {
final dynamicForward = await client.forwardDynamic(
bindHost: '127.0.0.1',
bindPort: 1080,
options: const SSHDynamicForwardOptions(
handshakeTimeout: Duration(seconds: 10),
connectTimeout: Duration(seconds: 15),
maxConnections: 128,
),
filter: (host, port) {
// Optional allow/deny policy.
return true;
},
);

print('SOCKS5 proxy at ${dynamicForward.host}:${dynamicForward.port}');
}
```

This currently supports SOCKS5 `NO AUTH` + `CONNECT`.
It requires `dart:io` and is not available on web runtimes.

Quick verification from your terminal:

```sh
curl --proxy socks5h://127.0.0.1:1080 https://ifconfig.me
```

If the proxy is working, this command returns the public egress IP seen through
the SSH tunnel.

### Authenticate with public keys

```dart
Expand Down Expand Up @@ -591,9 +629,39 @@ void main() async {
- `diffie-hellman-group1-sha1 `

**Cipher**:
- `aes[128|256]-gcm@openssh.com`
- `aes[128|192|256]-ctr`
- `aes[128|192|256]-cbc`

AES-GCM is currently available as opt-in via `SSHAlgorithms(cipher: ...)`, and is not enabled in the default cipher preference list yet.

Example (opt-in AES-GCM with explicit fallback ciphers):

```dart
void main() async {
final client = SSHClient(
await SSHSocket.connect('localhost', 22),
username: '<username>',
onPasswordRequest: () => '<password>',
algorithms: const SSHAlgorithms(
cipher: [
SSHCipherType.aes256gcm,
SSHCipherType.aes128gcm,
SSHCipherType.aes256ctr,
SSHCipherType.aes128ctr,
SSHCipherType.aes256cbc,
SSHCipherType.aes128cbc,
],
),
);

// Use the client...
client.close();
}
```

`chacha20-poly1305@openssh.com` is not supported yet.

**Integrity**:
- `hmac-md5`
- `hmac-sha1`
Expand Down
55 changes: 55 additions & 0 deletions example/forward_dynamic.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import 'dart:async';
import 'dart:io';

import 'package:dartssh2/dartssh2.dart';

Future<void> main() async {
final host = Platform.environment['SSH_HOST'] ?? 'localhost';
final port = int.tryParse(Platform.environment['SSH_PORT'] ?? '') ?? 22;
final username = Platform.environment['SSH_USERNAME'] ?? 'root';
final password = Platform.environment['SSH_PASSWORD'];

final socket = await SSHSocket.connect(host, port);

final client = SSHClient(
socket,
username: username,
onPasswordRequest: () => password,
onVerifyHostKey: (host, verifier) {
print('WARNING: Host key verification disabled for testing.');
return true;
},
);

final dynamicForward = await client.forwardDynamic(
bindHost: '127.0.0.1',
bindPort: 1080,
);

print(
'SOCKS5 proxy ready on ${dynamicForward.host}:${dynamicForward.port}.',
);
print('Press Ctrl+C to stop.');

StreamSubscription<void>? sigintSub;
StreamSubscription<void>? sigtermSub;
var isShuttingDown = false;

Future<void> shutdown() async {
if (isShuttingDown) return;
isShuttingDown = true;

await sigintSub?.cancel();
await sigtermSub?.cancel();
await dynamicForward.close();
client.close();
await client.done;
exit(0);
}

sigintSub = ProcessSignal.sigint.watch().listen((_) => shutdown());
sigtermSub = ProcessSignal.sigterm.watch().listen((_) => shutdown());

await client.done;
await shutdown();
}
108 changes: 63 additions & 45 deletions lib/src/algorithm/ssh_cipher_type.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ import 'package:pointycastle/export.dart';

class SSHCipherType extends SSHAlgorithm {
static const values = [
aes128gcm,
aes256gcm,
aes128cbc,
aes192cbc,
aes256cbc,
aes128ctr,
aes192ctr,
aes256ctr,
aes128gcm,
aes256gcm,
chacha20poly1305,
];

Expand All @@ -34,6 +34,26 @@ class SSHCipherType extends SSHAlgorithm {
cipherFactory: _aesCtrFactory,
);

static const aes128gcm = SSHCipherType._(
name: 'aes128-gcm@openssh.com',
keySize: 16,
isAead: true,
ivSize: 12,
blockSize: 16,
aeadTagSize: 16,
cipherFactory: _aesGcmFactory,
);

static const aes256gcm = SSHCipherType._(
name: 'aes256-gcm@openssh.com',
keySize: 32,
isAead: true,
ivSize: 12,
blockSize: 16,
aeadTagSize: 16,
cipherFactory: _aesGcmFactory,
);

static const aes128cbc = SSHCipherType._(
name: 'aes128-cbc',
keySize: 16,
Expand All @@ -52,28 +72,14 @@ class SSHCipherType extends SSHAlgorithm {
cipherFactory: _aesCbcFactory,
);

static const aes128gcm = SSHCipherType._(
name: 'aes128-gcm@openssh.com',
keySize: 16,
cipherFactory: _aesGcmFactory,
isAEAD: true,
tagSize: 16,
);

static const aes256gcm = SSHCipherType._(
name: 'aes256-gcm@openssh.com',
keySize: 32,
cipherFactory: _aesGcmFactory,
isAEAD: true,
tagSize: 16,
);

static const chacha20poly1305 = SSHCipherType._(
name: 'chacha20-poly1305@openssh.com',
keySize: 32, // ChaCha20-Poly1305 uses a single 256-bit key
keySize: 32,
isAead: true,
ivSize: 12,
blockSize: 16,
aeadTagSize: 16,
cipherFactory: _chacha20Poly1305Factory,
isAEAD: true,
tagSize: 16,
);

static SSHCipherType? fromName(String name) {
Expand All @@ -88,37 +94,42 @@ class SSHCipherType extends SSHAlgorithm {
const SSHCipherType._({
required this.name,
required this.keySize,
required this.cipherFactory,
this.isAEAD = false,
this.tagSize = 0,
this.cipherFactory,
this.isAead = false,
this.aeadTagSize = 0,
this.ivSize = 16,
this.blockSize = 16,
});

/// The name of the algorithm. For example, `"aes256-ctr`"`.
@override
final String name;

final int keySize;

final int ivSize = 16;
final bool isAead;

final int blockSize = 16;
final int aeadTagSize;

/// Whether this is an AEAD cipher mode
final bool isAEAD;
final int ivSize;

/// Authentication tag size for AEAD modes
final int tagSize;
final int blockSize;

final dynamic Function() cipherFactory;
final dynamic Function()? cipherFactory;

/// Creates cipher for non-AEAD modes
BlockCipher createCipher(
Uint8List key,
Uint8List iv, {
required bool forEncryption,
}) {
if (isAEAD) {
throw StateError('Use createAEADCipher for AEAD modes');
if (isAead) {
throw UnsupportedError(
'AEAD ciphers are packet-level and do not expose BlockCipher',
);
}

final factory = cipherFactory;
if (factory == null) {
throw StateError('No block cipher factory configured for $name');
}

if (key.length != keySize) {
Expand All @@ -129,32 +140,42 @@ class SSHCipherType extends SSHAlgorithm {
throw ArgumentError.value(iv, 'iv', 'IV must be $ivSize bytes long');
}

final cipher = cipherFactory() as BlockCipher;
final cipher = factory();
cipher.init(forEncryption, ParametersWithIV(KeyParameter(key), iv));
return cipher;
}

/// Creates cipher for AEAD modes. Returns a dynamic AEAD cipher instance
/// (either AEADBlockCipher like GCM, or AEADCipher like ChaCha20-Poly1305)
/// supporting `init(...)` and `process(Uint8List)`.
dynamic createAEADCipher(
Uint8List key,
Uint8List nonce, {
required bool forEncryption,
Uint8List? aad,
}) {
if (!isAEAD) {
if (!isAead) {
throw StateError('Use createCipher for non-AEAD modes');
}

if (key.length != keySize) {
throw ArgumentError.value(key, 'key', 'Key must be $keySize bytes long');
}

final cipher = cipherFactory();
if (nonce.length != ivSize) {
throw ArgumentError.value(
nonce,
'nonce',
'Nonce must be $ivSize bytes long',
);
}

final factory = cipherFactory;
if (factory == null) {
throw StateError('No AEAD cipher factory configured for $name');
}

final cipher = factory();
final params = AEADParameters(
KeyParameter(key),
tagSize * 8, // tagSize in bits
aeadTagSize * 8,
nonce,
aad ?? Uint8List(0),
);
Expand All @@ -172,13 +193,10 @@ BlockCipher _aesCbcFactory() {
return CBCBlockCipher(AESEngine());
}

/// Creates AES-GCM cipher factory
dynamic _aesGcmFactory() {
return GCMBlockCipher(AESEngine());
}

/// Creates ChaCha20-Poly1305 AEAD cipher factory
dynamic _chacha20Poly1305Factory() {
// Construct AEAD from underlying ChaCha20 engine and Poly1305 MAC
return ChaCha20Poly1305(ChaCha7539Engine(), Poly1305());
}
Loading
Loading