Skip to content

Repository files navigation

qrdrop

Send a file straight from one device to another.
A QR code carries the key; the file goes over WebRTC;
nothing in between ever holds a readable copy.

npm ci node licence

share.stan-ely.com — no install, no account, no upload.

One real qrdrop transfer, filmed from both ends. On a laptop, report.pdf is dropped onto share.stan-ely.com and a QR code appears. A phone's camera scans the code. Both screens then show the same four symbols — taco, umbrella, snake, rainbow — under “Check both devices show the same symbols”. The laptop sends the file and reports it delivered while the phone opens the received report.pdf. The clip loops through a brief dip to black.

npx qrdrop send report.pdf     # prints a QR code in your terminal
npx qrdrop receive             # on the other machine
The send screen: a QR code, the pairing code as text with a Copy button, and a line explaining that any camera app can open it. The verification screen: four tiles, each an emoji above a word — elephant, trumpet, rocket, wave — under the heading “Check both devices show the same symbols”. The beam screen: a large, dense QR code mid-animation, the filename beside a small “Not encrypted” tag, a note to keep the code on screen, and a speed control.
Show the code Match the words Beam, for no network

Install

Nothing to install for the browser: open share.stan-ely.com on both devices. Everything else, by where you are:

Any machine with Node 22+ npx qrdrop send report.pdf and npx qrdrop receive — the QR is printed in the terminal
CLI on PATH brew install stan-ely/tap/qrdrop · scoop install stan-ely/qrdrop (after scoop bucket add stan-ely https://github.com/stan-ely/scoop-bucket) · npm i -g qrdrop
Browser UI, run locally npx qrdrop web — serves this copy on 127.0.0.1
Windows app Microsoft Store · scoop install stan-ely/qrdrop-app · Releases
macOS app (Apple silicon) brew install --cask stan-ely/tap/qrdrop-app · Releases
Android app F-Droid repository (this project's own, same signed APK) · Releases · Google Play is in closed testing — join as a tester
Linux app .deb and AppImage on Releases — Beam only; the webview has no WebRTC

A file sent from the CLI can be received in a browser, and the other way round. That interoperability is the reason this is one package rather than three.

Where How
Browser share.stan-ely.com, or <qr-drop> in a page of your own
Terminal npx qrdrop send / receive — the QR is printed as text
Library import { openRoom, sendFile, createReceiver } from 'qrdrop'
Air-gapped Beam — animated QR on one screen, a camera on the other
App A desktop and mobile shell — a real file picker, the system camera, and scanned links that open it

What is in here

  • One protocol, three runtimes. The same src/core/ runs in a browser, in Node and in a Tauri app, and is typechecked twice — with and without Node's globals — so "isomorphic" is a build failure rather than a promise. Protocol
  • Keys from a QR code, checked by eye. 32 random bytes in the code, HKDF for everything else, an ephemeral ECDH per session, per-chunk AES-GCM on top of WebRTC's own encryption, and a four-emoji SAS that gets exactly one attempt per secret. WebCrypto only. Protocol · Threat model
  • A fountain code for no network at all. Beam animates LT-coded QR frames, so a receiver needs enough frames rather than particular ones — 1.48 × N frames at 10% loss, against 8.3 × N for numbered chunks on a loop. Beam
  • No framework, four runtime dependencies. The UI is a custom element over a hand-rolled virtual DOM, laid out to never scroll, and checked by a script that walks every screen in Chromium and Firefox, at four viewports, with a mouse and with touch.
  • Shipped where people are. npm with provenance, Homebrew, Scoop, the Microsoft Store, and a self-hosted F-Droid repository whose APK is built reproducibly — two builds in different directories are byte-identical. The app

How it works

Two devices in the same room. One shows a QR code, the other points a camera at it, and that scan is the one channel an attacker cannot stand in the middle of.

Sequence diagram of the WebRTC path. The sender generates 32 random bytes and shows them as a QR code, which the receiver scans; the room ID, signalling password, session keys and the SAS all derive from those bytes. Both peers announce on every signalling network at once, exchange session descriptions encrypted under the derived password, and open a WebRTC data channel. An ephemeral ECDH gives one key per direction; four emoji are read aloud and matched by hand; only then does the manifest go out, the receiver accepts, and the file chunks follow, each one sealed.

The QR carries 32 bytes of CSPRNG output. Everything else derives from it:

Derivation Purpose
HKDF(secret, "topic") Trystero room ID — what peers meet on
HKDF(secret, "signal") Trystero password, encrypting session descriptions
HKDF(ECDH, salt=secret, "host->guest") file bytes, sender to receiver
HKDF(ECDH, salt=secret, "guest->host") file bytes, the other way
HKDF(ECDH, salt=secret, "sas") the four emoji shown on both screens

The room ID is derived rather than being the secret itself. Using the secret as the room name is the obvious shortcut, works perfectly in testing, and silently publishes your key to every relay on the network.

Both devices then show the same four emoji, and the sender confirms they match before the filename or size is sent. The receiver's Accept is the only other gesture. Why each of those steps is there, and what breaks without it, is in docs/protocol.md.

Threat model

Protected: relays and TURN servers see ciphertext, timing and volume only; a network attacker without the QR cannot join, read or tamper with a transfer; past transfers stay closed if a code leaks afterwards; truncated, reordered or altered files are rejected rather than written.

Not protected: anyone holding the code can join — it is the whole credential, so show it to a person and not to a room or a screen share; both peers learn each other's IP address; the host serving the page could serve modified code; relays see that two keys met.

Warning

Beam is not encrypted, and cannot be. There is no handshake on a one-way channel. It is for air-gapped machines, where the alternative is a USB stick.

The full version, including how long a code stays useful and what the route indicator can and cannot tell you, is docs/threat-model.md. Vulnerabilities go privately through SECURITY.md.

Development

npm install
npm test            # unit suite, offline
npm run typecheck   # two tsc runs, browser and Node; nothing is compiled
npm run build       # esbuild -> site/dist/
npm start           # build, then serve site/dist/ on :4173

localhost counts as a secure context, so WebCrypto and the camera both work against npm start without a certificate. The end-to-end suites (npm run test:e2e, npm run test:e2e:interop) need a network and public Nostr relays, so they stay out of npm test and out of CI. CONTRIBUTING.md has the house style, the source layout, and the rules a change can break silently.

Documentation

Protocol Pairing, key derivation, framing, the transport seam, sources and sinks
Threat model What is and is not protected, and known limitations
Beam The no-network mode: the fountain code, its measurements, and prior art
The app Desktop and mobile installs, what works on each platform, signing
Hosting Running the site bundle yourself, and how share.stan-ely.com is deployed
app/CAPABILITIES.md The app's lab notebook: every platform measurement, phase by phase
CHANGELOG.md · app/CHANGELOG.md Release notes for the npm package and the app

Licence

MIT.

About

Send a file straight from one device to another. A QR code carries the key; the file goes over WebRTC; nothing in between ever holds a readable copy.

Topics

Resources

Contributing

Security policy

Stars

4 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages