# 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::chunk:") 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/ \ -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