That didn’t look like a valid p2p key — a key is 26 Crockford-base32 characters. Here’s the normal install instead.
Install p2p and start chatting with them — you’ll become friends and can reconnect anytime.
their key — install below, the key is already baked into the command.
p2p is a tiny chat framework with zero dependencies and no coordinator server of ours. One 26-character key is your whole contact surface: send it to a friend, they find you anywhere on the internet and message you — direct, end-to-end encrypted, and provably free of a man-in-the-middle from the very first message.
This page explains the whole thing — including the part that sounds impossible: how two computers behind two home routers find each other and talk, with nobody in the middle to introduce them.
01 — identity
Your machine generates two keypairs — Ed25519 (signing) and X25519 (Diffie–Hellman) — and keeps the
private halves on disk (~/.p2p/default.json, mode 0600). Your 26-character key is
not random: it is a fingerprint of your public keys, in Crockford base32 (uppercase,
no I L O U, so it survives being read aloud or typed).
SHA256("p2p-id-v1" ‖ edPub ‖ xPub) · 15-bit checksum (a typo is caught locally, before any
packet leaves). Nobody can hand out a string that points at your identity but is secretly theirs: they
would have to find a second preimage of a 110-bit hash — 2110 work. The checksum makes the key
safe to dictate over the phone.02 — the magic
Normally “how does computer A reach computer B?” needs a server both sides already know — a chat server, a signalling server, something with a fixed address. We have none. Three ideas stacked make it work anyway.
Idea 1 — the key doubles as a secret meeting point. Both sides hash the key into a
rendezvous id: rid = HKDF(S, "p2p-rv-<channel>-v1", epoch). Anyone holding the key
computes the identical rid; anyone who doesn’t, can’t — it’s one-way. The epoch is the UTC day, so the rid
rotates daily (readers check ±1 day; listeners pre-announce the next epoch, so there’s no blackout at
midnight). No server told the peers where to meet. The math did.
Idea 2 — leave the note on bulletin boards that already exist. We don’t run a server, but the internet is full of free, public, shared notice boards built for other reasons. We ride three, publish to all of them, and race the reads.
ip:port hint; the tracker is a live matchmaker
that carries the full candidate list (IPv6 / LAN / STUN) in the offer blob. Zero trust in all of them:
anyone can announce a lie — the commitment gate and the handshake below make lies useless.03 — the magic, part two
Idea 3 — the hole punch. Both sides now know each other’s public address, but home routers (NAT) drop unexpected inbound packets. So each side first asks a free public STUN server “what does my address look like from outside?”, then both fire a UDP packet at the other at the same moment. Your outbound packet teaches your own router “I’m expecting a reply from that address” — which props the pinhole open just long enough for the other side’s packet to arrive. Both firewalls open at once, and a direct path exists.
ip:port; the simultaneous send makes each NAT believe the inbound packet is a reply it already
expected. Direct connectivity succeeds for roughly 70–90% of real-world pairs. When both peers sit behind
symmetric NAT, no direct path can form — the design falls back down the ladder to a relay through a
mutual online peer (still end-to-end encrypted, so the relay is blind; v2).04 — the proof
A path exists — but is the machine at the other end really the owner of the key? Every rendezvous channel is treated as hostile: anyone holding the key can announce a bogus address. Two mechanisms make that harmless.
HELLO is public and replayable, so it authenticates
nothing on its own — it only supplies candidate public keys, which are gated against the 110-bit
commitment baked into the key you were given. Then a standard Noise IK handshake (validated byte-exact
against the official noise-c test vectors) runs on top. connect() resolves only after msg2
decrypts — that success is the MITM proof. When the CLI prints ✅ secure channel established —
verified, no MITM, that line is a mathematical statement, not a hopeful label. Honest limits: forward
secrecy is session-granular (no in-session ratchet), and metadata (an IP, an opaque rid) is visible to the
rendezvous channels.05 — behavior
First contact, the same-Wi-Fi fast path, the cross-internet path, the fallback ladder, and what happens when a peer’s laptop closes and comes back.
wire.js gives ordered, exactly-once delivery within a session; node.js adds an
app-level outbox keyed by peer identity, so messages queued while a friend was offline flush on reconnect and are
never delivered twice. Honest status: same-LAN is verified end-to-end; cross-internet machinery (live STUN, live
DHT lookups, live trackers, punch) is proven piecewise; offline store-and-forward and peer-relay are v2.06 — the code
Node ≥ 22, plain ESM, no build step, zero npm dependencies — it is the only runtime where every
primitive is native: X25519, Ed25519, ChaCha20-Poly1305, HKDF, SHA-256 (node:crypto), UDP/TCP
(node:dgram/node:net), WebSocket (global).
| module | job | lines |
|---|---|---|
| src/key.js | Identity. Keygen, the 26-char encode/decode, checksum, the commitment gate, rid derivation. | 225 |
| src/noise.js | Crypto. Noise_IK_25519_ChaChaPoly_SHA256, in-house, byte-exact against the official noise-c vectors. | 381 |
| src/wire.js | Reliability. Framing, sliding-window ARQ, cumulative ack, RTT-adaptive resend, keepalive, connId roaming. | 361 |
| src/transport.js | Pipes. Dual-stack UDP, in-house STUN client, ICE-lite hole punch, TCP fallback. | 555 |
| src/rendezvous/ | Meeting. mDNS, Mainline DHT (+ bencode), WebSocket trackers, and the publish-fanout / read-race. | 1159 |
| src/node.js | The API. identity(), listen(), connect(), peer lifecycle, the reconnect outbox. | 420 |
Use it as a library — it is designed to be the messaging backbone of any project:
import { identity, listen } from '@fire17/p2p'
const me = await identity() // me.S = your 26-char key — share it
const node = await listen(me)
node.on('message', (peer, data) => console.log(data.toString()))
const peer = await node.connect(theirKey) // resolves only after the MITM-proof handshake
await peer.send('hey')
07 — people
Everyone you connect with is remembered — their key is saved locally (~/.p2p/<profile>.friends.json),
so you never need the long string twice. p2p friends lists them; reconnect by name. Because the rid is
derived from the key, a friend is findable again from anywhere, on any network, whenever you’re both online. No
account, no directory, no server that could ever be asked who your friends are.
08 — get it
curl -fsSL https://p2p.akeyo.io/init | sh
irm https://p2p.akeyo.io/init.ps1 | iex
Already have p2p? Just run p2p
p2p command on your machine — no account, no signup.p2p key prints your 26 characters. That string is you.p2p.akeyo.io/init/<your-key>, which installs p2p and
dials you in one step.p2p <key>; you both see ✅ verified, no MITM and you’re talking — directly.The installer is a plain shell script you can read before you run it: init · init.ps1.