Files
kokkoja-rel/docs/implementation.md
T

15 KiB

How Kokkoja works

This part is for the curious, for anyone who wants to run a Kokkoja server, and for anyone who wants to read or change the code. It describes the parts, what travels between them, what is protected and what is not, and how pages are stored. The code is the final word; it is short and commented.

The parts

Kokkoja is three programs and a few static files.

  • The client (client/) runs in the web browser and does everything the user sees: the pen, the page, the text, the encryption, the saving. It is plain JavaScript modules, run by the browser as written, with no build step and no third-party packages. Its types are written in JSDoc comments and checked by the TypeScript compiler during development.
  • The local program (kokkoja.py) runs on the user's computer. It serves the client to the browser at http://localhost:8760/, keeps the user's identity, and reads and writes the notebook directories, which a web page cannot do on its own. It uses nothing but Python's standard library.
  • The relay (relay/kokkoja_relay.py) runs on a server that every participant can reach. It passes messages between the members of a session and nothing more: it cannot read them and it stores nothing. It is Python with one library, websockets.
  • The web site is the client/ directory served over HTTPS, with a config.json beside it, so that a guest can join without the local program. deploy/ has the server setup.

Every participant's computer holds the whole page. Only changes travel: a stroke as it is drawn, a text block as it is typed, an erasure, a new page, an image once. There is no central copy.

Why the relay exists

Two computers behind home routers generally cannot connect to each other directly, so something in between has to pass the messages. The relay is the smallest such thing: a WebSocket server that passes messages among the members of a session. What a user calls a session, the relay calls a room: the word appears in the relay's code and log and in the address a client connects to, wss://<server>/relay/room/<room id>, and nowhere else. The relay gives each connection a number, tells the others that a peer has arrived, and from then on sends every message it receives from one member to all the others, stamped with the sender's number. When the last member leaves, nothing of the session remains at the relay. It holds at most 32 members per session and 500 sessions, and refuses messages over 1 MB; clients split anything larger.

Sessions and encryption

The invitation is

<relay address>#<secret>.<host id>

The secret is 16 random bytes, made by the host's browser. From it every client derives two values with HKDF-SHA-256: the room id, which names the session at the relay, and the message key, a 256-bit AES-GCM key with which every message is encrypted before it is sent and decrypted on arrival. The relay sees the room id, the members' network addresses, and the size and timing of encrypted messages. It cannot compute the key from the room id, so it cannot read anything. Neither can anyone watching the network between the participants and the relay, which is TLS-protected in any case.

A link invitation wraps the same text:

https://<site>/#join=<invitation>

Browsers never send the part of an address after # to the site, so the secret stays between the host and the guests. The Session window also shows the link as a QR code, so that a phone or a tablet in the room can join by camera; the encoder is Kokkoja's own (qr_code.js: byte mode, error correction level M, any version), checked by the tests against Nayuki's reference encoder.

The client writes its singular events (pages, images, the connection, the co-host right, the first input report) to the browser's console and keeps them, with a trace of every press, release and cancellation of the pointer (an eraser's release says how many strokes and text blocks it removed), every change of tool and the touch events, in memory (log.js, the last 400 entries). The Session window shows the record with a Copy button, so that a guest on a tablet, where there is no console, can send it when the pen misbehaves. The record stays in the browser tab; nothing of it is sent anywhere by Kokkoja.

Identity and layers

Each participant has a signing key pair (ECDSA on P-256), made in the browser the first time and then kept, by the local program in its configuration directory or by the browser in its own storage. The participant id is the first 16 bytes of the SHA-256 hash of the public key. When a client connects to a session it announces itself with a signed hello carrying its name and public key; the signature covers the room id and the peer number the relay stamped on the message, so nobody can replay it elsewhere or claim another peer's connection. Until a peer has sent a valid hello, everything else from it is ignored.

A page is a stack of layers, one per participant, keyed by participant id. A client accepts a change to a layer only from the participant whose id it is, so the rule "nobody can change anyone else's writing" is not a permission that could be misconfigured but a consequence of how the data is organised. Because the id is derived from the key, and the key is kept, a person is the same participant in every session from the same computer, and their writing on a page loaded weeks later is still theirs.

The host is whoever holds the key whose id is in the invitation. Pages, images and the list of co-hosts are accepted only from the host (or from an allowed guest, for pages and images).

Pages

Page coordinates are in units of 0.1 mm. A blank page is an A4 sheet in landscape, 2970 by 2100 units. "Widen" makes the sheet wider, and "Shrink" makes it the bounding box of everything on the page plus the greater of the two margins, exactly, on every side; both are changes to the page like adding an image, carried in the page descriptor (its left and top edges and its size) with the images' revision raised. Writing within 5 mm of an edge grows the page by 5 mm on that side, counted from the sheet's edge. An image is placed at its natural size, taken from the resolution recorded in the file, or 0.1 mm per pixel when there is none (the nominal 72 and 96 dpi that cameras and screenshots write count as none), centred across the page, 5 mm from the top or from the image above, the sheet widened equally on both sides if it is wider than the page; only one wider than a page can be, 2 m, is scaled down. A page holds at most 12 images and 24 MB of image data. An image file over 2400 pixels on a side or 1.5 MB, or not PNG or JPEG, is redrawn at most 2400 pixels and stored as JPEG before it is shared; smaller PNG and JPEG files are shared byte for byte.

A text block is free text, or a text rectangle with a width and a height, drawn with a red frame on a white ground; the ground is drawn beneath every layer, so that it hides an image and never anyone's writing, and the frame within the owner's layer. A Hide rectangle is a page object like an image, in the page descriptor and the page file, drawn white above every layer with blue corners on screen; made, moved and removed by the host and co-hosts. Both are moved by a hold of the pen and resized from their corners, like images but with their shape free; an image keeps its shape.

A stroke is a list of points with pressure, and the line width is a function of pressure: nominal at a pressure of 0.4, about 2.6 times that at 0.75. Points are sent while the stroke is drawn, so the others see it grow. A text block is sent whole at every keystroke, with its line breaks already laid out by the writer's browser, so that everyone shows the same lines.

Each page has a number, counted from 1 in the session, and a random id. A client that joins late asks the longest-connected member for the layers of absent participants, and receives each present participant's layer from that participant. A client that reconnects after a dropped connection sends its whole layer again, so that nothing drawn meanwhile is lost.

The page file

A notebook is a directory. The page being written is Current.svg in it, rewritten a moment after every change (at most every 400 ms while writing continues, at least every 2 s). A kept page is a copy of Current.svg named YYYY_MM_DD_NN.svg, the day being when the page was last changed and NN a serial number within that day. A copy is made when the user asks (Keep), and by the program whenever Current.svg is about to be given to another page: when the session moves to a new page, when the notebook is changed, and when a session begins and finds a page of another session in the notebook. Nothing is copied if the page is blank or an identical copy already exists.

The file is an SVG document that any viewer displays: the images as embedded <image> elements, each layer as a group with the participant's name and id, each stroke as a path and each text block as text. Beside the shapes, the same elements carry Kokkoja's own data in attributes, the exact points and pressures, the text and its breaks, so that Kokkoja reads the page back without loss, with each layer still belonging to its participant. The session id and host id are recorded too, which is how a reloaded tab knows that Current.svg is its own page to continue from.

With the local program, the notebooks are in Kokkoja in the Documents directory (--notebooks chooses another place), the identity and the browser's token in the configuration directory (~/.config/kokkoja on Linux, ~/Library/Application Support/kokkoja on macOS). In the browser, the notebooks are in the site's IndexedDB under the same names and rules, and the identity in its localStorage; a kept page and an exported picture are offered as downloads, since that is the only way a web page can hand a file to its user.

The local program and its token

The local program listens on 127.0.0.1 only. Any web page open in the browser could nevertheless send requests to localhost:8760, so every request that touches files must carry a secret token, which the program makes once, keeps in its configuration directory, and passes to the browser in the address it opens (#token=..., which the browser keeps in its local storage and never sends to any server). Requests without the token are refused. The program also checks that requests come from its own origin. Outside the notebooks it reads only image files and writes only PNG files, each where the file chooser was pointed.

The web site, and whom you trust

A client served by a web site is a web application like any other: the site operator's code runs in your browser. The end-to-end encryption protects the session from the relay and the network, but a dishonest site could serve a client that gives the key away. Participants who run the local program load the client from their own disk and are not exposed to that; they trust the code they downloaded once. The site sends a Content Security Policy that lets the client load nothing from anywhere else, which protects against the ordinary web attacks but not against the operator. For a lesson this is the same trust one places in any video call service, and considerably less than in one that sees the content; it is stated here so that nobody is surprised.

Running a server

A server is a machine with a public name, TLS and nginx (or any reverse proxy that passes WebSockets). deploy/README.md describes the one Kokkoja comes with: the relay as a systemd service bound to localhost, nginx terminating TLS, serving the client's files at https://<name>/ and forwarding wss://<name>/relay/ to the relay, and a config.json telling the client which relay to use and that notebooks are kept in the browser. A host uses the server with

python3 kokkoja.py --relay wss://<name>/relay

and invitations become links to https://<name>/. One server serves any number of hosts and sessions at once; the relay's work per session is passing a few kilobytes per second. Since the relay cannot read what it passes, a server run by someone else is a matter of availability, not of privacy; the web site is the part that requires trust, as described above. A teacher who wants no dependence on anyone's server needs a machine of their own that is reachable from the Internet, which a computer behind a home router is not without arrangement.

What is not protected yet

  • Only hello is signed. A participant working together with a dishonest relay could forge another participant's strokes. Signing every message, or page descriptors with the host's key, would close this.
  • A participant can announce a page with a higher number than the host's, and the others move to it.
  • The relay sees who talks to whom, when, and how much.

Pen input

The client uses the browser's pointer events, which deliver every sample a pen produces, with pressure, in Firefox and in Chromium-based browsers on Linux and macOS. The browser's console shows what arrives. When a pen has been used, touches are ignored, so a hand resting on a touch screen does not draw; this rule has not yet been tested with a device that has both a pen and a touch sensor. Kokkoja opens none of the browser's system dialogs (the colour palette and, with the local program, the file chooser are its own), because one such dialog crashed Firefox on Linux.

Adjustable choices

Some choices of the interface are named constants at the top of the file that uses them, so that they can be changed in one place, or offered as preferences later: the line widths and the five colours (main.js), the zoom step of the wheel and the zoom per 100 pixels of a pen drag (renderer.js), the dead zone of that drag (pen_input.js), the margin by which the page grows (page_model.js), the tooltip delay (tooltip.js), the hold that moves an image (600 ms, with 6 pixels of slack, pen_input.js), the reach of an image's corner (24 pixels, renderer.js), the smallest an image can be scaled to (1 cm, page_model.js), how often a dragged image's place is sent (40 ms, session.js) and the points of a stroke (25 ms, session.js), the colour and width of a text rectangle's frame (page_file.js) and whether the frame is shown whole or as corners on screen (TEXT_FRAME_STYLE in renderer.js; the file and the export always hold the whole frame).

Development

The code is Python and JavaScript only. The tests run with

.venv/bin/python -m unittest discover tests -v

where tests/test_end_to_end.py drives a headless Firefox through geckodriver with several participants, with and without the local program, and npm run check type-checks the client (the TypeScript compiler is the only development dependency). The relay's --host and --port and the local program's options are listed by --help.