Ferry

How Ferry works

Two browsers that have never met need something to introduce them. Ferry uses a ten-character code for that, and builds everything else on top of it, so the introduction service never learns anything worth knowing.

A crossing, step by step

  1. 1

    One device opens a crossing

    It mints a ten-character code from your browser's cryptographic random source — about fifty bits — and shows it as a QR. That code is the only secret in the system. It travels in the QR, or in the part of a link after the # that browsers never send to a server.

  2. 2

    The other device reads it

    Scan the QR, or type the ten characters. The code is Crockford base-32, which leaves out I, L, O and U, so there is no pair of characters you can confuse reading it aloud down a phone line.

  3. 3

    They find each other at the relay

    Both browsers ask the relay for the same room. The room is named by a SHA-256 hash of your code, never the code itself, so the relay learns a meaningless string and cannot work backwards to the secret.

  4. 4

    They agree on a key

    Each side generates a throwaway P-256 key pair and they exchange public halves. The shared secret goes through HKDF salted with a value derived from your code — so anyone who does not hold the code, the relay included, ends up with a different key.

  5. 5

    Four words appear on both screens

    Those words are derived from the agreed key. If both screens show the same four, the two devices hold the same key and nobody is sitting in between. If they differ, stop.

  6. 6

    The data goes across

    On the same network the bytes travel device to device and never touch the relay at all. Where a network blocks that, the same encrypted frames are forwarded instead — the relay still only sees ciphertext.

Why the relay cannot read your data

Two browsers on different networks cannot dial each other directly; something has to introduce them. That introduction service is the one always-on piece Ferry needs, and it is treated as hostile from the start.

It is handed a room name that is a hash of your code, so it cannot recover the code. The key agreement is salted with that same code, so a relay that tried to insert itself as your peer would derive a different key and its very first frame would fail authentication. And every payload is sealed with AES-256-GCM before it is handed over — including the WebRTC offer, answer and network candidates, so the relay does not even learn your local network addresses.

Keys are thrown away when the tab closes. Recording the traffic and learning the code afterwards does not help: the key pair that encrypted it no longer exists anywhere.

Two routes across

Ferry prefers a direct browser-to-browser link. On a shared network the bytes go straight from one device to the other and the relay carries nothing at all.

A small number of networks block that — some corporate Wi-Fi, some mobile carriers. Ferry notices and starts passing the same sealed frames through the relay instead. It is slower, and it is still unreadable to everyone in between.

What arrives, and how long it stays

Received items clear themselves: a password after two minutes, text after five, a file after fifteen. Each one offers you two more minutes, or a button to keep it for as long as the tab is open. Copying a password offers to wipe your clipboard afterwards.

Files stream across in 64 KB pieces with backpressure, so a large transfer does not build up in memory. A single transfer can be up to 250 MB.

What Ferry deliberately does not protect against

  • Anyone who gets your code before the second device joins can take that place. A room holds two devices and no more, so if a stranger got there first your own device is refused and you will know. Treat the code the way you would a door key.
  • A device that is already compromised. End-to-end encryption ends at the ends.
  • Traffic analysis by the relay. It can see that two sockets exchanged some number of bytes at some time. It cannot see what they were.

See for yourself

Every claim on this page is in the source, and the properties are checked by tests that run against real WebCrypto on every change: that both sides agree on a key, that the two directions use different ones, that a peer with the wrong code is refused, that a tampered frame is rejected.

Read the source on GitHub or open Ferry and try a crossing.