DartSSH 2

repository·main·Indexed 42 days ago

https://github.com/terminalstudio/dartssh2

A feature-rich, pure Dart implementation of SSH and SFTP clients designed for the Dart VM, Flutter, and web platforms. It supports SSH session management, SFTPv3 protocol, local/remote/dynamic port forwarding, and various authentication methods including password, PEM files, and custom identities (YubiKey, FIDO2, Secure Enclave). The library provides high-performance, non-blocking connectivity by offloading heavy calculations to background isolates on supported VM platforms.

Tokens
12.7K
Snippets
42
Records
59
Agent score
4%

What's inside DartSSH 2

  1. Overview of DartSSH 2 features

    main

    DartSSH 2 is a pure Dart SSH and SFTP client designed to run on the Dart VM, Flutter, and the web. Key capabilities include:

    • SSH Session Management: Execute commands, spawn shells, set environment variables, and use pseudo-terminals.
    • Authentication: Supports password, in-memory private keys (SSHKeyPair), external asynchronous identities (SSHIdentity for Secure Enclave, YubiKey/FIDO2, smart cards, OS agents), public-key probing (RFC 4252 §7.8), hostbased authentication (RFC 4252 §9), and keyboard-interactive authentication.
    • Forwarding: Supports local, remote, and dynamic forwarding (SOCKS5 CONNECT).
    • SFTP: Full support for the SFTPv3 protocol, including upload, download, list, link, remove, and rename operations.
    • Performance: Uses non-blocking key exchange by offloading heavy calculations (X25519, NIST Curves, DH) to background isolates on supported VM platforms to prevent UI freezing.
  2. Download remote files using SFTP

    main

    The SFTP API provides two ways to download files:

    1. sftp.download(path, sink): The simplest one-liner. It handles opening and closing the remote file automatically. Use this when you only have a remote path.
    2. file.downloadTo(sink): Use this when you already have an open SftpFile handle. This is useful for partial downloads using offset and length or for reusing a handle.

    You can tune performance for high-latency links or large files using chunkSize and maxPendingRequests.

    // Simple download
    void main() async {
      final sftp = await client.sftp();
      final output = File('local_file.txt').openWrite();
    
      final bytes = await sftp.download(
        '/remote/file.txt',
        output,
        onProgress: (bytesRead) => print('downloaded: $bytesRead bytes'),
        closeDestination: true,
      );
    
      print('download complete: $bytes bytes');
    }
    
    // Partial download using an existing handle
    void main() async {
      final sftp = await client.sftp();
      final file = await sftp.open('/remote/file.txt');
      final output = File('local_partial.bin').openWrite();
    
      try {
        await file.downloadTo(
          output,
          offset: 1024,
          length: 4096,
          closeDestination: true,
        );
      } finally {
        await file.close();
      }
    }
  3. Verify the SSH host key

    main

    By default, SSHClient validates that the host key signature is valid, but it does not verify if the key belongs to the expected server. To prevent Man-in-the-Middle attacks, use the onVerifyHostKey callback.

    This callback receives the key type and the OpenSSH-style SHA256 fingerprint. Returning false will abort the connection. The handler can return a Future<bool> to allow for asynchronous UI prompts or database lookups.

    Warning: Omitting onVerifyHostKey means every host key is accepted. Using disableHostkeyVerification: true is a stronger opt-out that skips the signature check entirely; avoid this outside of local testing.

    final client = SSHClient(
      await SSHSocket.connect('localhost', 22),
      username: '<username>',
      onPasswordRequest: () => '<password>',
      onVerifyHostKey: (type, fingerprint) {
        final expected = knownHosts['localhost']; // your own storage
        if (expected == null) {
          // First connection: ask the user, then remember the answer.
          return promptUserToTrust(type, utf8.decode(fingerprint));
        }
        return utf8.decode(fingerprint) == expected;
      },
    );
  4. Connect to a remote host with SSHClient

    main

    To establish a basic SSH connection, use SSHClient with an SSHSocket. You must provide a username and an onPasswordRequest callback if using password authentication.

    Note: SSHSocket.connect() uses native TCP sockets (dart:io) and is not available on Flutter Web or Dart Web. For web support, you must provide a custom SSHSocket implementation (e.g., using WebSockets).

    void main() async {
      final client = SSHClient(
        await SSHSocket.connect('localhost', 22),
        username: '<username>',
        onPasswordRequest: () => '<password>',
      );
    }
  5. Write and upload files via SFTP

    main

    Write to a remote file

    Use sftp.open(path, mode: SftpFileOpenMode.write) to create a writable handle. You can write bytes at a specific offset using file.writeBytes(data, offset: n).

    Upload a local file

    To upload, open a remote file with SftpFileOpenMode.create | SftpFileOpenMode.write and pipe a local stream into file.write(stream).

    Upload features

    • Pause/Resume: The uploader returned by file.write() supports .pause() and .resume() methods.
    • Truncate: To clear a remote file before writing, use SftpFileOpenMode.truncate.
    • Tuning: Use chunkSize and maxPendingRequests in the write method to optimize throughput.
    // Upload a file
    void main() async {
      final sftp = await client.sftp();
      final file = await sftp.open('file.txt', mode: SftpFileOpenMode.create | SftpFileOpenMode.write);
      await file.write(File('local_file.txt').openRead().cast());
    }
    
    // Pause and resume upload
    void main() async {
      final uploader = await file.write(File('local_file.txt').openRead().cast());
      // ...
      await uploader.pause();
      // ...
      await uploader.resume();
      await uploader.done;
    }
  6. Implement SSH for Web support

    main

    Since browsers cannot open TCP sockets, SSHSocket.connect() will fail on web runtimes. To use dartssh2 on Flutter Web or Dart Web, you must implement the SSHSocket interface using a transport the browser supports, such as a WebSocket connected to a proxy that bridges to your SSH endpoint.

    Web Limitations:

    • chacha20-poly1305@openssh.com is not available because the underlying library requires 64-bit integers not fully supported in the same way. AES-GCM or AES-CTR will be negotiated instead.
    • SftpFile.downloadToRandomAccess is unavailable because it requires dart:io. Use SftpFile.downloadTo (which takes a sink) or SftpFile.read instead.
  7. Configure SSH algorithms for legacy servers

    main

    By default, dartssh2 uses modern, secure algorithms (e.g., ssh-ed25519 for host keys, curve25519-sha256 for KEX, and aes-gcm for ciphers).

    If you are connecting to a legacy device (like an old router or NAS) that requires deprecated algorithms (e.g., diffie-hellman-group14-sha1 or ssh-rsa), you must explicitly pass them via the algorithms parameter in the SSHClient constructor using SSHAlgorithms.

    void main() async {
      final client = SSHClient(
        await SSHSocket.connect('localhost', 22),
        username: '<username>',
        onPasswordRequest: () => '<password>',
        algorithms: const SSHAlgorithms(
          // Only do this for a server that supports nothing better.
          kex: [SSHKexType.dh14Sha1, SSHKexType.dh1Sha1],
        ),
      );
    
      // Use the client...
      await client.close();
    }
  8. Connect through a Jump Server

    main

    To connect to a target server through a jump host (gateway), first establish an SSHClient to the jump server. Then, use jumpServer.forwardLocal(targetHost, targetPort) to create a tunnel, and use the resulting connection to initialize a second SSHClient targeting the destination.

    final jumpServer = SSHClient(
      await SSHSocket.connect('<jump server>', 22),
      username: '...',
    );
    
    final targetClient = SSHClient(
      await jumpServer.forwardLocal('<target server>', 22),
      username: '...',
    );
  9. Install and use the dartssh CLI

    main

    You can use DartSSH via a command-line interface. First, install the dartssh2_cli package globally using dart pub. Once installed, you can use the dartssh command in place of the standard ssh command, or dartsftp for SFTP operations.

    # Install the `dartssh` command.
    dart pub global activate dartssh2_cli
    
    # Then use `dartssh` as regular `ssh` command.
    dartssh user@example.com
    
    # Example: execute a command on remote host.
    dartssh user@example.com ls -al
    
    # Example: connect to a non-standard port.
    dartssh user@example.com:<port>
    
    # Transfer files via SFTP.
    dartsftp user@example.com
  10. Understand Strict Key Exchange (Terrapin Countermeasure)

    main

    The library supports Strict Key Exchange, which is a countermeasure against the Terrapin attack (CVE-2023-48795).

    When both the client and server advertise support for strict key exchange in their initial SSH_MSG_KEXINIT message:

    1. The SSH_MSG_KEXINIT must be the very first packet of the connection (sequence number 0).
    2. Certain optional transport messages—specifically SSH_MSG_IGNORE (2), SSH_MSG_UNIMPLEMENTED (3), and SSH_MSG_DEBUG (4)—are forbidden during the key exchange phase.

    If a violation is detected (e.g., an unexpected message is received during the exchange), the library will throw an SSHHandshakeError and tear down the connection.

  11. Handle SSH host key verification

    main

    To prevent Man-in-the-Middle attacks, you should implement the onVerifyHostKey handler in SSHTransport. This handler is called when the host key is received during the handshake.

    The handler must return a FutureOr<bool>:

    • true: The key is valid; proceed with the connection.
    • false: The key is invalid; the transport will reject the key and disconnect.

    The fingerprint provided is an OpenSSH-style SHA256 fingerprint, UTF-8 encoded as SHA256:<base64>.

    // Example of a verification handler
    SSHHostkeyVerifyHandler myVerifier = (String type, Uint8List fingerprint) async {
      final fingerprintString = utf8.decode(fingerprint);
      if (fingerprintString == 'SHA256:expected_base64_fingerprint_here') {
        return true;
      }
      return false;
    };
  12. Manage a local dynamic forwarding server with SSHDynamicForward

    main

    The SSHDynamicForward abstraction represents a local dynamic SOCKS5 forwarding server managed by an SSHClient. It allows you to bind a local port to act as a SOCKS proxy that tunnels traffic through the SSH connection.

    Key properties:

    • host: The local host/interface the SOCKS server is bound to.
    • port: The local port the SOCKS server is listening on.
    • isClosed: Indicates if the forwarder has been closed.

    To stop the server and close all active connections, call close().