Skip to content

SFTP backend, with an SSH client on WebCrypto - #38

Merged
sntran merged 3 commits into
mainfrom
feat/sftp
Oct 20, 2026
Merged

sntran merged 3 commits into
mainfrom
feat/sftp

Conversation

@sntran

@sntran sntran commented Oct 20, 2026

Copy link
Copy Markdown
Owner

Add the sftp backend, as in rclone. Its SSH client is in JavaScript, so it runs in Node.js, Deno, Bun and Cloudflare Workers.
Key exchange: curve25519 and ECDH. The strict key exchange protects against the Terrapin attack.
Ciphers: AES-GCM and AES-CTR with HMAC-SHA2.
Host keys: Ed25519, ECDSA and RSA with SHA-2.
Sign-in with a password, keyboard-interactive, or a key.
Security: the backend checks the host key with host_keys or known_hosts_file. With neither option, it refuses the server, and the error gives the server's key line for host_keys. (rclone accepts any key in this case.)
Fixes:
obscure now gives the same result as rclone, so rclone can read the passwords of rfetch.
Parts cuts the parts of an upload with no copy of the data.
The command line shows an error that comes in the middle of an answer.
Tests: the shared suite on rclone serve sftp, each key type and cipher, known_hosts_file, and a comparison with rclone on the same server.

…n answer

obscure wrote the standard base64, with "+", "/" and "=". rclone reads
only the base64 of URLs with no padding: rclone could not reveal a
password of rfetch. Now obscure writes the base64 of rclone, and the
counter of AES-CTR has 128 bits, as the counter of Go. reveal reads the
two forms, as before. A test checks the alphabet.

Parts copied the rest of a large chunk for each part. The local backend
reads chunks of 1 MiB: an upload in parts of 32 KiB copied about 4 GB
for 64 MB. Now a part is a view of the chunk when the chunk has the part,
and the parts of s3 and azureblob have no such copy too.

The command line had no handler for an error after the start of an
answer, for example a server that loses a file in the middle of a cat:
Node.js wrote the stack of the error. Now the error is one line
"Error: ...", with the exit code 1, as rclone.
The sftp backend reads and writes the files of an SFTP server, as the
sftp backend of rclone. Its client of SSH is in JavaScript: it runs in
Node.js, Deno, Bun and workerd.

The client of SSH (backend/sftp/ssh):

- The transport of RFC 4253: the key exchanges curve25519-sha256 and
  ecdh-sha2-nistp256, -nistp384 and -nistp521, the strict key exchange
  of OpenSSH against the Terrapin attack, and new keys when the server
  asks for them.
- The ciphers aes128-gcm@openssh.com, aes256-gcm@openssh.com and
  AES-CTR with HMAC-SHA2-256 or HMAC-SHA2-512, also as encrypt-then-MAC,
  with WebCrypto.
- The host keys ssh-ed25519, ECDSA and RSA with SHA-2.
- The user signs in with a password, with keyboard-interactive, or with
  a key of OpenSSH, PKCS #8, PKCS #1 or SEC 1, of Ed25519, ECDSA or RSA.
- The channels of RFC 4254, with their windows.

The client of SFTP (version 3) sends the reads and the writes of a file
at the same time, as rclone: chunk_size and concurrency.

The backend:

- A path is from the login folder, as rclone, and a path that starts
  with "/" is from the root of the server. fetch.js gives the backend
  RFETCH_SFTP_ROOT=/ for such a path: a config file, a variable, a
  header or the query cannot set it.
- host_keys and known_hosts_file check the host key of the server,
  before the password goes to the server. known_hosts_file reads the
  hashed names, the patterns and @Revoked of OpenSSH. With no option,
  the backend accepts each host key, as rclone.
- key_file and known_hosts_file are locations that the backend reads
  with the fetch of rfetch.
- MOVE of a file or of a folder is a rename on the server, PATCH sets
  the time to the second, and COPY makes a hard link only with
  copy_is_hardlink, as rclone.
- The names have the encoding of rclone: Slash, Ctl, Del and Dot.
- In Node.js, Deno and Bun, a connection waits for the next request for
  idle_timeout, and does not keep the process running. In workerd, each
  request has its own connection.
- The options of rclone that rfetch does not know, for example
  key_use_agent and ssh, give 501.

The tests run the shared suite on rclone serve sftp with a password and
with a key, and with small chunks. They sign in with each type of key,
use each host key, cipher and key exchange, check host_keys and
known_hosts_file, and compare rfetch with the sftp backend of rclone on
the same server: the files, the times, the encoded names, and keys in
the format of OpenSSH that the tests write and rclone reads. The keys
come from openssl and WebCrypto for each run.

On this machine, a copy of 64 MB to rclone serve sftp takes 1.9 s up
and 1.2 s down; rclone takes 0.4 s and 0.5 s. The calls of WebCrypto
for each packet take most of the time.
With no host_keys and no known_hosts_file, the sftp backend refuses each
server, as ssh refuses a host that it does not know. Before, it accepted
each host key, as rclone: then a server in the middle could get the
password.

The answer 502 gives the fingerprint and the line of the host key of the
server. Make sure that it is the key of the server, then set host_keys
to this line. The check is before the password and before the signature
with the key of the user.

known_hosts_file=none is not a value with a special meaning now.

The tests use all the host keys of the test server for each algorithm of
host key, and check that the answer gives a key that then signs in.
@sntran
sntran merged commit 952fdab into main Oct 20, 2026
11 checks passed
@sntran
sntran deleted the feat/sftp branch October 20, 2026 00:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant