A pure-Dart SSH and SFTP library, forked from dartssh2 and hardened for use in ZestSSH.
The reason it is public: the code that talks to your servers should be code you can read. This is the exact SSH stack ZestSSH ships, nothing stripped out for the open-source version. If you want to know how the client authenticates, what it negotiates, or how it handles a host key, it is all here.
MIT licensed, free to use, and staying that way.
This started as a fork of dartssh2, which is also MIT. dartssh2 was originally written by xuty (TerminalStudio/dartssh2) and is now maintained at vicajilau/dartssh2, which is what this repo forks. The upstream license and copyright are kept in LICENSE.
Fork point: dartssh2 2.14.0. The GitHub "forked from" header and the compare view show the full diff against that exact upstream release, so you can see every change rather than taking this README's word for it.
What changed, at a glance:
- Weak algorithms removed from the negotiated defaults (see the table below). They are still implemented, but a server has to be met with an explicit opt-in profile, not offered them by every connection.
- Terrapin (CVE-2023-48795) strict key exchange, with sequence-number reset after the first
NEWKEYS. - The client identification banner reports
zest_ssh_core, not the upstream string, so a server operator or a scanner can attribute the behaviour to this library. - A batch of reliability and SFTP fixes carried in from ZestSSH field use.
Two profiles. The default is what every connection proposes. The compatibility profile is opt-in, for reaching an old server that cannot do better, and you pass it deliberately.
| Category | Default (proposed to every server) | Compatibility only (opt-in) |
|---|---|---|
| Key exchange | curve25519-sha256@libssh.org, ecdh-sha2-nistp521, ecdh-sha2-nistp384, ecdh-sha2-nistp256, diffie-hellman-group-exchange-sha256, diffie-hellman-group14-sha256 |
diffie-hellman-group14-sha1, diffie-hellman-group-exchange-sha1, diffie-hellman-group1-sha1 |
| Host keys | ssh-ed25519, rsa-sha2-512, rsa-sha2-256, ecdsa-sha2-nistp521, ecdsa-sha2-nistp384, ecdsa-sha2-nistp256 |
ssh-rsa (SHA-1) |
| Ciphers | chacha20-poly1305@openssh.com, aes256-ctr, aes128-ctr |
aes256-cbc, aes192-cbc, aes128-cbc |
| MACs | hmac-sha2-512-etm, hmac-sha2-256-etm, hmac-sha2-512, hmac-sha2-256, hmac-sha1, hmac-sha2-512-96, hmac-sha2-256-96 |
hmac-md5 |
Two honest caveats:
hmac-sha1is still in the default MACs, listed last, for older-server reach. The SHA-1 key exchanges and thessh-rsaSHA-1 host key are the ones that were pulled from the defaults, because a SHA-1 collision matters far more in key exchange and signatures than in a MAC. If you want SHA-1 gone entirely, dropSSHMacType.hmacSha1from yourSSHAlgorithms.- A few algorithms exist in the source but are not in the default proposal (for example AES-GCM and
aes192-ctr). The definitive lists are theconstdefaults inlib/src/ssh_algorithm.dart; that file is the source of truth, not this table.
Verified with the Terrapin scanner that strict key exchange is negotiated and that no vulnerable cipher and MAC combinations are offered by default. The scanner checks advertised algorithms and strict-KEX support, not runtime behaviour, so that is exactly what this claim covers, no more.
The defaults are a const SSHAlgorithms(). To reach a legacy server, build your own and pass it:
final client = SSHClient(
await SSHSocket.connect('legacy.example.com', 22),
username: 'user',
algorithms: SSHAlgorithms(
cipher: [
SSHCipherType.chacha20poly1305,
SSHCipherType.aes256ctr,
SSHCipherType.aes256cbc, // opt back in, on purpose
],
),
// ...
);Upstream dartssh2 is tracked, and its security and correctness fixes are merged. Keeping this a real fork rather than a rewrite is exactly why: when something lands upstream it can be pulled in instead of reimplemented. Anything out of scope for a security fix stays close to upstream so those merges keep working.
Being a fork does not mean following upstream blindly. Where upstream takes a direction that is wrong for this library, it is not taken, and the reason is written down rather than left implicit.
The record so far, last audited 2026-08-19 against upstream 3.3.0:
| Upstream change | Status here |
|---|---|
| SFTP short read causing silent data loss | Merged |
| P-521 ECDH scalar generated one byte short | Merged |
Per-handshake Isolate.run offload for key exchange (2.20.0+) |
Not taken. It costs several times more than the curve operation it hides and can lose a race against a server's handshake timeout (upstream issue #226). This fork has always computed X25519 and the NIST curves synchronously, which is what upstream reverted to. |
3.0.0 API changes (SSHClient.identities, close()) |
Not applicable, this fork has its own surface |
Divergence from upstream's current defaults is deliberate: upstream now prefers AES-GCM in its default cipher list and has dropped the truncated 96-bit MACs, while this library leads with ChaCha20-Poly1305 and still offers the truncated MACs last. Both are defensible; the table in Security posture is the authority for what this library actually negotiates.
CHANGELOG.md is this fork's history. Upstream's history up to the fork point is preserved in
CHANGELOG-dartssh2.md.
dependencies:
zest_ssh_core:
git: https://github.com/AffluentMods/zest_ssh_core.gitimport 'dart:convert';
import 'package:zest_ssh_core/zest_ssh_core.dart';
void main() async {
final client = SSHClient(
await SSHSocket.connect('example.com', 22),
username: 'user',
onPasswordRequest: () => 'password',
);
final result = await client.run('uname -a');
print(utf8.decode(result));
client.close();
await client.done;
}More in example/: interactive shells, command execution, SFTP, local and remote and dynamic forwarding, jump hosts, and public-key auth.
Found something? See SECURITY.md. Reports go straight to a person, not a ticket queue.
MIT. See LICENSE. The upstream dartssh2 copyright is preserved there alongside this fork's.