ZetCode

Dart SSH

last modified October 18, 2026

In this article we show how to work with SSH and SFTP in Dart language. We use the DartSSH 2 library.

Secure SHell (SSH) is a protocol which allows to create an encrypted channel between two networked hosts. Secure File Transfer Protocol (SFTP) is a network protocol for securely accessing, transferring and managing files. It runs on SSH.

There are two widely used ways of SSH authentication for secure remote access:

The DartSSH 2 library provides an SSH and SFTP client written in pure Dart. It allows to create SSH sessions, provides authentication and forwarding. It supports all features of the SFTPv3 protocol, including upload, download, remove, rename, or delete.

$ dart pub add dartssh2

The examples connect to a remote machine, which the reader must provide. We use the placeholder host 192.0.2.10 (an address reserved for documentation) and the user name user7; replace them with your own values. The program output shown below is an example; it depends on the remote system. The examples read the password from the terminal; never store passwords in source code.

Dart SSH execute command

In the next example, we execute a command remotely.

main.dart
import 'dart:convert';
import 'dart:io';

import 'package:dartssh2/dartssh2.dart';

void main() async {
  const host = '192.0.2.10';
  const username = 'user7';

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

  final client = SSHClient(
    socket,
    username: username,
    onPasswordRequest: () {
      stdout.write('Password: ');
      stdin.echoMode = false;
      final password = stdin.readLineSync();
      stdin.echoMode = true;
      return password ?? exit(1);
    },
  );

  final res = await client.run('uname -a');
  print('');
  print(utf8.decode(res));

  client.close();
  await client.done;
}

We connect to a computer on a local network and launch the uname command.

import 'package:dartssh2/dartssh2.dart';

We import the dartssh2 library.

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

A socket is created with SSHSocket.connect. Port 22 is the default SSH port.

final client = SSHClient(
  socket,
  username: username,
  onPasswordRequest: () {
    stdout.write('Password: ');
    stdin.echoMode = false;
    final password = stdin.readLineSync();
    stdin.echoMode = true;
    return password ?? exit(1);
  },
);

Using the socket, we create an SSH client. Upon connecting, the client will ask for a password. The password is not echoed; we restore the echo mode right after reading it.

final res = await client.run('uname -a');

We run the uname command. The run method waits for the command to finish and returns its standard output as bytes.

print(utf8.decode(res));

We print the decoded response.

client.close();

The connection is terminated with close.

await client.done;

We wait until the transport is closed.

$ dart main.dart
Password:
Linux debian 6.1.0-18-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.76-1 x86_64 GNU/Linux

Dart SSH command output and exit code

The run method returns only the standard output. To get the standard error and the exit code too, we use execute, which returns an SSHSession.

main.dart
import 'dart:convert';
import 'dart:io';

import 'package:dartssh2/dartssh2.dart';

void main() async {
  const host = '192.0.2.10';
  const username = 'user7';

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

  final client = SSHClient(
    socket,
    username: username,
    onPasswordRequest: () {
      stdout.write('Password: ');
      stdin.echoMode = false;
      final password = stdin.readLineSync();
      stdin.echoMode = true;
      return password ?? exit(1);
    },
  );

  final session = await client.execute('ls /nonexistent');

  final out = await utf8.decodeStream(session.stdout);
  final err = await utf8.decodeStream(session.stderr);
  await session.done;

  print('stdout: "$out"');
  print('stderr: "${err.trim()}"');
  print('exit code: ${session.exitCode}');

  client.close();
  await client.done;
}

The session provides the stdout and stderr streams. After the command finishes, the exitCode property holds its exit status; a non-zero value signals a failure.

$ dart main.dart
Password:
stdout: ""
stderr: "ls: cannot access '/nonexistent': No such file or directory"
exit code: 2

Dart SSH shell

With the shell function, we can create an interactive shell session.

main.dart
import 'dart:io';
import 'dart:typed_data';

import 'package:dartssh2/dartssh2.dart';

void main() async {
  const host = '192.0.2.10';
  const username = 'user7';

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

  final client = SSHClient(
    socket,
    username: username,
    onPasswordRequest: () {
      stdout.write('Password: ');
      stdin.echoMode = false;
      final password = stdin.readLineSync();
      stdin.echoMode = true;
      return password ?? exit(1);
    },
  );

  final shell = await client.shell();

  stdout.addStream(shell.stdout);
  stderr.addStream(shell.stderr);
  stdin.cast<Uint8List>().listen(shell.write);

  await shell.done;

  client.close();
  await client.done;
  exit(0);
}

The program creates an interactive shell session.

final shell = await client.shell();

We create a shell session on the remote host.

stdout.addStream(shell.stdout);
stderr.addStream(shell.stderr);
stdin.cast<Uint8List>().listen(shell.write);

We plug the remote standard streams to our local streams. Everything we type is sent to the remote shell.

await shell.done;

We wait until the shell terminates. We can terminate the shell with the exit command. The final exit(0) is needed because the listener on stdin would otherwise keep the program alive.

Dart SSH public key authentication

In the next example, we authenticate using public key authentication, which is more secure.

main.dart
import 'dart:convert';
import 'dart:io';

import 'package:dartssh2/dartssh2.dart';

void main() async {
  const host = '192.0.2.10';
  const username = 'user7';

  final home = Platform.environment['HOME'] ?? Platform.environment['USERPROFILE']!;
  final keyFile = File('$home/.ssh/id_rsa');

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

  final client = SSHClient(
    socket,
    username: username,
    identities: [
      ...SSHKeyPair.fromPem(await keyFile.readAsString(), 'passphrase'),
    ],
  );

  final uptime = await client.run('uptime');
  print(utf8.decode(uptime));

  client.close();
  await client.done;
}

The program connects to the remote system, authenticates using public key authentication, and executes the uptime command.

final home = Platform.environment['HOME'] ?? Platform.environment['USERPROFILE']!;
final keyFile = File('$home/.ssh/id_rsa');

This is the location of the private key. The home directory is taken from the HOME environment variable (USERPROFILE on Windows).

identities: [
  ...SSHKeyPair.fromPem(await keyFile.readAsString(), 'passphrase'),
],

We pass the private key to the SSHKeyPair.fromPem function. The second argument is the passphrase; it is needed only when the key is encrypted, so for an unprotected key we omit it. Also note that the remote system must contain our public key in the ~/.ssh/authorized_keys file.

final uptime = await client.run('uptime');
print(utf8.decode(uptime));

We run the command and print the decoded response.

$ dart main.dart
06:45:06 up 30 min,  1 user,  load average: 0.00, 0.00, 0.00

Dart SFTP list directory

In the following example, we list directory contents via SFTP.

main.dart
import 'dart:io';

import 'package:dartssh2/dartssh2.dart';

void main(List<String> args) async {
  const host = '192.0.2.10';
  const username = 'user7';

  final dirname = args.first;

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

  final client = SSHClient(
    socket,
    username: username,
    onPasswordRequest: () {
      stdout.write('Password: ');
      stdin.echoMode = false;
      final password = stdin.readLineSync();
      stdin.echoMode = true;
      return password ?? exit(1);
    },
  );

  final sftp = await client.sftp();
  final items = await sftp.listdir(dirname);

  print('');

  for (final item in items) {
    print(item.longname);
  }

  client.close();
  await client.done;
}

The program accepts an argument, which is a path to the directory, such as /home/user7/projects.

final dirname = args.first;

We get the first command-line argument. It is expected to be the path to the remote directory.

final sftp = await client.sftp();
final items = await sftp.listdir(dirname);

An SFTP client is created with the sftp function. The directory listing is retrieved with listdir, which returns a list of SftpName objects.

for (final item in items) {
  print(item.longname);
}

We go through the list and print the long names, which have the format of the ls -l command. The filename property contains just the name and attr has the size, permissions, and times.

$ dart main.dart /home/user7/projects
Password:
drwxr-xr-x    5 user7    user7        4096 Oct 18 20:00 .
drwx------   56 user7    user7        4096 Oct 11 16:39 ..
drwxr-xr-x    2 user7    user7        4096 Oct  4 12:31 dbi
drwxr-xr-x    2 user7    user7        4096 Oct  4 12:31 hash
drwxr-xr-x    2 user7    user7        4096 Oct 19 13:39 loops
drwxr-xr-x    2 user7    user7        4096 Oct 18 20:00 sockets

Dart SFTP make directory

The mkdir creates a new directory. Other useful methods are remove (a file), rmdir (a directory), rename, and stat.

main.dart
import 'dart:io';

import 'package:dartssh2/dartssh2.dart';

void main(List<String> args) async {
  const host = '192.0.2.10';
  const username = 'user7';

  final dirname = args.first;

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

  final client = SSHClient(
    socket,
    username: username,
    onPasswordRequest: () {
      stdout.write('Password: ');
      stdin.echoMode = false;
      final password = stdin.readLineSync();
      stdin.echoMode = true;
      return password ?? exit(1);
    },
  );

  final sftp = await client.sftp();
  await sftp.mkdir(dirname);

  print('');
  print('directory created');

  final stat = await sftp.stat(dirname);
  print('is directory: ${stat.isDirectory}');

  client.close();
  await client.done;
}

The program creates a new directory on the remote system. The directory name is passed as the first command-line argument. After the creation, we verify it with stat.

$ dart main.dart /home/user7/newdir
Password:
directory created
is directory: true

Dart SFTP upload file

The open function opens a remote file.

main.dart
import 'dart:io';

import 'package:dartssh2/dartssh2.dart';

void main(List<String> args) async {
  const host = '192.0.2.10';
  const username = 'user7';

  final lname = args.first;
  final rname = args.last;

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

  final client = SSHClient(
    socket,
    username: username,
    onPasswordRequest: () {
      stdout.write('Password: ');
      stdin.echoMode = false;
      final password = stdin.readLineSync();
      stdin.echoMode = true;
      return password ?? exit(1);
    },
  );

  final sftp = await client.sftp();

  final file = await sftp.open(
    rname,
    mode: SftpFileOpenMode.truncate |
        SftpFileOpenMode.create |
        SftpFileOpenMode.write,
  );

  await file.write(File(lname).openRead().cast());
  await file.close();

  print('');
  print('file successfully uploaded');

  client.close();
  await client.done;
}

The program uploads a file to a remote system.

final lname = args.first;
final rname = args.last;

We have two command-line parameters. The first is the local filename to be uploaded and the second is the full path of the remote file.

final file = await sftp.open(
  rname,
  mode: SftpFileOpenMode.truncate |
      SftpFileOpenMode.create |
      SftpFileOpenMode.write,
);

We open a file in the write mode on the remote system with open. The file is truncated if it exists, or created if it does not.

await file.write(File(lname).openRead().cast());
await file.close();

The data is streamed from the local file to the remote file, so even large files do not have to fit into memory. We close the remote file to make sure all data has been written.

$ dart main.dart report.pdf /home/user7/report.pdf
Password:
file successfully uploaded

Dart SFTP download file

The following program downloads a file.

main.dart
import 'dart:io';

import 'package:dartssh2/dartssh2.dart';

void main(List<String> args) async {
  const host = '192.0.2.10';
  const username = 'user7';

  final rname = args.first;
  final lname = args.last;

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

  final client = SSHClient(
    socket,
    username: username,
    onPasswordRequest: () {
      stdout.write('Password: ');
      stdin.echoMode = false;
      final password = stdin.readLineSync();
      stdin.echoMode = true;
      return password ?? exit(1);
    },
  );

  final sftp = await client.sftp();

  final rfile = await sftp.open(rname);
  final data = await rfile.readBytes();
  await rfile.close();

  await File(lname).writeAsBytes(data);

  print('');
  print('file downloaded (${data.length} bytes)');

  client.close();
  await client.done;
}

The program is similar to the previous one; we again utilize the open function.

final rname = args.first;
final lname = args.last;

The program takes two parameters. The first is the full path to the remote file (such as /home/user7/report.pdf), and the second is the local filename.

final rfile = await sftp.open(rname);

We open the remote file. The default mode is read-only.

final data = await rfile.readBytes();

We read the whole file as bytes with readBytes.

await File(lname).writeAsBytes(data);

We write the bytes to the local file. Writing bytes, rather than decoding them to a string, makes the program work for binary files too.

$ dart main.dart /home/user7/report.pdf report-copy.pdf
Password:
file downloaded (48213 bytes)

Dart SSH host keys

By default, the client accepts any host key. To protect against man-in-the-middle attacks, we should verify the fingerprint of the server's key with the onVerifyHostKey callback.

main.dart
import 'package:dartssh2/dartssh2.dart';

void main() async {
  const host = '192.0.2.10';
  const trusted = '3f:2a:...';

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

  final client = SSHClient(
    socket,
    username: 'user7',
    onVerifyHostKey: (type, fingerprint) {
      final hex = fingerprint
          .map((b) => b.toRadixString(16).padLeft(2, '0'))
          .join(':');
      print('$type $hex');
      return hex == trusted;
    },
  );

  await client.authenticated;
  client.close();
}

The callback receives the key type and the MD5 fingerprint of the key. Returning false aborts the connection. We compare the fingerprint to a value we obtained from the administrator of the server, for instance with ssh-keygen -l -E md5 -f ssh_host_ed25519_key.pub. The trusted value above is a placeholder.

Source

dartssh2 documentation

In this article we have worked with SSH and SFTP in Dart.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Dart tutorials.