KDX
Handoff · v0.1

KDX Client & Server

A from-scratch Rust rebuild of KDX — an early-2000s Hotline-style chat and file-sharing system. A custom binary protocol over TLS 1.3, a challenge-response login where the password never leaves the machine, chat rooms, a virtual file tree with drop boxes, and resumable chunked transfers both directions. The client is a Tauri desktop app in the classic black / blood-red / phosphor-green aesthetic.

Crates
6 + 1 app
Tests
107 green
Commands
11
Events
11
Windows
4

This document is the reference for what the interface does and how each feature works underneath. Read the interface sections to operate the app; read "Under the hood" to understand or extend the machinery each window drives.

Two halves, one protocol The client links the same kdx-protocol and kdx-crypto crates the server uses, so the wire format has a single implementation shared by both sides — no drift between what the server speaks and what the client expects.
Orientation

System map

Every user action follows one path: the webview calls a Tauri command, the command forwards to the async client library, which frames the request and writes it to the TLS socket. Server pushes travel back the same channels as events.

request path →
Webview UIdist/ · vanilla JS
→
Tauri shellcommands.rs
→
client-coreconnection actor
→
kdxdserver daemon
← event / push path
Crate / dirRole
kdx-protocolWire format: 20-byte framed packets, codec, fragmentation, typed messages. Shared by client and server.
kdx-cryptoArgon2id password hashing + challenge-response helpers.
kdx-server-coreConnection state machine, auth, chat rooms, file tree, transfers, tracker.
kdx-storagesqlx / SQLite persistence (accounts, file tree, resume state).
kdx-client-coreAsync client library, zero UI deps. TOFU trust, transfer engines, event stream.
kdxdThe server daemon binary (+ useradd subcommand).
apps/kdx-clientTauri v2 desktop app: a thin Rust shell + the webview frontend.
The interface

The desktop

The client is a single OS window hosting a desktop metaphor: four draggable, focusable panels rendered in the webview, not separate OS windows. A menu bar across the top focuses (and un-hides) each panel; a footer shows connection state. Click a title bar to drag; click anywhere in a panel to bring it forward.

Menu bar

  • File / Chat / Transfers — focus that window.
  • Status chip (right) — offline connecting online, showing kdx://host:port once connected.

The four windows

  • Connect — server address, login, TOFU trust prompt.
  • Chat — the lobby: messages, users, topic.
  • Files — browse, upload, download.
  • Transfers — live per-chunk progress.
The interface

Connect window

The entry point. Enter server host, port, and account, then Authenticate. The window remembers your last host / port / username via local storage, and stays available after login so it doubles as the reconnect surface.

CONNECT
SERVER
127.0.0.1
PORT
10700
LOGIN
phraq
PASSWORD
••••••••
AUTHENTICATE

The authenticate flow

  • 1 · connect — TCP + TLS + KDX version handshake.
  • 2 · trust check — first contact triggers the TOFU prompt (below).
  • 3 · login — challenge-response; password never sent.
  • 4 · join lobby — you land in #lobby and the Chat window focuses.

Status appears inline below the button: logged in as phraq (power user) or a red error line.

Trust-on-first-use prompt

KDX pins server certificates SSH-style. The first time you reach a server, or any time its key changes, the connect step is refused and an inline prompt shows the certificate fingerprint for you to verify out-of-band before trusting it.

Unrecognized server First contact. The prompt shows the sha256:… fingerprint. Trust & connect pins it and reconnects. Expected against the self-signed dev server — its key regenerates each run, so you'll re-prompt on every restart.
⚠ Server key changed A different key than the one previously pinned. Could be a reinstall — or an impostor. The prompt shows both the new and previous fingerprints and warns in red before you can trust it.
The interface

Chat window

A topic bar, a scrolling message stream, a live user list, and an input line. The title-bar tag shows the current member count.

Message line types

21:58*** phraq joined
21:59<acidburn> anyone seed the 1620 build?
22:00*** zerocool waves
! flood protection: slow down
! not in that room
  • normal — <sender> text, phosphor green, sender highlighted.
  • action — /me messages, italic, *** sender text.
  • system — join/leave and server notices, dim italic (clients can't forge these).
  • warning — flood control and advisories, amber.
  • error — server error frames, red.

Input commands

You typeEffectCommand
hello thereSend a normal message to the room.send_chat(flags 0)
/me wavesAction message.send_chat(flags 1)
/topic digital freedomSet the room topic (needs privilege).set_topic
The interface

Files window

A path bar with Up / Refresh / Upload controls over a directory listing. Each row shows a kind tag and — for files — a size. Clicking a folder navigates in; clicking a file opens a native save dialog and downloads it. Uploading opens a native file picker and sends into the current directory.

FILES /pub
[DIR] docs
[DROP] incoming write-only
[UP] uploads
[FILE] kdx-1620.zip 409 KB

Node kinds

  • [DIR] — a folder; click to enter.
  • [FILE] — click to download to a chosen path.
  • [DROP] — a drop box, shown in amber. You may upload into it but never list or read it — enforced server-side for every class, admins included.
  • [UP] — an upload folder.
Live refresh When a transfer completes, the Files window refreshes automatically so a just-uploaded file appears without a manual reload.
The interface

Transfers window

One card per active transfer, each with a live chunk-bitmap grid: every cell is one chunk, lighting up phosphor-green as it is sent (upload) or received and hash-verified (download). The card shows direction (↑/↓), percent, and done/total chunks; on completion it flips to verified ✓ or a failure message.

↑ kdx-1620.zip72%
69/96 chunksupload

The bitmap is driven directly by the transfer_progress event's bitmap field, so what you see is the actual resume state — the same bytes persisted for resuming an interrupted transfer.

The bridge

Command reference

The webview calls these via __TAURI__.core.invoke. Each is a thin async forward into kdx-client-core; none holds business logic. Errors return a tagged object — kind: "untrusted_certificate" (with host, port, fingerprint, previous) drives the TOFU prompt, everything else is kind: "message".

CommandArgumentsReturnsEffect
connecthost, port"connected"TCP + TLS + KDX handshake; spawns the event forwarder. May return an untrusted-cert error.
trusthost, port, fingerprint—Pin a fingerprint in known_hosts.toml so the next connect trusts it.
loginusername, passwordclass: u8Challenge-response login; returns the account's base class (0–3).
join_roomroom—Join (or create, with privilege) a room.
leave_roomroom—Leave a room.
send_chatroom, flags, text—Send a message; flags bit 0 = action (/me).
set_topicroom, topic—Set a room topic (needs CHAT_SET_TOPIC).
list_filespath{path, entries[]}List a directory; entries carry name, kind, size.
uploadlocal, remote_dir—Upload a local file; resolves when verified. Progress via events.
downloadremote_path, local—Download to a local path, resuming from a sidecar if present.
disconnect——Close the connection.
The bridge

Event stream

The server's pushes and transfer progress arrive on one channel. kdx-client-core emits a typed Event; the Tauri shell forwards each as a "kdx" webview event whose payload is tagged by type (snake_case). The UI switches on that tag.

typePayloadWhat the UI does
connectedfingerprintInformational; connection is considered up after login succeeds.
authenticatedclassReserved — login() already returns the class directly.
chatroom, sender, timestamp, flags, textRender a chat line (normal / action / system by flags).
user_listroom, users[]Repaint the user list; update the member-count tag.
topicroom, topicUpdate the topic bar.
transfer_progressid, direction, done, total, bitmapUpdate / create the transfer card and its chunk-bitmap grid.
transfer_completeid, direction, status, messageMark the card done (status 0 = verified); refresh Files.
server_warningtextAmber chat line (e.g. flood control).
server_errortextRed chat line.
server_infotextSystem-styled chat line.
disconnectedreasonDrop to offline, tag chat "offline", focus Connect for reconnection.
Under the hood

Protocol & framing

Every message is a length-framed packet. The header is a fixed 20 bytes, all multi-byte fields big-endian:

// 20-byte header
magic       4  b"KDX\0"
version     1
packet_type 1  handshake / auth / file / chat / system ranges
flags       2  bitfield incl. fragmented, more-fragments
sequence    4  per-connection monotonic
length      4  payload bytes (u32)
reserved    2
checksum    2  CRC32C-folded — framing guard, not security
  • Fragmentation — payloads over a 16 KB frame cap (a file chunk, say) split across frames flagged FRAGMENTED / MORE_FRAGMENTS and reassemble on the far side, guarded by a size cap and timeout.
  • Integrity comes from TLS and per-chunk SHA-256 — the header checksum only catches framing corruption.
Under the hood

Auth & trust

Challenge-response login

The server stores only an Argon2id hash. To log in, the client receives a one-time random challenge plus the account's salt and cost parameters, recomputes the Argon2 verifier locally, and answers with SHA-256(challenge ‖ verifier). The server compares in constant time. The password never crosses the wire — not even inside TLS — and a captured answer is useless because each login gets a fresh challenge. Unknown usernames still get a plausible decoy challenge, so the wire can't distinguish "no such user" from "wrong password."

Trust-on-first-use

The client pins the SHA-256 of the server's certificate per host:port in known_hosts.toml. An unknown or changed key fails the TLS handshake; the library surfaces the observed fingerprint (and any previous one) so the UI can prompt. This is the desktop-app answer to self-signed and rotating server certs without a CA.

Under the hood

Chat & user classes

Each chat room is its own actor task owning its membership, topic, and per-member flood gate — broadcasting a message is N channel sends with no shared lock on the hot path. A token-bucket flood gate (burst 5, refill 1/s) throttles a spammer with a server_warning. Clients cannot forge system-flagged messages.

Classes & privileges

Four base classes with strictly-increasing privilege sets; per-account overrides can grant beyond or revoke from the class.

ClassGains (on top of the one below)
guestCHAT_SEND, FILE_LIST
userCHAT_PRIVATE, FILE_DOWNLOAD, FILE_UPLOAD
powerCHAT_CREATE_ROOM, CHAT_SET_TOPIC, FILE_DELETE, FILE_MANAGE_TREE
admineverything (USER_KICK/BAN/ADMIN, SERVER_ADMIN)
Under the hood

Files & ACLs

The file tree is loaded from SQLite into memory (read-heavy, write-rare) and written through on mutation. Every folder carries a minimum-class-to-read and minimum-class-to-write. File bytes live on the filesystem, never in the database.

  • Drop boxes reject listing and reading structurally — the check sits in the ACL layer and fires for every caller regardless of class, so write-only is a property of the node, not a convention.
  • Downloads resolve the file through the same read-ACL path, so a file inside a drop box can be uploaded but never downloaded.
Under the hood

Transfers & resume

Files move in 32 KB chunks, each carrying its own SHA-256; the whole file is SHA-256-verified before it's accepted. Both directions are throttled by a token bucket and support resume — the difference is who remembers what:

Upload ↑

The server holds the resume state: a received-chunk bitmap in the transfer_state table, updated after every chunk-ack. A reconnecting client is told which chunks the server already has and sends only the rest. A crash loses at most one chunk.

Download ↓

The client holds the resume state: a .kdxpart sidecar next to the partial file records the bitmap. On resume the client sends its bitmap and the server streams only the missing chunks; the whole-file hash is checked before .part is renamed to the final name.

The chunk grid in the Transfers window is a direct view of this bitmap — the visualization and the resume mechanism are the same bytes.

Operating it

Running it

Server

# create an account: guest | user | power | admin
cargo run -p kdxd -- useradd phraq s3cret power

# start it (defaults to 0.0.0.0:10700, self-signed dev cert)
cargo run -p kdxd

Client

cargo run -p kdx-client

The window opens; enter host 127.0.0.1, port 10700, your account, and Authenticate. Accept the TOFU fingerprint on first contact. No Node toolchain is needed — the bundled frontend is served directly.

macOS packaging cargo install tauri-cli --version '^2', then in apps/kdx-client/src-tauri run cargo tauri icon icons/icon.png and cargo tauri build for a signed .app + .dmg. A placeholder icon ships so cargo run works out of the box.
Operating it

Verification

The whole stack runs green under cargo test --workspace — 107 tests, no external services. The client library is tested headlessly against a real in-process kdxd (real TLS over 127.0.0.1), and the server's own tests are an independent second implementation of the wire flows.

  • Protocol — codec round-trips, checksum corruption, fragment reassembly.
  • Crypto — Argon2 verify, challenge round-trip, replay rejection.
  • End-to-end — login, chat both directions, uploads/downloads with kill-and-resume in both directions, DropBox refusal, throttle pacing.
  • Cross-process smoke — cargo run -p kdx-client-core --example smoke drives a separately-running kdxd: TOFU-pin → login → join → chat → list, over a real socket.
Operating it

Gaps & next steps

Tracker window The tracker (a directory of KDX servers) exists in kdx-server-core as an in-process registry but has no client-facing wire protocol yet — so there is no Tracker window. Exposing it is its own protocol milestone (new packet types + client method), on the model of the download work.
Account admin Accounts are created only via the kdxd useradd CLI. No in-app admin UI for creating / editing accounts or setting privilege overrides.
  • Private messaging — the CHAT_PRIVATE privilege exists but direct messages aren't wired end to end yet.
  • Multi-room UI — the client fixes on #lobby; the protocol supports many rooms, so a room switcher is a UI-only addition.
  • Postgres — persistence is SQLite; a Postgres swap is confined to kdx-storage.
  • GUI manual smoke — the desktop window itself is verified by hand (cargo run -p kdx-client); every layer it wraps is covered by automated tests.