- Scheme 81.4%
- Shell 15.5%
- Makefile 3%
- Common Lisp 0.1%
| .builds | ||
| .jerboa | ||
| docs | ||
| scripts | ||
| server | ||
| support | ||
| test | ||
| tools | ||
| wormhole | ||
| .gitignore | ||
| .jerbuild | ||
| AGENTS.md | ||
| jpkg.lock | ||
| jpkg.policy.sexp | ||
| jpkg.sexp | ||
| LICENSE | ||
| Makefile | ||
| NOTICE | ||
| README.md | ||
| SECURITY.md | ||
jerboa-wormhole
A magic-wormhole-style secure transfer tool, reimplemented in Jerboa Scheme. Get a short code on one computer, type it on the other, and the two meet through a relay and move a text message or a file between them — end-to-end encrypted, with the code itself protected by a PAKE so a network attacker gets only a single online guess.
$ wormhole send report.pdf
Wormhole code is: 4-purple-sausages
On the other computer, run:
wormhole receive 4-purple-sausages
$ wormhole receive 4-purple-sausages
Receive file 'report.pdf' (51234 bytes)? [Y/n] y
receiving (51234 bytes) over transit ...
received 51234 bytes -> report.pdf
This is a clean-room reimplementation of the ideas of magic-wormhole, not a
port of its code, and it is Jerboa-to-Jerboa: the wire format and crypto
are our own, so it interoperates with other jerboa-wormhole peers (and the
bundled relay), not with the Python wormhole.
Security status
jerboa-wormhole is private/experimental until the release gates in
SECURITY.md, docs/threat-model.md, and docs/release-evidence.md are
complete. It includes daemon surfaces, native crypto/socket FFI, file and
directory writes, archive extraction, optional SSH public-key transfer, and a
custom Jerboa-to-Jerboa protocol. Do not expose relays or publish binaries as
production-ready without the documented soak, fuzzing, reproducibility, target
OS, and external review evidence.
Quickstart
Build the (self-contained) binary and put it on your PATH:
make install # -> ~/.local/bin/wormhole (put ~/.local/bin on PATH)
# or: make binary and run ./wormhole-bin
A send and a receive meet at a relay. For a first run, everything can live on one machine — open three terminals.
Terminal 1 — start the relay (listens on port 4000 by default; this is also
the clients' default, so no --relay flag is needed below):
$ wormhole relay 4000
wormhole mailbox relay on 0.0.0.0:4000
for NAT transit fallback, run `wormhole transit-relay` as a
SEPARATE process and pass --transit-relay HOST:PORT to clients
(ctrl-c to stop)
Terminal 2 — send a file. It prints a one-time code and waits:
$ echo "the eagle lands at dawn" > secret.txt
$ wormhole send secret.txt
Wormhole code is: 7-revenge-spaniel
On the other computer, run:
wormhole receive 7-revenge-spaniel
sending (24 bytes) secret.txt ...
ok — confirmed by receiver
Terminal 3 — receive it. Type (or paste) the code; you're asked to confirm before the bytes move, and the sender's SHA-256 is checked on arrival:
$ wormhole receive 7-revenge-spaniel --output got.txt
Receive file 'secret.txt' (24 bytes)? [Y/n] y
receiving (24 bytes) over transit ...
received 24 bytes -> got.txt
$ cat got.txt
the eagle lands at dawn
That's the whole loop: the code rides the relay (PAKE-protected), the file streams over a direct, end-to-end-encrypted transit connection, and a hash confirms it byte-for-byte. Handy variants:
wormhole send --text "ping" # a text message instead of a file
wormhole send ./project # a whole directory (auto-packed)
wormhole receive # no code -> prompt + TAB-complete
wormhole send secret.txt --verify # show + confirm the verifier
Two machines. Run the relay where both can reach it and point each side at
it with --relay HOST:PORT (or export WORMHOLE_RELAY=HOST:PORT). For the
file's direct transit connection the sender advertises its detected LAN IP;
override it with WORMHOLE_TRANSIT_HOST=<reachable-ip>. If the two peers
can't reach each other directly (both behind NAT), also run a separate
wormhole transit-relay and pass --transit-relay HOST:PORT to both — see
Usage.
What works
- Wormhole codes —
nameplate-word-wordusing the real PGP even/odd word list; tab-completion helpers included. - PAKE — SPAKE2 (symmetric) over an RFC 3526 2048-bit MODP group, so a low entropy code yields a strong shared key with no offline dictionary attack.
- Key confirmation + verifier — an encrypted
versionphase confirms both sides derived the same key; a verifier string is available for an out-of-band check. - Mailbox relay — a WebSocket rendezvous server (
wormhole relay) that allocates nameplates and relays the encrypted phase messages between peers. - Text transfer — rides entirely on the mailbox.
- File transfer — negotiated over the mailbox, streamed over a transit
connection as length-prefixed encrypted records, with a SHA-256 confirmation
back to the sender. The receiver enforces the offered size exactly, hashes
while writing to a private
0600temporary, and publishes only a complete file without replacing an existing destination. The receiver is asked to confirm before accepting (--acceptto skip). - Directory transfer —
wormhole send ./dirpacks the tree into a flat archive and streams it like a file; the receiver unpacks it through held directory descriptors, never follows archive-created symlinks, and never replaces an existing file (wormhole/archive.ss). - Transit relay (NAT) — when two peers can't reach each other directly,
both connect to a buddy-matching transit relay that pipes the encrypted
records between them (
wormhole transit-relay; opt-in via--transit-relay). - Verifier check —
--verifyshows the verifier on both sides and waits for confirmation before transferring (defends against a MITM). - Codes — auto-allocated, custom (
--code 4-foo-bar), adjustable length (--code-length), and interactive entry with TAB completion whenreceiveis run with no code. - ssh-key transfer —
wormhole ssh invite/wormhole ssh accept CODEappends the accepter's public key to the inviter's~/.ssh/authorized_keys. - Tor / SOCKS5 —
--torroutes the mailbox and transit connections through a SOCKS5 proxy (Tor's127.0.0.1:9050by default, proxy-side DNS). - Dilation — a leader/follower encrypted connection multiplexing many
bidirectional subchannels (
wormhole/dilation.ss), with reconnect/resume: every frame is sequence-numbered, ACKed, and held in an unacked buffer until the peer confirms it, so when the L2 connection drops a manager re-establishes it (next generation) and replays the unacked records — the subchannels survive with no lost or duplicated data. Reader/writer threads over a never-allocating-under-lock queue keep it GC-safe.
Encryption is ChaCha20-Poly1305 (NaCl-SecretBox-style framing); hashing/MAC is
SHA-256 / HMAC-SHA256; KDF is HKDF-SHA256 — all from the bundled, ring-backed
libjerboa_native, so there's no OpenSSL dependency and it runs the same on
macOS and Linux.
Limitations
The transit relay must run as its own process (not co-hosted with the
mailbox): its bulk byte-piping allocates heavily, and sharing the mailbox's heap
perturbs the mailbox under Chez's stop-the-world GC. So wormhole relay starts
the mailbox only, and the transit relay is opt-in — even then the relay path is
~90% reliable under heavy concurrent load (the direct path is solid), so clients
retry. The wire format is our own, so this does not interoperate with the
Python wormhole.
Receiver resource policy is deliberately finite: an offer is at most 64 GiB; transit plaintext records are exactly 64 KiB except for the final remainder; and a directory archive is limited to 10,000 files, 4,096-byte paths, 16 GiB per entry, and 64 GiB total file data. The receiver checks available space before writing, requires the logical end marker at the exact offered byte count, rejects trailing archive bytes, and removes partial temporary/output files after failure.
Debug logging is off unless the operator explicitly configures a path. Client
protocol diagnostics use WORMHOLE_BOSS_DEBUG_LOG and
WORMHOLE_TRANSIT_DEBUG_LOG; relay processes use
WORMHOLE_MAILBOX_DEBUG_LOG and WORMHOLE_RELAY_DEBUG_LOG. WH_DEBUG no
longer chooses predictable /tmp files. New logs are created exclusively at
0600, while an existing log must already be a regular, singly-linked,
current-user-owned 0600 file and must not exceed 16 MiB at startup. Rotate or
erase it explicitly as the service user, then restart the component. Logged
events omit sides, nameplates, codes, handshake material, relay tokens, and
private paths.
Build
Needs jerbuild and a C compiler — nothing else (Chez + the
Jerboa stdlib are bundled).
make binary # -> ./wormhole-bin
make install # -> ~/.local/bin/wormhole
make test # unit + transport tests
make verify # security check + tests + bundled native RustSec audit
make sbom
make reproducibility-report
make soak-evidence
make release-evidence
make e2e # separate-process send/receive (text + file + directory)
make e2e-relay # transit-relay (NAT) path, relay as a separate process
make dilate # Dilation multiplexed-subchannel end-to-end
make dilate-reconnect # Dilation survives an L2 drop with no data loss
make soak-evidence runs local direct, transit-relay, regular dilation, and
dilation-reconnect smokes. When timeout(1) or gtimeout(1) is available, the
smokes run under an outer timeout and the timeout posture is recorded in
dist/soak/status.txt.
make binary is self-contained: tools/cc-static.sh force-loads the
ring-crypto objects from libjerboa_native.a into the executable and exports
them, so Chez's dlsym resolves them from the binary itself — no
libjerboa_native.{dylib,so} is needed at runtime. Build the plain,
dylib-dependent binary instead with make binary STATIC=0 (it then needs
libjerboa_native.{dylib,so} next to it or under ./lib). The jerbuild exec
dev path always uses the shared library.
Usage
The two peers meet at a relay. Run one anywhere both can reach it:
wormhole relay 4000
Then point the clients at it (default 127.0.0.1:4000, override with
--relay HOST:PORT or $WORMHOLE_RELAY):
# text
wormhole send --text "hello there" --relay relay.example:4000
wormhole receive 4-purple-sausages --relay relay.example:4000
# a file (the receiver is asked to confirm; --accept skips the prompt)
wormhole send ./photo.jpg --relay relay.example:4000
wormhole receive 4-purple-sausages --output got.jpg --relay relay.example:4000
# a directory (packed + unpacked automatically)
wormhole send ./project --relay relay.example:4000
wormhole receive 4-purple-sausages --output ./project --relay relay.example:4000
# custom code, out-of-band verify, interactive entry (TAB completes)
wormhole send --code 4-foo-bar report.pdf --verify
wormhole receive --verify # prompts: Enter receive wormhole code:
The sender advertises its detected LAN IP as the direct transit address;
override with $WORMHOLE_TRANSIT_HOST. When two peers are both behind NAT and
can't connect directly, run a transit relay as a separate process and point
both clients at it:
wormhole transit-relay 4001 # on a reachable host
wormhole send ./big.iso --relay R:4000 --transit-relay R:4001
wormhole receive CODE --relay R:4000 --transit-relay R:4001
Design notes
Each connection is owned by a single thread and clients are single-threaded:
the mailbox protocol is request/response, so every receive is an inline
blocking read. The relay runs in its own process. This is deliberate — Chez's
stop-the-world GC deadlocks if a thread is parked in a non-__collect_safe
foreign call (or condition-wait, or a contended mutex) while another thread
allocates. The socket and crypto FFI here are therefore declared
__collect_safe (wormhole/net.ss, wormhole/crypto-native.ss). The full
investigation is written up in
~/mine/jerboa/docs/chez-gc-findings.md.
Layout
wormhole/crypto.ss sha256 / hmac / hkdf / hex / mod-expt
wormhole/crypto-native.ss GC-safe ring FFI
wormhole/secretbox.ss ChaCha20-Poly1305 authenticated box
wormhole/spake2.ss SPAKE2 PAKE (MODP group)
wormhole/wordlist.ss PGP word list
wormhole/codes.ss nameplate-word-word codes
wormhole/messages.ss phase keys + JSON payloads
wormhole/net.ss GC-safe TCP sockets
wormhole/ws.ss WebSocket transport
wormhole/rendezvous.ss mailbox client (single-threaded)
wormhole/boss.ss code/PAKE/key/phase orchestration
wormhole/transfer.ss offer/answer app protocol
wormhole/transit.ss bulk file/dir transfer (direct + relay)
wormhole/archive.ss flat directory archive (pack/unpack)
wormhole/socks.ss SOCKS5 client (Tor)
wormhole/queue.ss GC-safe lock-light FIFO
wormhole/dilation.ss multiplexed subchannels + reconnect/resume
wormhole/input.ss interactive code entry + TAB completion
wormhole/cli.ss the `wormhole` command
server/mailbox-server.ss the rendezvous relay
server/transit-relay.ss the transit relay (NAT buddy-matching)
server/relay-main.ss standalone relay launcher
tools/cc-static.sh link wrapper: bake ring crypto into the binary
License
Apache License 2.0. See LICENSE and NOTICE for clean-room provenance notes.