Crypto / protocol
- v4 envelope: X25519 + ML-KEM-768 hybrid (PQXDH-style HKDF over both
secrets). Heartbeats now carry an ML-KEM key; senders emit v4 whenever
the peer's heartbeat has one, v3 otherwise. Adds @noble/post-quantum.
- Heartbeats are signed by the KEZ identity and verified against the
recipient's primary before use. Previously a spoofed nostr pubkey
(from a re-wrapped replayed envelope) let an attacker substitute their
own heartbeat and read messages.
- Heartbeat retention counts from supersession, never deletes the
newest key, and GC runs after rotation — returning after a long
offline stretch no longer destroys the key the backlog needs.
- Verified-heartbeat cache per peer: 5-min TTL (one lookup per file
instead of per chunk) and anti-downgrade to identity ECDH.
- Always republish heartbeat on session start; rotate on the poll timer;
atomic IDB updates; wipe heartbeats on "Forget this account".
- Dedupe inbound envelopes by sender_sig (replays / re-broadcasts).
Server
- Proxy /v1/sigchains/* to the internal sig-server and hand out the
public https://<server>/v1/sigchains/... URL. Sigchain sync from the
app was failing ("Load failed") against http://sig-server:7878.
App
- Relay settings apply to profile / NIP-65 publishing (republished on
change); wss-only relays (ws:// for localhost); order-aware defaults.
- Clear chat also deletes attachments; retention changes apply
immediately with a warning when shortening.
- Photos: HEIC -> JPEG and downscale to 2048px (iOS canvas limit, strips
EXIF); snapshot the conversation at send time.
- Overlay a11y/Escape fixes, scroll-pin fix, svelte-check to 0 errors.
- Clear chat, image zoom, file-body fallback rendering, NIP-65 read
relays in publish set, relay settings UI, forward-secrecy settings.
Docs
- README rewritten to explain the whole app; SECURITY.md updated for v4;
heartbeat self-check (src/lib/heartbeat.selfcheck.ts).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Swap the chat transport from the kez-chat server inbox to Nostr relays
without touching the identity model or the E2E crypto. The existing
SealedEnvelope (ed25519/x25519 + AES-GCM, our own key) is unchanged and
becomes the content of a Nostr event — Nostr only moves the bytes.
- nostr-id.ts: derive a secp256k1 signing key from the ed25519 seed
(HKDF, domain-separated — internal transport credential, never the
user's real Nostr account); route by a hash of the recipient's public
ed25519 primary since the curves can't be cross-derived.
- nostr-transport.ts: send/poll/stream mirroring messages.ts, via
SimplePool; per-handle time cursor + seen-id dedupe in localStorage.
- transport.ts: facade selecting server vs nostr via VITE_TRANSPORT
(code default stays "server"; this branch's .env flips it to nostr).
- inbox-service + Messages import from the facade.
Directory lookup (handle->primary) still runs on the kez-chat server;
identity stays internal. Metadata privacy is at parity with the server
transport (relay sees the from/to graph, body stays confidential).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>