Upgrades the signaling layer from static X25519 (2.0) to per-session
ephemeral X25519 (2.1). Recorded signaling traffic cannot be decrypted
even if long-term Ed25519 keys later leak, because esk is zeroed on
session close.
Protocol:
- Each peer generates a fresh X25519 keypair (esk/epk) per session.
- Peers exchange signed `ekey` messages sealed under static keys before
the offer/answer. Offer/answer/candidate payloads are then sealed with
ephemeral keys (crypto_box(·, peer_epk, my_esk)).
- ekey sig binds both peer IDs and the epk to prevent replay to third parties.
- Offerer waits up to 2 s for the peer's ekey; if none arrives it falls back
to YAW/2.0 static-key sealing and logs "2.0 fallback offer".
- 2.0 peers silently ignore the unknown `ekey` kind — full interop preserved.
Implementation:
- crypto.go: add EphemeralKey.PublicRaw/PrivateRaw/Wipe helpers.
- proto.go: add SigEkey kind; EPK/V/EkeySig fields on SignalingPayload.
- anchor/client.go: replace flat pcs map with peerSession struct tracking
ephemeral keys, peerEPK, and fs flag; openBoxAuto tries ephemeral then
static; sealAndSend chooses seal based on session state.
- test-network.sh: pipe daemon stderr through tee to daemon.log; add
YAW/2.1 FS verification section.
- test-tui.sh: same daemon.log capture.
- README.md: document 2.1 forward secrecy, file transfer IPC, updated roadmap.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
File chunks travel over a per-transfer "f:<xid>" WebRTC DataChannel —
direct DTLS-encrypted P2P, the anchor never sees file data. Once the
initial handshake is done the anchor can disappear and transfers continue.
Key design choices:
- Receiver sends "ok" on the file DC before sender streams, eliminating
a race where tiny files could be fully sent/closed before the receiver's
OnMessage handler is registered (open-race §6 analogue for data DCs).
- Auto-accept: receiver accepts every incoming offer immediately.
- Download dir: per-network at <data-dir>/downloads-<netid>/.
- Backpressure: bufferedAmountLowThreshold to avoid overwhelming the DC.
- SHA-256 verified on receive; mismatches emit EvtError and discard temp file.
- IPC: send_file {peer_id, path} → offers the named file from share dir.
- EvtFileComplete {transfer_id, path} emitted on success.
IPC command: {"type":"send_file","peer_id":"<hex>","path":"<filename>"}
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Five interop issues fixed against PROTOCOL.md:
- Join signature now covers nonce_raw || net_ascii (64-char UTF-8 hex
string) as specified in §5.1, not net_raw_bytes. Both anchor server
and client updated to match.
- Chat wire fields renamed to spec names: text/ts (Unix ms int64)
replacing body/sent_at (ISO timestamp). Flat layout on PeerMessage
matches §8 exactly; store and TUI updated accordingly.
- Direct messages now use the spec "pm" type (flat {type,mid,text,ts})
instead of chat+to. Receiver reconstructs a ChatMessage with
dm:<short-id> room for IPC/storage. §8 compliant.
- File transfer message types changed to spec hyphenated names:
file-offer, file-accept, file-cancel, file-done with spec field
names (name/size not filename/size_bytes). §9 compliant.
- DataChannel open-race (§14 gotcha #3) fixed with sync.Once: doOpen
fires on OnOpen callback or immediately if the channel is already
open when WireDataChannel is called (answerer race).
Also fixes two bugs found during testing:
- mid was missing from outgoing wire messages, causing all received
messages to arrive with mid="" and collide on the UNIQUE DB
constraint. mid is now included on all sent chat/pm messages; a
random mid is generated for any received message that omits it.
- Test scripts hardened: kill -9 + active lsof polling replaces blind
sleep for port cleanup; join_network sent before peer_field queries
(local_peer is now network-scoped and nil until joined).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>