Skip to content

Latest commit

 

History

History
181 lines (137 loc) · 9.1 KB

File metadata and controls

181 lines (137 loc) · 9.1 KB

PRD — Terminal Encrypted Chat (working title: shellchat)

1. Goal

A single-binary, terminal-based chat app in Go. One user (the admin) configures and spawns a server on a chosen port with a passphrase. Anyone with the address + passphrase joins from their own terminal into a shared IRC-style room. All messages on the wire are encrypted with a key derived from the passphrase. Heavy emphasis on a fancy, fully themeable TUI — the admin brands the server (name, logo, colors) and every connected client renders that branding.

Same build style as ZTF666/Vanadinite: Go + Bubble Tea + Lip Gloss.

2. Stack

Concern Library
TUI loop github.com/charmbracelet/bubbletea
Styling github.com/charmbracelet/lipgloss
Components (textinput, viewport, list) github.com/charmbracelet/bubbles
KDF golang.org/x/crypto/argon2
AEAD cipher golang.org/x/crypto/chacha20poly1305 (use XChaCha20-Poly1305)
Config github.com/BurntSushi/toml
Emoji shortcodes github.com/enescakir/emoji
ASCII banner github.com/common-nighthawk/go-figure
Runewidth (alignment) github.com/mattn/go-runewidth

Do not hand-roll any crypto. Use only the vetted primitives above.

3. Architecture

Two modes in one binary:

shellchat serve   # admin: run setup wizard (if no config) then start server
shellchat join    # client: prompt for host:port + passphrase + username, connect
  • Transport: plain TCP. Server net.Listen, goroutine per client, in-memory broadcast hub (channel fan-out to all connected clients).
  • Framing: length-prefixed frames — 4-byte big-endian uint32 length, then payload. Every payload after the handshake is a ciphertext blob.
  • Relay model: server receives an encrypted frame from one client and forwards the same ciphertext to all others. Clients decrypt locally.

3.1 Security model — state this honestly in the README

  • The passphrase-derived key is a shared symmetric key. It protects messages against network eavesdroppers and anyone who lacks the passphrase.
  • It is not true per-peer E2E: the admin (server operator) holds the same key and could decrypt traffic. The relay code forwards ciphertext without decrypting, but the threat model is "confidential against outsiders," not "server can't read." Document this plainly — no false promises.
  • There is no forward secrecy. A leaked passphrase exposes past captured traffic. Acceptable for v1; a future Noise-protocol upgrade path can add it.

4. Handshake & auth

Salt is public and sent in the clear; the passphrase never crosses the wire.

  1. Client opens TCP connection.
  2. Server → client (plaintext frame): server metadata as JSON — { server_name, motd, theme, banner, argon2_salt (base64), challenge (32 random bytes, base64) }. The theme/banner travel here so the client renders the admin's branding.
  3. Client prompts for passphrase (unless supplied via flag), derives key = Argon2id(passphrase, salt, t=3, m=64MB, p=4, keyLen=32).
  4. Client → server (encrypted frame): XChaCha20-Poly1305 seal of { challenge, username }. Knowing the key is the proof of passphrase.
  5. Server opens the frame with its own derived key. If the AEAD tag verifies and the returned challenge matches → authenticated; register client, broadcast a join system-message. Otherwise close the connection (optionally after a fixed delay to blunt guessing).

5. Message protocol (post-handshake)

Every chat frame is an XChaCha20-Poly1305 sealed JSON object:

{ "type": "msg|join|leave|system", "user": "ztf", "body": "hey :tada:", "ts": 1737600000 }
  • Nonce: 24-byte random per message, prepended to the ciphertext (XChaCha's 192-bit nonce makes random generation collision-safe).
  • Server relays type:"msg" ciphertext untouched. Server generates join/leave/system frames itself (sealed with the same key) so all clients render them consistently.
  • Enforce max_message_length (config) client-side before send and server-side on receive.

6. Config system

Single config.toml next to the binary. Two ways to produce it:

  • First-run setup wizard (TUI) when shellchat serve finds no config — walks the admin through every option below, writes the file.
  • Hand-edit for power users.

config.toml shape:

[server]
name        = "ZTF's Deck"
motd        = "welcome to the wire, choom"
port        = 6667
passphrase  = "correct-horse-battery"   # admin-set; used to derive the key at boot
max_message_length = 2000
scrollback  = 500

[display]
timestamps      = true
clock           = "24h"        # "12h" | "24h"
join_leave_msgs = true
username_colors = "hash"       # "hash" | "fixed"
prompt_glyph    = ""

[theme]
preset = "meatspace"           # "meatspace" | "vanadinite" | "custom"
# when preset = "custom", these override:
bg          = "#0a0a0f"
fg          = "#663399"
accent      = "#39ff14"
username    = "#00f5ff"
system_msg  = "#cc99ff"
timestamp   = "#ffcc00"
error       = "#ff2d78"
success     = "#39ff14"
border      = "#663399"
border_style = "rounded"       # rounded | thick | double | normal | hidden

[banner]
mode = "figlet"                # "figlet" (generate from name) | "file"
file = "banner.txt"            # used when mode = "file"; ANSI/ASCII splash

[emoji]
custom_file = "emoji.toml"     # optional custom shortcode → glyph aliases

7. Theming (the bragging-rights layer)

Every color is a Lip Gloss style resolved from [theme]. Ship two built-in presets; custom reads the explicit hex values.

Preset: meatspace (default)

bg #0a0a0f  fg #663399  accent #39ff14  username #00f5ff
system #cc99ff  timestamp #ffcc00  error #ff2d78  border #663399

Preset: vanadinite

accent #cc0000  secondary #ff2b47  (dark bg, red-forward — mirror the Vanadinite TUI palette)
  • border_style maps to Lip Gloss's built-in border sets.
  • username_colors = "hash" → hash each username to a stable color from the palette (classic IRC look); "fixed" → everyone uses theme.username.
  • Server pushes the resolved theme in the handshake so all clients see the admin's branding, not their local defaults.

8. Emoji

  • Shortcodes: run every outgoing/rendered body through the emoji lib so :tada: → 🎉. Restrict to a single-glyph set — strip/skip any multi-codepoint or ZWJ sequences (👨‍👩‍👧, flags, skin-tone joins) to keep terminal column math correct.
  • : autocomplete: when the user types : in the input, show a filtered Bubbles list popup of matching shortcodes; Tab/Enter inserts.
  • Custom aliases: optional emoji.tomlshrug = "¯\\_(ツ)_/¯", ztf = "" (Nerd Font glyphs welcome). Merge over the built-in map at load. Custom entries must also resolve to single-width/single-glyph output.
  • Use go-runewidth everywhere widths matter (alignment, truncation, input cursor).

9. TUI layout

Setup wizard (serve, first run): stepped screens — branding (name/motd/banner) → port → passphrase → theme preset picker (live preview pane) → display toggles → write config → start server.

Chat screen (join & serve host view):

┌─ <banner / server_name> ───────────────────────────┐
│  <scrollable message viewport>                      │
│  12:04 ztf   hey :tada:                             │
│  12:04 » maroua joined                              │
│                                                     │
├─────────────────────────────────────────────────────┤
│ ❯ <textinput>                          [users: 3]   │
└─────────────────────────────────────────────────────┘
  • Bordered layout via Lip Gloss (border_style from theme).
  • Viewport = scrollback (config-capped), timestamps per [display].
  • Input line with prompt glyph + emoji autocomplete popup.
  • Small user-count / connected indicator.
  • Ctrl+C quits (send leave, close cleanly); PgUp/PgDn scroll.

10. Milestones

  1. Config + TOML load/write + setup wizard.
  2. TCP server/hub + client dial, plaintext handshake JSON.
  3. Argon2id KDF + XChaCha20-Poly1305 seal/open + challenge auth.
  4. Encrypted message relay, join/leave/system frames.
  5. Bubble Tea chat UI (viewport + input + borders).
  6. Theming: presets, custom overrides, server-pushed theme.
  7. Emoji: shortcodes, single-glyph filter, : autocomplete, custom aliases.
  8. Banner (figlet + file mode), polish, README with the honest security note.

11. Non-goals (v1)

  • No inline images (Kitty/Sixel) — banner is static ANSI only.
  • No true per-peer E2E / forward secrecy (documented Noise upgrade path later).
  • No multi-room/channels — one room per server.
  • No message persistence — in-memory scrollback only, nothing written to disk.
  • No file transfer, no DMs.

12. Deliverable

Single Go module, go build → one binary. README.md covering install, serve/join usage, config reference, and the honest security model from §3.1.