Portal chat is E2EE. Messages are encrypted client-side withDocumentation Index
Fetch the complete documentation index at: https://docs.7331.org/llms.txt
Use this file to discover all available pages before exploring further.
nacl.secretbox (XSalsa20-Poly1305) and the server stores — and emits — only opaque ciphertext. This page describes the v2 protocol.
Message envelope
Every chat message on the wire is aCiphertextV2 bundle:
v— protocol version (always2)epoch— integer rotation counter for the chatct—secretbox(plaintext, nonce, room_key)with the 24-byte nonce prepended
epoch doesn’t match the chat’s current epoch (ERR_EPOCH_MISMATCH) and messages from senders who never received a chat key for that epoch (ERR_NO_ROOM_KEY).
Chat keys
Each chat has a symmetric chat key — 32 random bytes generated client-side by the first member and fanned out to every joiner. Fan-out uses X25519 public-key encryption: each user registers an ephemeral(pub, priv) keypair at POST /v1/portal/keys on first portal load. When a new user joins a chat, the key distributor encrypts the chat key with the joiner’s X25519 public key. The server stores the resulting blob at:
Epoch rotation
The epoch is a Redis-backed counter (INCR per chat) that rotates — invalidating every previous key blob — on four triggers:
| Trigger | Why |
|---|---|
| Wipe | User-requested history purge; new conversation must be unreadable to past members |
| Clear | Moderator/owner clears chat; same rationale |
| Kick | Kicked user must not be able to read future messages, even with cached ciphertext |
Joiner on history_visible=false | New member shouldn’t inherit readable history of a closed-history chat |
- Deletes all old key blobs for the old epoch
- Emits
room:epoch_changedto every member - One existing member (the “distributor”) generates a fresh chat key and re-fans-out to everyone at the new epoch
What the server cannot do
- Read message plaintext
- Reconstruct chat keys from key blobs (no private keys stored server-side)
- Silently add a member — a key distribution event is visible to all clients
- Replay messages under a different epoch — the version check rejects them
What the server still can do
- See who sent what, when, and to which chat
- See message lengths (up to padding)
- Stop delivery or kick users
- Freeze history (rotate the epoch without re-fanning)
