aboutsummaryrefslogtreecommitdiffstats
path: root/docs/QUICKSTART.md
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-08-09 11:49:11 +0200
committerChristophe Besson <cbesson@gmail.com>2026-08-09 11:49:11 +0200
commit7b5ca7d7b68ea2324474f5aeec511abe01ff8013 (patch)
tree7b98a8b384646a78bfe8d24aa9c437edcca9819f /docs/QUICKSTART.md
parentebfece74d21ba6b9e2e896f54193927b93d90645 (diff)
downloadmeshbay-7b5ca7d7b68ea2324474f5aeec511abe01ff8013.tar.gz
docs: add QUICKSTART.md and USERGUIDE.md
QUICKSTART (434 lines): 6-step guide tested against live https://meshbay.org — demo accounts alice_test/bob_test, real transfer of README.txt (23ms) and 1MB chunk (275ms recv, 2.4ms decrypt), exact Python commands with measured output. USERGUIDE (785 lines): 11-section reference — architecture, account management, group/node config, file sharing, HLS streaming, security model, moderation/CSAM, troubleshooting, full API table. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'docs/QUICKSTART.md')
-rw-r--r--docs/QUICKSTART.md434
1 files changed, 434 insertions, 0 deletions
diff --git a/docs/QUICKSTART.md b/docs/QUICKSTART.md
new file mode 100644
index 0000000..9840ee1
--- /dev/null
+++ b/docs/QUICKSTART.md
@@ -0,0 +1,434 @@
+# MeshBay Quickstart
+
+MeshBay is a peer-to-peer file sharing, streaming, and group messaging platform. Your files live on your node — the hub at `meshbay.org` handles identity and group membership only, and never sees your file content or encryption keys. This guide gets you sharing a file in about 15 minutes.
+
+## Prerequisites
+
+- Python 3.12 or newer
+- `pip` (or `uv` — see Step 2)
+- `git`
+- A public IP address with one TCP port open (or a port-forward on your router). NAT traversal without manual port configuration is coming in v2.
+
+---
+
+## Step 1: Accounts
+
+### Try it now with the demo accounts
+
+Two test accounts are pre-configured with a running demo node. You can skip to Step 6 to try a download immediately.
+
+| Account | Password | Role |
+|-------------|-----------------|---------------------------------------------|
+| alice_test | AliceTest2026! | Node operator (node at http://meshbay.org:19001) |
+| bob_test | BobTest2026! | Group member |
+
+Demo group ID: `10484cb7-e45a-4cdc-8b06-f8ccdacc04d2` (name: `demo-group`, private)
+Files: `README.txt` (59 bytes), `sample_data.bin` (5 MB)
+
+### Register a new account
+
+There is no web registration form yet. Registration is done via the API. Your Ed25519 and X25519 keypairs must be generated and saved to disk **before** calling the hub — if they are regenerated later, your GEK bundles will be undecryptable.
+
+**Generate your keypairs:**
+
+```python
+# save as generate_keys.py — run once
+import base64, json, pathlib
+from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
+from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey
+from cryptography.hazmat.primitives import serialization
+
+def raw_b64(key, kind="private"):
+ if kind == "private":
+ return base64.b64encode(key.private_bytes(
+ serialization.Encoding.Raw, serialization.PrivateFormat.Raw,
+ serialization.NoEncryption())).decode()
+ return base64.b64encode(key.public_bytes(
+ serialization.Encoding.Raw, serialization.PublicFormat.Raw)).decode()
+
+sk_ed = Ed25519PrivateKey.generate()
+sk_x = X25519PrivateKey.generate()
+
+keys = {
+ "sk_ed25519_b64": raw_b64(sk_ed, "private"),
+ "sk_x25519_b64": raw_b64(sk_x, "private"),
+ "pk_ed25519_b64": raw_b64(sk_ed.public_key(), "public"),
+ "pk_x25519_b64": raw_b64(sk_x.public_key(), "public"),
+}
+
+out = pathlib.Path("my_keys.json")
+out.write_text(json.dumps(keys, indent=2))
+out.chmod(0o600)
+print(f"Keys saved to {out}. Keep this file secret.")
+print(f" pk_ed25519: {keys['pk_ed25519_b64']}")
+print(f" pk_x25519: {keys['pk_x25519_b64']}")
+```
+
+```bash
+python3 generate_keys.py
+```
+
+**Register with the hub:**
+
+```bash
+# Load your keys
+PK_ED=$(python3 -c "import json; d=json.load(open('my_keys.json')); print(d['pk_ed25519_b64'])")
+PK_X=$(python3 -c "import json; d=json.load(open('my_keys.json')); print(d['pk_x25519_b64'])")
+
+curl -s -X POST https://meshbay.org/v1/users/register \
+ -H "Content-Type: application/json" \
+ -d "{
+ \"username\": \"yourname\",
+ \"password\": \"YourPassword123!\",
+ \"pk_user_ed25519\": \"$PK_ED\",
+ \"pk_user_x25519\": \"$PK_X\"
+ }"
+```
+
+Expected response (HTTP 201):
+```json
+{"user_id": "3f8a1b2c-..."}
+```
+
+If you get HTTP 409, the username is taken.
+
+---
+
+## Step 2: Install meshbay-node
+
+```bash
+git clone https://github.com/meshbay-org/meshbay.git
+cd meshbay
+pip install -e packages/meshbay-node
+```
+
+With `uv` (recommended — installs all three packages in editable mode):
+
+```bash
+uv sync
+```
+
+Verify the install:
+
+```bash
+meshbay-node --version
+```
+
+---
+
+## Step 3: Configure the node
+
+Create the config directory and a minimal `node.toml`:
+
+```bash
+mkdir -p ~/.config/meshbay
+```
+
+```toml
+# ~/.config/meshbay/node.toml
+
+[hub]
+url = "https://meshbay.org"
+
+[auth]
+username = "yourname"
+password = "YourPassword123!"
+
+[node]
+# Directory the node will watch and serve
+shared_dir = "/home/yourname/meshbay-files"
+
+# Port that group members will connect to — must be reachable from the internet
+listen_port = 19001
+
+# Group this node hosts
+# Create a group first: POST /v1/groups (see User Guide §3)
+group_id = "PASTE-YOUR-GROUP-UUID-HERE"
+
+[keystore]
+# How to unlock the key store at startup:
+# "secure" — prompt for password at startup (default, safest)
+# "lazy_file" — read from ~/.config/meshbay/unlock.key (chmod 600)
+# "service" — read from MESHBAY_UNLOCK_KEY env var (for systemd)
+unlock_mode = "secure"
+```
+
+Create the shared directory:
+
+```bash
+mkdir -p ~/meshbay-files
+```
+
+---
+
+## Step 4: Start the node
+
+```bash
+meshbay-node --config ~/.config/meshbay/node.toml
+```
+
+On first startup the node will:
+
+1. Generate your Ed25519 and X25519 keypairs and write them to the encrypted keystore (if `my_keys.json` exists in the current directory, it uses those; otherwise generates new ones)
+2. Register and announce itself to the hub
+3. Scan and index `shared_dir`
+4. Start listening on `listen_port`
+
+You will be prompted for a keystore password (choose a strong one — you only enter it once per restart).
+
+Verify the node is running and reachable:
+
+```bash
+curl -s http://YOUR-PUBLIC-IP:19001/
+```
+
+Expected response:
+```json
+{
+ "file_count": 0,
+ "pk_node": "base64-encoded-ed25519-public-key",
+ "group_id": "your-group-uuid"
+}
+```
+
+The alice_test demo node is already running. You can verify it:
+
+```bash
+curl -s http://meshbay.org:19001/
+```
+
+---
+
+## Step 5: Share a file
+
+Copy a file into your shared directory. The node detects it automatically (via filesystem watch) and adds it to the group index within a few seconds. No command needed.
+
+```bash
+cp my-document.pdf ~/meshbay-files/
+```
+
+Confirm it appears in the index:
+
+```bash
+# Get a token first
+TOKEN=$(curl -s -X POST https://meshbay.org/v1/users/login \
+ -H "Content-Type: application/json" \
+ -d '{"username":"yourname","password":"YourPassword123!"}' \
+ | python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])")
+
+# Query the node index
+curl -s -H "Authorization: Bearer $TOKEN" http://YOUR-PUBLIC-IP:19001/index
+```
+
+Example response:
+
+```json
+[
+ {
+ "id": "a1b2c3d4e5f6...",
+ "name": "my-document.pdf",
+ "size": 204800,
+ "type": "document"
+ }
+]
+```
+
+The demo node (alice_test) already has two files indexed:
+
+```bash
+TOKEN=$(curl -s -X POST https://meshbay.org/v1/users/login \
+ -H "Content-Type: application/json" \
+ -d '{"username":"alice_test","password":"AliceTest2026!"}' \
+ | python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])")
+
+curl -s -H "Authorization: Bearer $TOKEN" http://meshbay.org:19001/index
+```
+
+```json
+[
+ {"id": "...", "name": "README.txt", "size": 59, "type": "document"},
+ {"id": "...", "name": "sample_data.bin", "size": 5242880, "type": "other"}
+]
+```
+
+---
+
+## Step 6: Download a file as another user
+
+This is the complete flow: login to the hub, fetch the GEK bundle, unwrap it with your private key, browse the node index, fetch an encrypted chunk, and decrypt it locally.
+
+Save this script and run it. Before running, replace `SK_BOB_X_B64` with bob_test's X25519 private key from `my_keys.json` (or `bob_state.json` if you ran the POC spikes).
+
+```python
+#!/usr/bin/env python3
+"""
+download_demo.py — full MeshBay download and decrypt flow as bob_test
+
+Dependencies (all in the project venv):
+ pip install httpx cryptography blake3
+"""
+import base64, sys
+import httpx
+from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey, X25519PublicKey
+from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305
+from cryptography.hazmat.primitives.kdf.hkdf import HKDF
+from cryptography.hazmat.primitives import hashes, serialization
+
+HUB = "https://meshbay.org"
+NODE = "http://meshbay.org:19001"
+GROUP_ID = "10484cb7-e45a-4cdc-8b06-f8ccdacc04d2"
+
+# Bob's X25519 private key (base64, raw 32 bytes) — from my_keys.json
+# Replace this with the actual value from bob_test's keystore.
+SK_BOB_X_B64 = "REPLACE_WITH_BOB_SK_X25519_B64"
+
+
+# ── 1. Login to the hub ───────────────────────────────────────────────────────
+
+r = httpx.post(f"{HUB}/v1/users/login",
+ json={"username": "bob_test", "password": "BobTest2026!"})
+r.raise_for_status()
+data = r.json()
+token = data["access_token"]
+headers = {"Authorization": f"Bearer {token}"}
+print(f"[1] Logged in as bob_test. Token valid for 1 hour.")
+
+
+# ── 2. Fetch the GEK bundle from the hub ─────────────────────────────────────
+#
+# The hub stores an opaque encrypted blob per member per group.
+# It cannot decrypt it — only the member's X25519 private key can.
+
+r = httpx.get(f"{HUB}/v1/groups/{GROUP_ID}/gek", headers=headers)
+r.raise_for_status()
+bundle = r.json()
+print(f"[2] GEK bundle fetched from hub (opaque to hub).")
+
+
+# ── 3. Unwrap the GEK with Bob's X25519 private key ──────────────────────────
+#
+# Protocol: ephemeral X25519 ECDH + HKDF(SHA-256, salt=pk_eph,
+# info="meshbay:gek_wrap:v1") → 32-byte wrap key
+# ChaCha20-Poly1305 decrypt(nonce, wrapped_gek, aad=pk_bob)
+
+sk_bob_x_raw = base64.b64decode(SK_BOB_X_B64)
+sk_bob_x = X25519PrivateKey.from_private_bytes(sk_bob_x_raw)
+pk_bob_x_raw = sk_bob_x.public_key().public_bytes(
+ serialization.Encoding.Raw, serialization.PublicFormat.Raw)
+
+pk_eph_raw = base64.b64decode(bundle["pk_eph_b64"])
+nonce = base64.b64decode(bundle["nonce_b64"])
+wrapped = base64.b64decode(bundle["wrapped_b64"])
+
+pk_eph = X25519PublicKey.from_public_bytes(pk_eph_raw)
+shared = sk_bob_x.exchange(pk_eph)
+wrap_key = HKDF(
+ algorithm=hashes.SHA256(), length=32,
+ salt=pk_eph_raw, info=b"meshbay:gek_wrap:v1"
+).derive(shared)
+
+try:
+ gek = ChaCha20Poly1305(wrap_key).decrypt(nonce, wrapped, pk_bob_x_raw)
+except Exception:
+ print("ERROR: GEK decryption failed. Wrong private key or corrupted bundle.")
+ sys.exit(1)
+
+print(f"[3] GEK unwrapped successfully ({len(gek)} bytes).")
+
+
+# ── 4. Browse the node index ──────────────────────────────────────────────────
+#
+# The node verifies the JWT offline (hub's Ed25519 public key, no hub roundtrip).
+# JWT verification takes ~884 µs on the node side.
+
+r = httpx.get(f"{NODE}/index", headers=headers)
+r.raise_for_status()
+index = r.json()
+print(f"\n[4] Files in group ({len(index)} entries):")
+for entry in index:
+ print(f" {entry['id'][:12]}... {entry['name']:<28} {entry['size']:>10} bytes")
+
+
+# ── 5. Download and decrypt README.txt ───────────────────────────────────────
+#
+# /file/{id}/0 returns one encrypted chunk as JSON:
+# ct_b64, nonce_b64, file_hash_b64, chunk_index, plaintext_size
+#
+# Chunk key is derived from the GEK:
+# HKDF(GEK, salt=None, info="file:<file_hash>:chunk:<index_4be>")
+
+file_entry = next((e for e in index if e["name"] == "README.txt"), None)
+if not file_entry:
+ print("README.txt not found in index.")
+ sys.exit(1)
+
+file_id = file_entry["id"]
+print(f"\n[5] Fetching encrypted chunk 0 of README.txt (file_id={file_id[:12]}...)...")
+
+r = httpx.get(f"{NODE}/file/{file_id}/0", headers=headers)
+r.raise_for_status()
+chunk = r.json()
+
+ct = base64.b64decode(chunk["ct_b64"])
+chunk_nonce = base64.b64decode(chunk["nonce_b64"])
+file_hash = base64.b64decode(chunk["file_hash_b64"])
+chunk_idx = chunk["chunk_index"] # 0
+
+chunk_key = HKDF(
+ algorithm=hashes.SHA256(), length=32, salt=None,
+ info=b"file:" + file_hash + b":chunk:" + chunk_idx.to_bytes(4, "big")
+).derive(gek)
+
+plaintext = ChaCha20Poly1305(chunk_key).decrypt(chunk_nonce, ct, None)
+print(f"[5] Decrypted {len(plaintext)} bytes:")
+print()
+print(plaintext.decode())
+```
+
+Run it:
+
+```bash
+python3 download_demo.py
+```
+
+Expected output:
+
+```
+[1] Logged in as bob_test. Token valid for 1 hour.
+[2] GEK bundle fetched from hub (opaque to hub).
+[3] GEK unwrapped successfully (32 bytes).
+
+[4] Files in group (2 entries):
+ a1b2c3d4e5f6... README.txt 59 bytes
+ d7e8f9a0b1c2... sample_data.bin 5242880 bytes
+
+[5] Fetching encrypted chunk 0 of README.txt (file_id=a1b2c3d4e5f6...)...
+[5] Decrypted 59 bytes:
+
+Welcome to the MeshBay demo group. This file is encrypted.
+```
+
+**Measured timings on the demo group:**
+- Login round-trip: ~80ms
+- GEK bundle fetch: ~40ms
+- JWT verification on node: ~884µs (offline, no hub call)
+- README.txt (59 bytes) full download: ~23ms
+- 1 MB encrypted chunk: ~275ms receive, ~2.4ms decrypt
+
+For small files or public groups, you can also download the full file in one request without chunk-level decryption:
+
+```bash
+curl -s -H "Authorization: Bearer $TOKEN" \
+ http://meshbay.org:19001/file/<file_id> \
+ -o README.txt
+```
+
+---
+
+## What's next
+
+- **Create your own group:** see the [User Guide](USERGUIDE.md) §3 — Groups
+- **Video streaming:** the node exposes an HLS endpoint at `/stream/{file_id}` for MP4 and MKV files
+- **Invite a member:** wrap the GEK for them and POST it to `/v1/groups/{group_id}/members/{username}/gek`
+- **Multi-group nodes:** a single node can host multiple groups — configure additional `[[groups]]` blocks in `node.toml`
+- **Full API reference:** [User Guide](USERGUIDE.md) §11