From 422647e7898b8df67cd11f8fb060aec23dec84aa Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Wed, 14 Oct 2026 22:20:15 +0200 Subject: feat(cli): meshbay-cli, a command-line client for members Signs in to the hub, lists the account's groups and their folders, and downloads and uploads files directly with each group's node, end-to-end encrypted. With no argument it opens a two-pane interactive mode (Textual). Session and device keys are kept encrypted under the system's key storage, or a 0600 key file where there is none; node keys are pinned at first contact; a new machine links as a device. Packaged on its own on Linux (deb and rpm, on meshbay-common's venv, with a man page generated from the parser) and frozen next to meshbay-node in the full Windows installer, not the light one, with a self-test the build runs. client/keys.py ports keyderive.js and is LGPL like it. Co-Authored-By: Claude Opus 5.5 --- docs/MESHBAY_DESIGN.md | 114 +++++++++++++++++++++++++++++++++++++++--------- docs/PACKAGING-GUIDE.md | 17 +++++--- docs/USERGUIDE.md | 39 +++++++++++++++++ 3 files changed, 144 insertions(+), 26 deletions(-) (limited to 'docs') diff --git a/docs/MESHBAY_DESIGN.md b/docs/MESHBAY_DESIGN.md index 2174879..6dd1b2e 100644 --- a/docs/MESHBAY_DESIGN.md +++ b/docs/MESHBAY_DESIGN.md @@ -16,7 +16,7 @@ > them — it names the invariant that holds today, not the incident that produced > it. §13 is the register of those labels. > -> Wire versions at the time of writing: **MNP 6.2** (oldest peer accepted 4.0), +> Wire versions at the time of writing: **MNP 6.3** (oldest peer accepted 4.0), > **MHP 0.1**, packages **0.19.0**. The normative source for the wire format is > `MESHBAY_NODE_PROTOCOL.md`; this document states the design the protocol > serves, not its byte layout. @@ -302,6 +302,7 @@ comes from the hash binding: | Bound | Value | |---|---| | Request TTL | 1 h, `[node] device_request_ttl_minutes` | +| Pending requests per account per node | 10; filing one more drops the oldest | | Devices per account per node | 5 | | Attempts per connection | 5, audited on exhaustion | | Filing a request | requires the account to have at least one pinned identity already | @@ -1105,15 +1106,22 @@ legitimate rotation. > complete a handshake with. It is unverified at that point and is never a > substitute for the ack. -**The pre-proof window is three messages, and that is a bound to defend.** Only -the two bundle fetches and `join_request` are served before the proof, because -each is something a caller needs *in order to* prove possession of the group key: -a returning browser has to recover its identity, and a new member has no key to -prove with. Nothing else qualifies. Device messages are **authenticated-only** — -a device request is countersigned later by a device already pinned, so requiring -the caller to finish its own handshake first costs nothing and keeps the surface -at three. The bundle fetches are additionally bounded (4 per connection) and -audited, because that window is the neighbourhood **C4** and **C5b** came from. +**The pre-proof window is four messages, and that is a bound to defend.** Only +the two bundle fetches, `join_request` and `device_add_request` are served before +the proof, because each is something a caller needs *in order to* prove possession +of the group key: a returning browser has to recover its identity, a new member +has no key to prove with, and a new device of a known member has none either — the +group key is wrapped for an identity the node pinned, and a machine that keeps no +bundle there (the desktop application, the command-line client, a browser with +browser access off) holds no such identity until another device approves it. +Nothing else qualifies. Filing a device request is **inert**: nothing is admitted +until a device already pinned countersigns it, the code is bound to the new keys, +the request is bounded per connection (5) and per account (10 pending), and it is +audited. Every other device message — looking up, approving, listing, revoking, +saying hello — is sent by a device that can finish its own handshake, and is +**authenticated-only**. The bundle fetches are additionally bounded (4 per +connection) and audited, because that window is the neighbourhood **C4** and +**C5b** came from. **Pinning is defence in depth, not the primary control.** A substituted node already fails the key proof. Pinning covers the case where an attacker *holds* the @@ -2610,16 +2618,16 @@ The rules that make this safe: ## 8. Clients -### 8.1 Two clients, deliberately +### 8.1 The clients, deliberately different -| | Hub-served web SPA | Desktop client | -|---|---|---| -| Distribution | served by the hub | installed, signed release | -| Code integrity | **T3 accepted** — the hub can inject | detectable *if* reproducible builds ship | -| Key storage | IndexedDB / sessionStorage, plus a bundle on each node | OS-protected local storage; keys never bundled | -| Transport | WebRTC | WebRTC **+ QUIC** (sidecar, for hub-less `group://`) | -| Downloads | to disk where the engine allows it | native, streamed, unlimited | -| Positioning | **convenience tier** — zero install | recommended for sensitive use | +| | Hub-served web SPA | Desktop client | Command-line client (§8.6) | +|---|---|---|---| +| Distribution | served by the hub | installed, signed release | installed: its own `.deb`/`.rpm`, inside Windows Full and Store | +| Code integrity | **T3 accepted** — the hub can inject | detectable *if* reproducible builds ship | as the desktop client: the hub serves it no code | +| Key storage | IndexedDB / sessionStorage, plus a bundle on each node | OS-protected local storage; keys never bundled | OS key storage, else a 0600 file; opens a node's bundle, never writes one | +| Transport | WebRTC | WebRTC **+ QUIC** (sidecar, for hub-less `group://`) | WebRTC | +| Downloads | to disk where the engine allows it | native, streamed, unlimited | native, resumable, unlimited | +| Positioning | **convenience tier** — zero install | recommended for sensitive use | a member's files from a terminal, scripts and headless machines | **The SPA is not deprecated and stays.** It is the zero-install path, and the objective is explicit: **a native client must not prevent web use.** It must be @@ -2995,6 +3003,67 @@ MSE string) fed to a source buffer, with the node holding one slot per viewer. --- +### 8.6 The command-line client + +`meshbay-cli` (`packages/meshbay-cli`) is a member's files from a terminal: +`login`, `logout`, `groups`, `ls`, `get`, `put`, `trust`, and with no argument +an interactive two-pane view (Textual) over the same library. It is not an +operator tool — a node is administered through `meshbay-node` and its loopback +API (§6.7) — and it does not stream, play or chat: those belong to an engine +with a player and a page. English only. + +**The same protocol as the other clients, end to end.** The hub signs it in, +issues the per-node token (E10) and relays the WebRTC offer and answer; from the +DataChannel on, everything goes to the node and is sealed under the group key +(§5.1, §5.2), and transfers take leases like any client's (§5.5). Nothing +data-related passes through the hub. + +**Identity on a node, in this order:** the identity this machine kept from an +earlier run; the one waiting for a device approval; the bundle the node keeps +for browsers, opened with the passphrase (§3.7) and then kept locally; failing +all three, a new key pair filed as a device request (§3.3) — the code is +printed, an already linked device approves it, and the client joins with its +own keys. It never writes a bundle. That is the desktop client's position: an +installed program, no hub-served code (T3 does not apply), so device linking +is admitted for it as for the application. + +**What it keeps.** `state.json` (hub, account, pinned node keys) and +`secrets.enc` (AES-GCM: the refresh token, the device hub key, the node +identities) in the user's data directory (`$XDG_DATA_HOME/meshbay-cli`, +`%LOCALAPPDATA%\meshbay-cli`). The key that opens `secrets.enc` is in the OS +key storage; where there is none, in a 0600 file, which `login` says once. The +passphrase is never stored: a remembered session renews with the refresh +token, else with the device hub key, and asks for the passphrase only when both +fail. Two runs renewing at once take a file lock, so a rotated refresh token is +never spent twice. + +**Node keys are pinned at first contact.** A node presenting another key is +refused before anything is sent (exit 6) until `meshbay-cli trust reset NODE` +— the deliberate way out, for a rotation the operator announced. + +**Text from the network is data.** Every name a node or a member chose is +printed with control and bidi characters escaped, and drawn as plain text, +never markup, in the interactive view. Local names are made writable on the +machine and kept inside the destination; on Windows a download carries the +Mark of the Web. + +**Remote paths** are `group[@owner]/root/folder/file`: `@owner` only when two +groups share a name, names compared without case, `%2F` and `%25` for a `/` or +a `%` in a name, and a link copied from the application accepted as it is. + +**Concurrency.** At most six WebRTC negotiations in flight, one connection per +group; a group whose node is away is reported without stopping the others +(exit 4). + +**Exit codes** are a contract for scripts: 1 usage, 2 sign-in, 3 not found, +4 partial, 5 node unreachable or waiting for device approval, 6 node key +changed, 7 version, 8 refused before anything was sent, 130 interrupted. + +**Packaging.** Its own `.deb`/`.rpm` (`meshbay-cli`, with the packages only it +uses, on top of `meshbay-common`'s venv) and a man page generated from its +argument parser; on Windows, `meshbay-cli.exe` beside `meshbay-node.exe` in +the Full installer and the Store package, not in Light. + ## 9. Group applications ### 9.1 The plug-in architecture @@ -4487,7 +4556,9 @@ phone's photo backup (§9.12), the operator CLI and loopback control API, the desktop client through its identity and download stages, account recovery, the Windows port through packaging, and the Android client — the shell, native keys, downloads and uploads, casting, -and notifications while it is closed, fetched or pushed (§11.3). +and notifications while it is closed, fetched or pushed (§11.3) — and the +command-line client: groups, listing, downloads and uploads with resume, +device linking, and an interactive two-pane view (§8.6). The packages install: a machine has been taken from the built artefacts to a running hub and node on **Ubuntu 26.04 (`.deb`), Fedora 44 (`.rpm`) and @@ -4518,6 +4589,7 @@ process runs it — `systemctl --user` on Linux, Task Scheduler on Windows. | — | **HEIC/HEIF in Photos.** The indexer does not classify `.heic`/`.heif` as images and no browser but Safari draws them, so a phone that shoots HEIC has its backed-up photos stored and listed in Files but absent from Photos. Needs `pillow-heif` in every package and a JPEG rendition for the viewer (§9.12) | | — | **Photo backup with the application closed**, and **"remove what this phone sent"** (§9.12). A closed application has no page and so no transport; it runs at the next opening | | — | **Android: phone behaviour and release** — the back button driving the page, recovery from a network handover, keeping a download alive with the screen off, lock-screen media controls, and an update channel (§11.3) | +| — | **Command-line client: cross-group search and chat** (§8.6). Not run on Windows yet; its transfer windows are not yet measured against a node on another machine | | — | **Federation between two hubs.** The protocol is written and switched off in the code (§7.6); what is not built is one run between two machines | ### 15.3 Open, and why each is where it is @@ -4592,7 +4664,7 @@ superseded document kept under `docs/`, a note somebody holds elsewhere. | `draft-v5 §6.2`–`§6.4` — node registration, signaling, client addresses | §7.2 | | `draft-v5 §7` — cryptography | §4.1–§4.4, §4.6 | | `draft-v5 §7.1` — the keypair bundle and C4 | §3.7 | -| `draft-v5 §8.1`, `§8.2`, `desktop-client-v1.md §2` — the two clients, the shell | §8.1, §8.2 | +| `draft-v5 §8.1`, `§8.2`, `desktop-client-v1.md §2` — the clients, the shell | §8.1, §8.2 | | `draft-v5 §9`, `draft-v6 §5` — open items | §15.3 | | `draft-v5 §10` — testing posture | §12 | | `draft-v6 §2.2`, `desktop-client-v1.md §4`, `§4.5`, `§4.6` — device linking | §3.3 | diff --git a/docs/PACKAGING-GUIDE.md b/docs/PACKAGING-GUIDE.md index 5fbf7a7..e79fcd6 100644 --- a/docs/PACKAGING-GUIDE.md +++ b/docs/PACKAGING-GUIDE.md @@ -1,16 +1,19 @@ # MeshBay — Installation Guide -Four packages, all installed under `/opt/`: +Five packages, all installed under `/opt/`: | Package | What it does | |---|---| | `meshbay-common` | Shared Python venv with all dependencies | | `meshbay-hub` | Identity authority and group registry (server) | | `meshbay-node` | Local file host, streaming, chat daemon | +| `meshbay-cli` | Command-line client: your groups' files from a terminal (`man meshbay-cli`) | | `meshbay-client` | Desktop app (Electron) | Pick what you need: a desktop user installs **common + node + client**. -A server running the hub installs **common + hub**. +A server running the hub installs **common + hub**. **common + cli** gives a +machine — a server, one with no desktop — a member's access to their groups' +files from a terminal. --- @@ -23,6 +26,7 @@ sudo dpkg -i meshbay-common_0.9.0_amd64.deb sudo dpkg -i meshbay-node_0.9.0_amd64.deb # desktop machine sudo dpkg -i meshbay-hub_0.9.0_amd64.deb # server only sudo dpkg -i meshbay-client_0.9.0_amd64.deb # desktop machine +sudo dpkg -i meshbay-cli_0.9.0_amd64.deb # optional: the command-line client ``` If dpkg complains about missing dependencies: @@ -34,7 +38,7 @@ sudo apt-get install -f ### Uninstall ```bash -sudo dpkg --remove meshbay-client meshbay-hub meshbay-node meshbay-common +sudo dpkg --remove meshbay-client meshbay-cli meshbay-hub meshbay-node meshbay-common sudo rm -rf /opt/meshbay-* ``` @@ -49,12 +53,13 @@ sudo rpm -ivh meshbay-common-0.9.0-1.fc44.x86_64.rpm sudo rpm -ivh meshbay-node-0.9.0-1.fc44.noarch.rpm # desktop machine sudo rpm -ivh meshbay-hub-0.9.0-1.fc44.noarch.rpm # server only sudo rpm -ivh meshbay-client-0.9.0-1.fc44.x86_64.rpm # desktop machine +sudo rpm -ivh meshbay-cli-0.9.0-1.fc44.x86_64.rpm # optional: the command-line client ``` ### Uninstall ```bash -sudo rpm -e meshbay-client meshbay-hub meshbay-node meshbay-common +sudo rpm -e meshbay-client meshbay-cli meshbay-hub meshbay-node meshbay-common ``` --- @@ -69,7 +74,9 @@ node (with `meshbay-common` inside it). There is no Windows hub. Run the installer. It is **per-user** and lands in `%LOCALAPPDATA%\Programs\MeshBay\` without needing admin rights. The node daemon ships beside the app at `resources\node-runtime\meshbay-node.exe`; -the client finds it automatically. +the client finds it automatically. The command-line client, `meshbay-cli.exe`, +is beside it, and works in any new terminal (the light installer does not +carry it). It asks one thing — **when the node runs**: diff --git a/docs/USERGUIDE.md b/docs/USERGUIDE.md index bab2074..1c21c80 100644 --- a/docs/USERGUIDE.md +++ b/docs/USERGUIDE.md @@ -224,6 +224,43 @@ points the node at another account. A folder you pick in the folder dialog is no twice. That window belongs to the application, so nothing displayed in the page — which shows content from other people's nodes — can answer it for you. +### The command-line client + +`meshbay-cli` reaches your groups' files from a terminal — a server, a script, a +machine with no desktop. It is a package of its own on Linux (`meshbay-cli`, +with a manual page: `man meshbay-cli`), and comes with the desktop +application's full installer on Windows (not with the light one). + +```bash +meshbay-cli login -u alice # asks for the passphrase +meshbay-cli groups # your groups, their node, your role +meshbay-cli ls -l Family/photos # a group, a folder of it +meshbay-cli get -R Family/photos/2024 # download, here; -o elsewhere +meshbay-cli put -R ~/scans Family/documents +meshbay-cli logout +``` + +A group is named as the GROUP column of `meshbay-cli groups` prints it; add +`@owner` only when two of your groups share a name. Quote wildcards +(`'Family/music/*.flac'`): they are matched against the group, not against +your disk. An interrupted download resumes where it stopped the next time you +run the same `get`. Run `meshbay-cli` with nothing after it for a two-pane +view: this computer on the left, your groups on the right, F5 to copy the +selection across. + +`login` remembers the session, so later runs need no passphrase; `logout` +forgets it and everything else stored on this computer. What is kept is +encrypted, under a key held by the system's key storage — or, on a machine +that has none (a server with no desktop session), in a file only your user +can read, which `login` tells you. + +The first time it opens a group, the client may print a linking code instead +of the files: the node holds no copy of your keys (Browser access is off), so +this computer asks to be added as one of your devices. Enter the code in the +application under *Your devices on this node*; the client goes on by itself. + +Managing a node is not the client's job: that is `meshbay-node` (§6). + --- ## 4. Joining a group @@ -1198,6 +1235,8 @@ Better to know now than to go looking for it: phones take JPEG unless set otherwise. - **Hubs do not talk to each other yet.** Everyone in a group needs an account on the same hub. +- **The command-line client lists, downloads and uploads.** Searching across + groups and chat are not in it yet. - **Casting reaches Chromecast devices** from the desktop and Android applications, for films, photos and music. Support for other TV protocols is designed but not built. -- cgit v1.2.3