No description
  • Scheme 81.4%
  • Shell 15.5%
  • Makefile 3%
  • Common Lisp 0.1%
Find a file
2026-07-30 15:47:41 -06:00
.builds ci: set umask 022 in package/verify tasks too (jerbuild creates group-writable cache dirs during pkg operations) 2026-07-22 13:11:00 -06:00
.jerboa Harden wormhole release evidence privacy 2026-06-26 17:52:45 -06:00
docs Resolve security audit findings 2026-07-11 17:27:55 -06:00
scripts Resolve security audit findings 2026-07-11 17:27:55 -06:00
server Resolve security audit findings 2026-07-11 17:27:55 -06:00
support Security hardening and release readiness 2026-06-23 10:52:12 -06:00
test Resolve security audit findings 2026-07-11 17:27:55 -06:00
tools Resolve security audit findings 2026-07-11 17:27:55 -06:00
wormhole fix: constant-time handshake, streaming file transfer, bound buffers, atomic writes, bind address 2026-07-21 09:23:13 -06:00
.gitignore jpkg: complete binary package setup 2026-07-30 15:26:17 -06:00
.jerbuild Resolve security audit findings 2026-07-11 17:27:55 -06:00
AGENTS.md docs: remove jerboa-emacs restriction 2026-07-30 15:47:41 -06:00
jpkg.lock jpkg: complete binary package setup 2026-07-30 15:26:17 -06:00
jpkg.policy.sexp jpkg: complete binary package setup 2026-07-30 15:26:17 -06:00
jpkg.sexp jpkg: complete binary package setup 2026-07-30 15:26:17 -06:00
LICENSE Switch to MIT license 2026-07-21 13:42:22 -06:00
Makefile build: add lint target 2026-07-21 11:53:04 -06:00
NOTICE Security hardening and release readiness 2026-06-23 10:52:12 -06:00
README.md Resolve security audit findings 2026-07-11 17:27:55 -06:00
SECURITY.md Resolve security audit findings 2026-07-11 17:27:55 -06:00

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 codesnameplate-word-word using 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 version phase 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 0600 temporary, and publishes only a complete file without replacing an existing destination. The receiver is asked to confirm before accepting (--accept to skip).
  • Directory transferwormhole send ./dir packs 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--verify shows 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 when receive is run with no code.
  • ssh-key transferwormhole ssh invite / wormhole ssh accept CODE appends the accepter's public key to the inviter's ~/.ssh/authorized_keys.
  • Tor / SOCKS5--tor routes the mailbox and transit connections through a SOCKS5 proxy (Tor's 127.0.0.1:9050 by 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.