Message Transport

Message Transport

WebSocket opcode dispatch table, room identity and lifecycle, message validation, and the AES-256-GCM E2EE envelope.

Message Transport

#

Messages on the WebSocket are JSON objects with an op field and a d data field. The client sends operations; the server responds with the same opcode where a reply is expected, or pushes events asynchronously.

{ "op": 7, "d": { "text": "hello", "gameId": "<room id>" } }

The dispatch table from websocket/protocol.rs::process_message is:

OpcodeNameDirection
0Ping or errorBoth
1HeartbeatBoth
2Identify (authenticate session)Client to server
3Join roomBoth
4Leave roomClient to server
5Report killClient to server
6Version queryClient to server
7Send chat messageBoth
8Update client settingsClient to server
18Request room historyClient to server
19Toggle message reactionClient to server
21Delete messageClient to server
28Request link previewClient to server
29Edit messageClient to server
31Update typing stateClient to server
32Upload room iconClient to server
33Update room titleClient to server
35Request public profilesClient to server
36Publish prekey (PHANTOM)Client to server
37Fetch prekeys (PHANTOM)Client to server
39Update block list (PHANTOM)Client to server
40Create roomClient to server
41Update room descriptionClient to server
42Set member roleClient to server
43Ban memberClient to server
44Unban memberClient to server
45Kick memberClient to server
46Timeout memberClient to server
47Transfer ownershipClient to server
48Set chat lockClient to server
49Set moderator permissionsClient to server
50Set calls enabledClient to server
51Set call accessClient to server
52Unmute memberClient to server
98Update voice chat stateBoth
100Update mute stateClient to server
101Admin statusClient to server
104Admin broadcastClient to server
105Stats queryClient to server
110Update call media stateBoth
111Relay call signalBoth
112Update call deafened stateBoth
23Link preview patchServer to client
999Session evictionServer to client

Room identity and access token

A room has two identifiers:

FieldBytesEncoding
roomId16lowercase hex, 32 characters
roomKey32lowercase hex, 64 characters

A room access token is the concatenation roomId || roomKey, 96 hex characters. The roomId is a routing label visible to the server; the roomKey is the symmetric encryption secret shared by room members. The server stores room metadata keyed by roomId but does not require the roomKey to route or store messages.

Room lifecycle

Joining is opcode 3. The server:

  • Rate limits the join.
  • Validates the room id length and character set.
  • Requires an identified session.
  • Enforces a maximum of 100 rooms per player.
  • Rejects joins to a community room if the user is banned.
  • Persists the room record if it is new.
  • Broadcasts the updated roster, unless the join is silent or the user is invisible.
  • Returns the roster, profiles, presence, role, icon, and room record to the joining client.
  • Dispatches the buffered room history.

Room membership lives in PlayerSession.rooms. Room metadata such as title, icon, description, roles, bans, and timeouts is persisted in the RoomDatabase backend. Leaving is opcode 4; the server can also evict a session with opcode 999 when an account is deleted or disabled.

Message sending

Messages are sent with opcode 7. A message is either plaintext or an encrypted envelope, never both.

Validation performed by the server:

  • The room id must be valid.
  • If an encrypted envelope is present, its roomId must match the target room.
  • An encrypted message must not include a plaintext text or attachment.
  • The message must be non-empty.
  • The sender must be identified and a member of the room.
  • A minimum 400 millisecond interval applies between messages per session.
  • A community room can restrict who may speak.
  • Plaintext is truncated to 2000 characters.

The server constructs a ChatMessageRecord, stores it in the RAM message store, and broadcasts it to all room members. The sender receives a separate acknowledgment with the assigned message id.

Message storage

Room messages live in AppState.room_messages, a HashMap<room_id, Vec<ChatMessageRecord>>. The store is capped at 150 messages per room; the oldest are dropped when the cap is exceeded. The store is RAM only and is lost on restart. Room history is served on request with opcode 18.

End-to-end encrypted message envelope

Encrypted messages use a shared room key and a per-message derived AES-256-GCM key. The format is version 2 with algorithm identifier QXDR-A256GCM-HKDFSHA256.

{
      "v": 2,
      "alg": "QXDR-A256GCM-HKDFSHA256",
      "n": 1,
      "salt": "<b64url 32 bytes>",
      "iv": "<b64url 12 bytes>",
      "ciphertext": "<b64url>",
      "roomId": "<hex>",
      "senderDeviceId": "<hex 16 bytes>",
      "senderSigningKey": { "kty": "EC", "crv": "P-256", "x": "...", "y": "..." },
      "signature": "<b64url raw r||s>"
    }

Key derivation

messageKey = HKDF-SHA256(ikm  = roomKey bytes,
                             salt = salt,
                             info = "qxchat:e2ee:v2:" + roomId + ":" + n)
                 -> AES-256-GCM key
    
    aad = roomId + ":" + n + ":" + b64url(salt) + ":" + senderDeviceId

The plaintext JSON is encrypted with AES-256-GCM using the derived key, the random 12 byte IV, and the AAD above.

Authentication

The envelope is signed with the sender device ECDSA P-256 key. The signed bytes are the compact JSON of:

{ v, alg, roomId, n, salt, iv, ciphertext, senderDeviceId, senderSigningKey }

The signature is the raw r || s form encoded as b64url. On receipt, the client verifies the signature, optionally checks that the sender key matches a trusted key, then derives the message key and decrypts. The server parses and bounds-checks the envelope but cannot decrypt it because it does not possess the room key.