Jason Tudisco 5cb46e2aa1 feat(kez-chat): v0.1 chat — encrypted 1:1 messages (server + web client)
Time to actually chat. Server is a dumb relay storing opaque envelopes;
recipients decrypt client-side. Everything below is end-to-end encrypted,
the server can't read anything it stores.

Server (kez-chat-server):
  • New messages table (seq autoinc, recipient_handle, envelope blob,
    created_at). Indexed by (recipient, seq) for cursor paging.
  • POST /v1/messages
      body: { to: handle, envelope: <opaque JSON> }
      validates recipient exists; rejects > 256 KB envelopes.
  • GET /v1/inbox/:handle?since=<seq>&limit=<n>
      auth: X-KEZ-Auth: <unix_ts>:<sig_hex>
      sig = ed25519(handle's primary,
                    "GET\n/v1/inbox/<handle>\nsince=<n>\n<ts>")
      60s clock-skew tolerance; signed message includes cursor so a
      captured header can't page through history.
  • New ApiError::Unauthorized → 401.
  • kez-core: verify_ed25519_hex is now pub so the auth handler can
    use it for arbitrary-message verification (outside JCS envelopes).

Crypto (browser):
  • ed25519 seed → x25519 priv via Montgomery conversion
    (ed25519.utils.toMontgomerySecret).
  • ed25519 pubkey → x25519 pubkey for the recipient (toMontgomery).
  • ECDH → 32-byte shared secret → HKDF-SHA256(salt=nonce, info=
    "kez-chat-msg-v1") → AES-256-GCM key.
  • Per-message random 12-byte nonce; each message gets a unique AES key.
  • Sender signs envelope-minus-sig with their ed25519 primary so the
    recipient can confirm the sender authored the ciphertext + binding.

SPA UI:
  • /messages route, two-pane layout (sidebar conversations, thread view,
    compose box).
  • 5-second poller against /v1/inbox using the global cursor; new
    messages get decrypted + appended to the right thread.
  • Local IDB cache (lib/conversations-store.ts) so decrypted history
    survives reloads. Dedupes by seq+direction.
  • Page-specific max-w-6xl so the two-pane layout has room.

Tests:
  • 6 new unit tests in messages.rs covering auth header verification
    (stale ts, wrong handle, wrong cursor, malformed).
  • 4 new integration tests in tests/http.rs: full send + inbox round-
    trip, wrong-signer rejected, missing header rejected, unknown
    recipient → 404.
  • All 17 chat-server tests pass.

Followups (deferred):
  • NATS WebSocket push (live messages without 5s poll lag).
  • Group chats with proper member-key rotation.
  • Reverse handle resolution (/v1/by-primary) so the UI can show
    "@alice" instead of the truncated ed25519 hex.
  • At-rest encryption for the IDB conversations cache.
  • Sender spam mitigation on POST /v1/messages.

Live at https://kez.lat — try /messages with two browsers.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 16:10:43 -06:00
..