parley

package module
v0.6.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 18, 2026 License: MIT Imports: 0 Imported by: 0

README

parley

Croc-style pairing for long-lived, end-to-end-encrypted group sessions. A host gets a code phrase (lion-42-maple); others join with it. A relay — hosted anywhere, one http.Handler — forwards frames it can never read.

parley is the extracted session core of kibitz, where it carries chat and thirteen board games between browsers.

How it works

  • The phrase is the secret. It seeds a PAKE handshake (schollz/pake/v3, curve "siec" — croc's default) between each joiner and the host; the relay only ever sees SHA-256(label ∥ phrase)[:16], so it cannot even attempt to join.
  • One group key, wrapped per member. The host wraps a random 32-byte group key to each joiner under the PAKE-derived pairwise key (HKDF-SHA256). All traffic is XChaCha20-Poly1305 with associated data binding session, protocol version, and sender — no cross-session replay, no sender reflection.
  • Leavers are locked out. Any departure rotates the group key to the survivors. If the host leaves, a survivor is promoted and the rest re-PAKE with it; a short previous-key ring keeps in-flight frames decryptable across the swap.
  • The relay is blind and dumb. Session IDs, participant counts, opaque frames. Unexpected drops hold the slot for a grace window so clients can resume; clean leaves are final.

Usage

// Server: mount the relay anywhere.
srv := relay.New(relay.Options{})
http.Handle("/ws", srv)

// Host: create a session, share the phrase out of band.
client, phrase, err := session.Host(ctx, "wss://example.com/ws")

// Joiner: pair with the phrase.
client, err := session.Join(ctx, "wss://example.com/ws", phrase)

// Both ends: encrypted app frames by service ID.
client.Broadcast("chat", body)
for ev := range client.Events() { /* Frame, MemberKeyed, MemberLeft, Closed */ }

Protocol labels

Every end of a session must agree on an application label — it domain-separates the session-ID hash and the PAKE key schedule:

session.Host(ctx, url, session.WithProtocol("myapp/v1"))

The default is "parley/v1". Changing an application's label is a protocol version bump: ends with different labels derive different session IDs and keys and cannot talk. (kibitz's deployed label is "kibitz/v1"; both derivations are pinned by golden tests.)

Services

The service package multiplexes application services over one session: implement service.Service (ID/HandleFrame/Snapshot/Restore), register with service.NewMux(client, service.WithServices(...)), and get a host-authoritative roster with display names, snapshot catch-up for late joiners, per-sender sequence tracking, and host-migration election (WithSuccessor for custom policies). service/chat is a ready-made text chat with deduped late-join history.

Multi-node

One relay node is authoritative for a session in memory — the state is live socket ownership, not serializable — so you scale by session-affinity sharding: run N nodes and route every connection for a SessionID to the same node. Options.Router is the only seam: it fires before the WebSocket upgrade with the SessionID (from a ?s=<hex> query the client always sends) and the raw request, and returns either "serve here" (zero value) or a replay directive (headers + status). Nil Router = single-node, unchanged. The consistent-hashing + platform routing (e.g. Fly-Replay) lives in your app, not the library. For a clustered dashboard, relay.MergeStats plus dashboard.NewAggregator/InternalStatsHandler fan a stats poll out to peers and merge the shards.

Access-log caveat: the hex SessionID rides in ?s= and so appears in server/proxy access logs. It is not new information to the relay (it already learns the id from the hello frame) and is not secret, but it is correlatable across a session's requests — scrub ?s= from logs if you consider the hash sensitive. The phrase never appears in the URL and never leaves the client.

The optional dashboard package adds a GitHub-OAuth-gated admin page over the relay's blind Stats() snapshot: dashboard.New(cfg, relaySrv).Register(mux), gated on your own env/secrets, branded via Config.AppName. It reads only the relay's aggregate counters — never session contents.

Notes

  • session (and wire/crypto/phrase) compile to GOOS=js GOARCH=wasm; the relay is server-side only.
  • Reconnect = resume for network drops (relay holds the slot for a grace window); otherwise rejoin with the phrase.
  • Roles are pluggable: the host's RolePolicy (see WithRolePolicy) assigns each keyed joiner a role byte that rides inside the encrypted handshake — values 2..255 are the application's vocabulary. The default policy seats WithObserver joiners as RoleObserver and everyone else as RoleMember. Pass the policy to Join too: a joiner promoted by host migration becomes the role assigner.

MIT licensed.

Documentation

Overview

Package parley is a phrase-paired, end-to-end-encrypted group session library with a blind relay — croc-style pairing for long-lived sessions.

A host gets a short code phrase (like "lion-42-maple"); others join with it. The phrase never leaves the clients: it seeds a PAKE handshake (schollz/pake/v3) between each joiner and the host, the host wraps a random group key to each joiner under the PAKE-derived pairwise key, and all application traffic is XChaCha20-Poly1305 under the group key. The relay forwards opaque frames it can never read; it sees only a hash of the phrase (the session ID) and participant counts. The group key rotates whenever a member leaves, and host departure triggers migration: a survivor is promoted and the others re-PAKE with it.

Subpackages:

  • phrase: code-phrase generation (EFF short wordlist) and session-ID derivation
  • wire: the framing protocol — versioned messages and encrypted payload envelopes, deterministic CBOR
  • crypto: the security boundary — PAKE, HKDF pairwise keys, group-key wrap/unwrap, AEAD sealing with session/sender-bound associated data
  • session: the client engine — dial, handshake, events, rekey, reconnect/resume, host migration (compiles to WASM)
  • relay: the server — a blind frame forwarder, one http.Handler. Options.Router enables session-affinity sharding across multiple nodes; MergeStats combines per-node snapshots.
  • service: the layered-service mux — routes decrypted envelopes to application services, host-authoritative roster/names, snapshot transfer for late joiners, host-migration election (compiles to WASM)
  • service/chat: a ready-made text-chat service with late-join history
  • dashboard: an optional GitHub-OAuth-gated admin dashboard over the relay's blind Stats() snapshot (server-only)

Every end of a session must agree on an application protocol label (see session.WithProtocol): it domain-separates session IDs and the key schedule, so two applications built on parley can never be cross-joined.

Directories

Path Synopsis
Package crypto is parley's security boundary.
Package crypto is parley's security boundary.
Package dashboard serves a read-only admin view of the relay's blind-safe internal state, gated by GitHub OAuth and restricted to an allowlisted set of usernames.
Package dashboard serves a read-only admin view of the relay's blind-safe internal state, gated by GitHub OAuth and restricted to an allowlisted set of usernames.
Package phrase generates croc-style code phrases and derives session IDs from them.
Package phrase generates croc-style code phrases and derives session IDs from them.
Package relay implements the parley relay server: a blind frame forwarder.
Package relay implements the parley relay server: a blind frame forwarder.
Package service defines the layered-service abstraction and the mux that routes decrypted envelopes to services.
Package service defines the layered-service abstraction and the mux that routes decrypted envelopes to services.
chat
Package chat is the simplest layered service: broadcast text messages with a bounded history that late joiners receive via the ctl snapshot.
Package chat is the simplest layered service: broadcast text messages with a bounded history that late joiners receive via the ctl snapshot.
Package session is the client-side engine: it dials the relay, runs the create/join handshake, performs the PAKE + group-key exchange, and moves encrypted service envelopes.
Package session is the client-side engine: it dials the relay, runs the create/join handshake, performs the PAKE + group-key exchange, and moves encrypted service envelopes.
The inner payload layer: what clients put inside Direct/Broadcast Payload fields.
The inner payload layer: what clients put inside Direct/Broadcast Payload fields.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL