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>