aboutsummaryrefslogtreecommitdiffstats
path: root/docs/meshbay-draft-v3.md
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-08-09 03:58:52 +0200
committerChristophe Besson <cbesson@gmail.com>2026-08-09 03:58:52 +0200
commit0dc2b1b6bcd1a7c1bd508411b31efc2d5916bc58 (patch)
tree9f5757c94585dd7f9d7b6a17855f5c842b29ba14 /docs/meshbay-draft-v3.md
parent271adc8504aad32075d75d06fd42023877a649ec (diff)
downloadmeshbay-0dc2b1b6bcd1a7c1bd508411b31efc2d5916bc58.tar.gz
docs: add architecture draft v3 with POC findings
Key corrections from spikes 1-6: - JWT jti now required (prevents replay, enables revocation) - Argon2id params updated to target 500ms (256MB memory) - NAT order corrected: STUN before UPnP (UPnP unreliable on SFR) - Transport: TCP+TLS v1, QUIC v2 - GEK wrapping protocol confirmed (ECIES-like, 48B opaque bundle) - Hub API table complete with Spike 6 endpoints - 3-package monorepo structure documented Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'docs/meshbay-draft-v3.md')
-rw-r--r--docs/meshbay-draft-v3.md804
1 files changed, 804 insertions, 0 deletions
diff --git a/docs/meshbay-draft-v3.md b/docs/meshbay-draft-v3.md
new file mode 100644
index 0000000..c327415
--- /dev/null
+++ b/docs/meshbay-draft-v3.md
@@ -0,0 +1,804 @@
+# MeshBay — Architecture Draft v3
+
+> Status: preliminary draft — open points marked [TBD]
+> Changes from v2: jti mandatory in JWT (Spike 3), Argon2id params corrected (Spike 1), NAT traversal order corrected (Spike 4), transport flipped to TCP+TLS 1.3 v1 / QUIC v2, GEK wrapping protocol confirmed with exact parameters (Spike 6), hub API table expanded with 4 new endpoints (Spike 6), package structure decided (3 packages, uv monorepo), key persistence requirement added (Spike 6), new sections: Hub API Reference, TCP+TLS Transport v1, Package Structure.
+
+---
+
+## Changes from v2
+
+The following items are **mandatory corrections** driven by POC findings (spikes 1–6). They supersede the corresponding text in v2.
+
+| # | Category | What changed | Source |
+|---|---|---|---|
+| 1 | JWT | `jti` (UUID4) is now **required** in every access token — prevents replay and enables individual revocation. Without it, two tokens issued in the same second are bit-for-bit identical (Ed25519 is deterministic). | Spike 3 |
+| 2 | Argon2id | Parameters updated: `iterations=4`, `memory_cost=262144` (256 MB). Previous params (iterations=3, 64 MB) gave 78 ms — too fast. Target is 500 ms on a home server. CLI calibration command added. | Spike 1 |
+| 3 | NAT traversal | Order corrected: IPv6 → **STUN/hole-punching** → UPnP → TURN relay. UPnP moved to step 3 (disabled on tested SFR box). STUN is now priority 2, not UPnP. | Spike 4 |
+| 4 | Transport | TCP + TLS 1.3 is now the **v1 implementation**. QUIC is the v2 target. The v2 architecture doc had this reversed (QUIC primary, TCP fallback). A `Transport` abstraction layer ensures the switch requires no protocol-layer changes. | Spike 5 |
+| 5 | GEK wrapping | Exact protocol confirmed: ephemeral X25519 + `HKDF(salt=pk_eph, info="meshbay:gek_wrap:v1")` + `ChaCha20-Poly1305(aad=pk_recipient)`. Hub stores opaque 48-byte blobs. | Spike 6 |
+| 6 | Hub API | Four new endpoints validated in Spike 6: `GET /v1/users/{username}/pubkeys`, `POST /v1/groups`, `POST /v1/groups/{group_id}/members/{username}/gek`, `GET /v1/groups/{group_id}/gek`. Full table added as §4.1.5. | Spike 6 |
+| 7 | Packages | Repository structure decided: 3 packages (`meshbay-common`, `meshbay-hub`, `meshbay-node`) in a uv workspace monorepo. RPM package names defined. | POC structure |
+| 8 | Key persistence | X25519 keypairs **must be persisted** client-side before the first hub contact. Lesson from Spike 6 (`bob_state.json` fix). | Spike 6 |
+
+---
+
+## 1. Project Overview
+
+MeshBay is a decentralized, peer-to-peer platform for file sharing, video streaming, and group messaging. It combines identity federation (via Mesh Hubs) with truly peer-to-peer data exchange (via Mesh Nodes), designed to be resilient, censorship-resistant, and user-friendly.
+
+**Core principles:**
+- Data never transits through a central server — only identity and routing do
+- End-to-end encryption for all private content (files, indexes, messages)
+- The node operator is the legal host and is fully responsible for their content
+- The hub is a lightweight registrar, not a content host or indexer
+- Open source, self-hostable at every level
+
+**Domain:** meshbay.org (configurable at compile/deploy time throughout the codebase)
+
+---
+
+## 2. Terminology
+
+| Term | Role |
+|---|---|
+| **Mesh Hub** | Identity authority and group registry server |
+| **Mesh Node** | Local program on the host user's machine |
+| **Mesh Client** | Web browser or Android app (end user) |
+| **Mesh Relay** | Community-operated TURN fallback relay [future] |
+| **MNP** | Mesh Node Protocol — P2P protocol between nodes and clients |
+| **MHP** | Mesh Bay Hub Protocol — inter-hub federation protocol |
+| **GEK** | Group Encryption Key — symmetric key for private group content |
+| **Mesh Directory** | Public registry of groups (hub level) |
+| **Mesh Group Index** | File listing for a group (node level, encrypted for private groups) |
+
+---
+
+## 3. Protocol Versioning
+
+All protocols (MNP, MHP, hub REST API) carry explicit version information.
+
+**Format:** `MAJOR.MINOR`
+- MAJOR bump: breaking change, backward incompatible
+- MINOR bump: backward-compatible addition
+
+**Negotiation:** during handshake, both parties declare their supported version range. The highest mutually supported MINOR within the same MAJOR is used. If no common version exists, connection is refused with an explicit error.
+
+**Support policy:** a release supports the current MAJOR and at least the two previous MINOR versions (N-2).
+
+**Implementation:** a `version` field in every msgpack message header. Handshake step precedes all other exchanges.
+
+---
+
+## 4. System Components
+
+### 4.1 Mesh Hub
+
+A lightweight server acting as a registrar. Intentionally minimal to limit legal exposure and operational cost.
+
+**What the hub stores:**
+- User accounts: username, email (stored for account recovery — see §4.1.1), optional phone number, `PK_user` (Ed25519 + X25519), hub ID, status, creation timestamp
+- Group registry: name, `PK_group`, hosting node identifier, visibility, join policy, member list with encrypted GEK bundles (private groups only)
+- Mandatory connection logs (see §4.1.2)
+- Revocation lists (users and groups)
+- Registered peer hubs (explicit allowlist — no auto-discovery)
+
+**What the hub never stores:**
+- File content or metadata
+- Private group indexes
+- Message content
+- Node current IP (handled by ephemeral signaling — see §4.1.3)
+
+#### 4.1.1 Account Data
+
+Email is kept in full (not hashed) to support:
+- Account recovery (password reset)
+- Legal notifications
+- Abuse contact
+
+Phone number: optional, associable after account creation. On Android, both collected at registration. Accounts are fusionable (email + phone pointing to same account).
+
+Email and phone are stored encrypted at rest in the database.
+
+#### 4.1.2 Mandatory IP Logging (Legal Compliance)
+
+Legal frameworks (LCEN in France, EU e-Commerce Directive, DSA) require service providers to retain connection logs. The hub logs the following with timestamp and IP address:
+
+| Event | Retention |
+|---|---|
+| Account creation | 1 year minimum |
+| Login (success and failure) | 1 year minimum |
+| Group creation | 1 year minimum |
+| Group join / leave | 1 year minimum |
+| Group deletion | 1 year minimum |
+| Revocation actions | 1 year minimum |
+
+Logs are stored in a separate, access-controlled log table. They are not used for any purpose other than legal compliance and are not exposed to users or operators beyond legal requests.
+
+#### 4.1.3 Signaling Service
+
+NAT traversal coordination is handled by a lightweight WebSocket endpoint, logically separate from the main hub API. It is stateless: connection state is held in memory only and discarded after P2P connection establishment (typically within seconds). No persistent storage of node IP addresses.
+
+**Hub interaction summary:**
+
+| Event | Hub crypto load | Frequency |
+|---|---|---|
+| Account creation | Argon2 hash, store PK | Once |
+| Login | Verify password, issue JWT (Ed25519 sign) | Per session |
+| Group creation | Register metadata | Once per group |
+| Member add/remove | Store/remove GEK bundle | On admin action |
+| Group discovery | Return node address + PK_node + GEK bundle | Per initial access |
+| NAT signaling | Relay WebSocket messages (<1 KB) | Per new P2P connection |
+| Public search | Delegate to nodes, 60s in-memory cache | On demand |
+| MHP federation sync | Exchange Mesh Directory | Background, periodic |
+| Revocation | Ed25519-sign revocation token | Rare |
+
+**The hub is never in the data path after connection setup. JWT verification by nodes is local (Ed25519, no hub roundtrip). Confirmed at 884 µs in Spike 3.**
+
+#### 4.1.4 JWT Strategy
+
+Two tokens issued at login:
+
+**Access token** (JWT, signed Ed25519):
+- Validity: 1 hour
+- Payload: `jti` (UUID4, **mandatory** — unique per token, enables individual revocation and prevents replay), `user_id`, `PK_user`, `PK_user_x25519`, `hub_id`, `issued_at`, `expires_at`, hub-signed groups membership claim
+- Presented to nodes for authentication and group access verification
+- Verified locally by nodes using the hub's known public key — no hub roundtrip
+- Compromise window: 1 hour maximum
+
+> **Why `jti` is mandatory:** Ed25519 signing is deterministic. Two tokens with identical payloads issued within the same second produce the same byte sequence. Without a `jti`, they are indistinguishable — a captured token is replayable forever within its validity window, and individual revocation is impossible. The `jti` also provides the revocation handle: hub stores `jti` of invalidated tokens in a server-side denylist.
+>
+> This bug was found and fixed during Spike 3.
+
+**Refresh token** (opaque, random 256-bit):
+- Validity: 30–90 days [TBD exact duration]
+- Stored securely on client only
+- Used exclusively with the hub to obtain a new access token
+- Revocable immediately by the hub (invalidates all future refreshes for this token)
+- Stored server-side as a hashed value
+
+**Revocation flow:** hub invalidates the refresh token → next access token renewal fails → node access expires within 1 hour at most. For immediate revocation of an active access token: hub adds its `jti` to the token denylist; nodes that cache hub public key will periodically fetch the denylist.
+
+**Tech stack:**
+- Language: Python
+- Framework: FastAPI + Uvicorn
+- Database: PostgreSQL + SQLAlchemy + Alembic
+- Deployment: Apache reverse proxy (ProxyPass + SSL termination)
+- Authentication: own system (Ed25519 JWT, Argon2id for password hashing)
+- Hub accessible via domain and directly by IP (self-signed cert warning expected for IP access; documented)
+
+#### 4.1.5 Hub API Reference
+
+Complete table of validated and planned hub REST API endpoints. Endpoints marked ✓ were validated in the POC; endpoints marked [TBD] are designed but not yet implemented.
+
+**Hub metadata:**
+
+| Method | Path | Auth | Description | Status |
+|---|---|---|---|---|
+| GET | `/v1/hub/info` | None | Hub metadata: hub_id, versions, counters | ✓ Spike 2 |
+| GET | `/v1/hub/pubkey` | None | Hub Ed25519 public key (PEM) | ✓ Spike 2 |
+
+**User management:**
+
+| Method | Path | Auth | Description | Status |
+|---|---|---|---|---|
+| POST | `/v1/users/register` | None | Create user account (username, email, password, pk_ed25519, pk_x25519) | ✓ Spike 2 |
+| POST | `/v1/users/login` | None | Authenticate; returns access token + refresh token | ✓ Spike 2 |
+| POST | `/v1/users/token/refresh` | Refresh token | Issue new access token | ✓ Spike 2 |
+| GET | `/v1/users/{username}/pubkeys` | Access token | Fetch `pk_ed25519` and `pk_x25519` for a user (used for GEK wrapping) | ✓ Spike 6 |
+
+**Node management:**
+
+| Method | Path | Auth | Description | Status |
+|---|---|---|---|---|
+| POST | `/v1/nodes/announce` | Access token | Register node with endpoint_hint; returns node_id | ✓ Spike 2 |
+| GET | `/v1/nodes/{node_id}` | Access token | Retrieve node record (pk_node, endpoint_hint) | ✓ Spike 2 |
+
+**Group management:**
+
+| Method | Path | Auth | Description | Status |
+|---|---|---|---|---|
+| POST | `/v1/groups` | Access token | Create group (name, visibility, join_policy, pk_group) | ✓ Spike 6 |
+| GET | `/v1/groups` | None / Access token | List/search public groups; private groups require membership | [TBD] |
+| GET | `/v1/groups/{group_id}` | None / Access token | Group metadata | [TBD] |
+| DELETE | `/v1/groups/{group_id}` | Access token (admin) | Revoke group | [TBD] |
+
+**GEK distribution (private groups):**
+
+| Method | Path | Auth | Description | Status |
+|---|---|---|---|---|
+| POST | `/v1/groups/{group_id}/members/{username}/gek` | Access token (admin) | Upload opaque 48-byte GEK bundle for a member | ✓ Spike 6 |
+| GET | `/v1/groups/{group_id}/gek` | Access token (member) | Retrieve caller's GEK bundle | ✓ Spike 6 |
+
+**Revocation:**
+
+| Method | Path | Auth | Description | Status |
+|---|---|---|---|---|
+| POST | `/v1/revoke/user/{user_id}` | Access token (admin) | Revoke a user account | [TBD] |
+| POST | `/v1/revoke/group/{group_id}` | Access token (admin) | Revoke a group | [TBD] |
+| GET | `/v1/revoke/denylist` | None | Current `jti` denylist for active access tokens | [TBD] |
+
+### 4.2 Mesh Node
+
+A local program running on the host user's machine. The node is the actual host of all content.
+
+**Responsibilities:**
+- Watch and index shared directories (Mesh Group Index)
+- Serve files and video streams to group members
+- Manage all cryptographic keys locally (encrypted keystore)
+- Handle P2P connections and NAT traversal
+- Run the MNP protocol
+- Host the Python extension module sandbox
+- Serve the local web UI (localhost:18000)
+- Host the group chat (core feature)
+
+**Platform:** Linux primary, cross-platform from the start (Windows/macOS). Python ensures portability.
+
+#### 4.2.1 Keystore and Unlock
+
+Private keys (user identity Ed25519, user exchange X25519, group identity Ed25519, GEK copies) are stored in a local encrypted keystore file.
+
+**Format:** msgpack container encrypted with AES-256-GCM, key derived from master password using Argon2id.
+
+**Argon2id parameters (production):**
+- `iterations = 4`
+- `memory_cost = 262144` (256 MB)
+- `parallelism = 1` (or match CPU count — tune to target hardware)
+- Target derivation time: ~500 ms on a home server
+
+> **Why these parameters:** Spike 1 measured iterations=3, memory=64 MB at 78 ms — far too fast. At 78 ms an attacker can attempt millions of guesses per second-equivalent with a GPU cluster. The target of 500 ms on a home server limits offline dictionary attacks to a tractable rate while remaining acceptable for a node that unlocks once at startup.
+
+**CLI calibration:**
+```
+meshbay-node --calibrate-argon2
+```
+This command iterates through parameter combinations and reports the derivation time on the current hardware. The operator selects parameters meeting the 500 ms target and stores them in `~/.config/meshbay/node.toml`. Recommended starting point: `iterations=4, memory_cost=262144`.
+
+**Key persistence requirement:** All keypairs (Ed25519 + X25519) **must be written to the keystore before the first hub contact.** If keypairs are generated at registration time but not persisted before the hub call, subsequent runs will regenerate different keypairs, making all stored GEK bundles on the hub undecryptable. This was identified as a real failure mode in Spike 6 (`bob_state.json` fix).
+
+**Three unlock modes:**
+
+| Mode | How it works | Security level |
+|---|---|---|
+| **Secure (default)** | Password prompted at startup via terminal or local web UI | High |
+| **Lazy file** | Password or derived key stored in `~/.config/meshbay/unlock.key` (chmod 600), read automatically at startup | Medium — acceptable for physically secure home machines. Risk documented at setup. |
+| **Service (headless)** | `MESHBAY_UNLOCK_KEY` environment variable, set via systemd `EnvironmentFile=` pointing to a chmod 600 file | Medium-high — standard practice for server deployments |
+
+Future: OS keychain integration (libsecret/GNOME Keyring on Linux, Windows Credential Manager, macOS Keychain).
+
+#### 4.2.2 Hardware Sizing
+
+The main constraint is **upload bandwidth**, not CPU or RAM.
+
+| Scenario | Simultaneous users | Upload needed | CPU | RAM |
+|---|---|---|---|---|
+| Files + chat, minimal streaming | 10 | 20–50 Mbps | 2 cores | 512 MB |
+| Active 1080p streaming (5–6 streams) | 10 | 50–80 Mbps | 2–4 cores | 1 GB |
+| Mixed use | 50 | 200–300 Mbps | 4 cores | 2 GB |
+| Active streaming | 50 | 400 Mbps | 4–8 cores | 2–4 GB |
+| All use cases | 100 | 800 Mbps–1 Gbps | 8 cores | 4–8 GB |
+
+Beyond 20–30 active streaming users, a dedicated server is required. A home fiber connection (100–500 Mbps symmetric) is suitable for small groups.
+
+Crypto overhead is confirmed negligible: Spike 5 measured full encrypt+sign and verify+decrypt at under 10 ms for a 1 MB chunk. Network latency dominates.
+
+**Tech stack:**
+- Language: Python (primary). Rust extension only if a specific hot path proves insufficient.
+- Transport abstraction layer: `Transport` interface decouples TCP+TLS 1.3 (v1) from QUIC (v2). Application protocol is identical across both transports.
+- v1 transport: **TCP + TLS 1.3** (`asyncio` + `ssl` module, standard library)
+- v2 transport (future): **QUIC** (`aioquic`, Cloudflare-maintained)
+- ICE/STUN: `aioice`
+- WebRTC [future]: `aiortc`
+- Crypto: `cryptography` (PyCA, OpenSSL-backed, hardware-accelerated AES-NI/ChaCha)
+- Serialization: `msgpack`
+- Compression: `zstandard` (zstd)
+- File watching: `watchdog`
+- Local DB: SQLite
+- Local web UI: served by node on `localhost:18000`
+
+### 4.3 Mesh Client
+
+Web browser or Android app. Consumes content from nodes; manages account via hub.
+
+**Hub-side operations:**
+- Account creation and login (Android: email + phone at registration)
+- Public group search and discovery
+- Group membership management
+
+**Node-side operations (direct P2P):**
+- File browsing via Mesh Group Index
+- Group chat (messages + attachments, Signal-like — core feature)
+- File download
+- Video streaming (VOD)
+- [Future] Ephemeral video feed
+
+**Client modes** [to be designed]:
+- Explorer mode: file browser for group content
+- Feed mode: chat thread with attachments
+- Hub/node UI articulation to be defined; Android app will connect to node directly as a near-term priority after account creation
+
+### 4.4 Mesh Relay
+
+**[Future feature]** Community-operated TURN relay. Used only as last-resort fallback when all P2P connection methods fail (symmetric NAT behind CGNAT, approximately 15–20% of connections in the worst case). Traffic is always E2E encrypted — the relay sees only opaque ciphertext.
+
+Not operated by meshbay.org. A relay registration protocol (hub-mediated) will be designed when this feature is introduced. It does not affect the current design.
+
+### 4.5 Package Structure and Repository
+
+**Repository layout:** monorepo managed with [uv workspaces](https://docs.astral.sh/uv/concepts/workspaces/).
+
+```
+meshbay/
+├── packages/
+│ ├── meshbay-common/ # Shared crypto, serialization, protocol types
+│ ├── meshbay-hub/ # Hub server (FastAPI + Uvicorn)
+│ └── meshbay-node/ # Node daemon + local web UI
+├── poc/ # POC and spikes — reference implementation
+│ ├── spike1_crypto/
+│ ├── spike2_hub/
+│ ├── spike3_node_reg/
+│ ├── spike4_nat/
+│ ├── spike5_transfer/
+│ ├── spike6_gek/
+│ └── spike-results.md
+├── docs/
+│ └── meshbay-draft-v3.md
+└── pyproject.toml # Workspace root
+```
+
+**Three packages:**
+
+| Package | RPM name | Contents |
+|---|---|---|
+| `meshbay-common` | `python3-meshbay-common` | Crypto primitives (Ed25519, X25519, ChaCha20, Argon2, HKDF), msgpack schemas, protocol constants, MNP message types |
+| `meshbay-hub` | `python3-meshbay-hub` | FastAPI hub application, database models (SQLAlchemy), Alembic migrations, JWT issuance, GEK bundle storage |
+| `meshbay-node` | `python3-meshbay-node` | Node daemon, keystore, file watcher, TCP+TLS transport, local web UI, extension module sandbox |
+
+**`meshbay-hub` and `meshbay-node` both depend on `meshbay-common`.** There is no runtime dependency between hub and node packages.
+
+**POC directory as reference implementation:** The `poc/` directory contains the working code from spikes 1–6. It is not production code and not packaged, but serves as the canonical reference for:
+- Exact crypto parameter choices (Spike 1)
+- GEK wrapping/unwrapping implementation (Spike 6)
+- Hub API skeleton (Spike 2)
+- NAT detection and STUN interaction (Spike 4)
+- TCP file transfer pipeline (Spike 5)
+
+Developers implementing production features should read the corresponding spike before writing production code.
+
+---
+
+## 5. Group Model
+
+Groups are the core organizational unit.
+
+| Parameter | Options |
+|---|---|
+| Visibility | Public / Private |
+| Join policy | Open / On request / By invitation only |
+| Admin | The hosting node operator (legal host) |
+
+A public group functions like a themed forum: files, chat thread, member list. Join policy is independent of visibility (a public group can require approval to join).
+
+A private group's content (files, index, messages) is always E2E encrypted with the GEK. Only members holding the GEK can decrypt anything.
+
+**Group addressing:**
+```
+meshbay.org/u/username/groupname — public group via hub
+meshbay.org/g/groupname — public group (shorthand)
+group://<PK_group_fingerprint>@<node_addr> — hub-less direct access
+```
+`meshbay.org` is fully configurable throughout the codebase (constant/config file). The hub is reachable via domain or IP (IP access requires self-signed cert; browsers will warn — expected and documented behavior).
+
+---
+
+## 6. Cryptographic Architecture
+
+### 6.1 Key Hierarchy
+
+```
+User Identity Key Ed25519 Signing, authentication
+User Exchange Key X25519 Key agreement (GEK wrapping, session ECDH)
+Group Identity Key Ed25519 Group metadata signing (held by admin node)
+Group Encryption Key ChaCha20 Private content and index encryption (symmetric, 256-bit)
+Session Keys X25519/HKDF Perfect forward secrecy per P2P connection
+```
+
+All private keys stored exclusively on the node (or client device) in the encrypted keystore. The hub never sees any private key.
+
+Both `PK_ed25519` and `PK_x25519` are registered with the hub at account creation. The hub exposes them via `GET /v1/users/{username}/pubkeys` so that group admins can wrap GEK bundles for members without any direct contact between nodes.
+
+### 6.2 GEK Management
+
+**Scope:** GEK applies to private groups only. Public groups use TLS transport only (no application-layer encryption).
+
+**GEK wrapping protocol (ECIES-like, confirmed in Spike 6):**
+
+```
+Admin side (wrap_gek):
+ sk_eph, pk_eph = X25519.generate() # fresh ephemeral keypair per bundle
+ shared = X25519(sk_eph, pk_recipient)
+ wrap_key = HKDF(shared, salt=pk_eph,
+ info="meshbay:gek_wrap:v1",
+ length=32)
+ nonce = random_bytes(12)
+ wrapped = ChaCha20-Poly1305(wrap_key).encrypt(
+ nonce, gek, aad=pk_recipient) # aad binds bundle to recipient
+ bundle = pk_eph || nonce || wrapped # 32 + 12 + 32+16 = 92 bytes on wire
+ # hub stores as opaque 48-byte blob
+ # (without pk_eph in compact form — see note)
+
+Member side (unwrap_gek):
+ shared = X25519(sk_recipient, pk_eph)
+ wrap_key = HKDF(shared, salt=pk_eph,
+ info="meshbay:gek_wrap:v1",
+ length=32)
+ gek = ChaCha20-Poly1305(wrap_key).decrypt(
+ nonce, wrapped, aad=pk_recipient)
+```
+
+> **Hub-stored blob size:** the hub stores the opaque bundle. Spike 6 confirmed the hub stores 48-byte blobs (nonce=12 + ciphertext=20 + tag=16 in the compact wire format used in the spike — `pk_eph` is stored separately in the bundle record). Production schema: hub bundle record = `{ pk_eph (32B), nonce (12B), ciphertext (32B), tag (16B) }` = 92 bytes total per member per group, stored as a single column.
+
+**Security properties confirmed in Spike 6:**
+- Hub never sees the GEK in cleartext
+- Ephemeral keypair is unique per bundle — same GEK and same recipient produce different ciphertext across calls
+- AAD (`pk_recipient`) binds the bundle to its intended recipient — reuse for a different member is detected and rejected
+- Wrong private key → AEAD authentication tag failure → immediate rejection
+
+**Group creation:**
+1. Admin node generates GEK (ChaCha20-Poly1305, 256-bit, CSPRNG)
+2. GEK wrapped for each initial member via the protocol above
+3. Wrapped bundles uploaded to hub via `POST /v1/groups/{group_id}/members/{username}/gek`
+4. Members retrieve their bundle via `GET /v1/groups/{group_id}/gek`
+
+**Member addition:**
+- Admin fetches new member's `pk_x25519` from hub
+- Wraps GEK for them and uploads bundle
+
+**Member revocation:**
+- Admin node generates new GEK
+- Re-encrypts for all remaining members, uploads new bundles
+- New content encrypted with new GEK from this point
+- Former member can still decrypt previously received content (acceptable trade-off — full retroactive re-encryption not planned)
+
+**Key persistence requirement:** before uploading a GEK bundle, the recipient's keypairs must already be registered on the hub and persisted locally. If a user registers, generates keypairs, but does not persist them before the first hub contact, subsequent sessions will regenerate different keypairs and all bundles will be undecryptable. The node initializes and persists all keypairs to the keystore before any hub API call.
+
+### 6.3 On-the-Fly Encryption for File Transfer
+
+Files are stored in plaintext on the host's disk. The node encrypts at read time.
+
+```
+Disk (plaintext) → zstd compress → GEK encrypt (per-chunk) → TCP+TLS 1.3 session → Client → TLS decrypt → GEK decrypt → plaintext
+```
+
+(In v2 transport: replace TCP+TLS 1.3 with QUIC — application pipeline is identical.)
+
+**Chunking:**
+- Chunk size: 1 MB (amortizes AEAD overhead; enables seeking)
+- Per-chunk key derivation: `chunk_key = HKDF(GEK, salt="file:" || blake3(file) || "chunk:" || index)`
+- Each chunk independently decryptable → enables VOD seeking
+- Compress before encrypt (compression is ineffective on ciphertext)
+
+**Chunk authentication:** each chunk signed with the node's Ed25519 key. Client verifies before decryption. Prevents data injection by compromised relay.
+
+**Encryption performance (Spike 5, 1 MB chunk, TCP, Fedora → OVH VPS):**
+
+| Operation | Time |
+|---|---|
+| Encrypt + sign (node side) | 3.2 ms |
+| Verify + decrypt (client side) | 3.9 ms |
+| Total crypto overhead (1 MB) | < 10 ms |
+| Network transfer | 99–234 ms (network-limited) |
+
+Encryption is not the bottleneck. Network latency and bandwidth dominate.
+
+**Pipeline optimization:**
+- `cryptography` (PyCA) uses OpenSSL under the hood, bypasses Python GIL for crypto ops
+- ChaCha20-Poly1305: ~1750 MB/s (Spike 1); AES-256-GCM: >2 GB/s with AES-NI
+- asyncio pipeline (read → compress → encrypt → send) without loading full files into memory
+- GEK-derived chunk keys computed in batch at transfer start, not per-chunk
+
+### 6.4 Transport Security
+
+**Implementation phases:**
+
+| Phase | Transport | Status | Notes |
+|---|---|---|---|
+| v1 | TCP + TLS 1.3 | Current implementation target | Standard library (`asyncio` + `ssl`), well-understood, works everywhere |
+| v2 | QUIC (TLS 1.3 integrated, UDP, multiplexed streams) | Future upgrade | `aioquic`, no protocol changes needed — only transport layer |
+
+The `Transport` abstraction interface in `meshbay-node` decouples the application protocol from the underlying transport. Switching from TCP+TLS to QUIC requires implementing a new `Transport` backend with no changes to MNP message handling, GEK pipeline, or NAT traversal logic.
+
+**Per-connection session keys:** X25519 ECDH + HKDF, independent of the GEK layer. Provides forward secrecy per connection regardless of transport.
+
+**Rationale for TCP+TLS 1.3 first:** UDP hole-punching (required for QUIC in NAT scenarios) adds complexity in the early implementation. TCP outbound from behind NAT (as used in Spike 5) works without any NAT coordination. TLS 1.3 provides equivalent confidentiality guarantees to QUIC's integrated TLS. QUIC's benefits (0-RTT, multiplexing, no head-of-line blocking) are meaningful for performance but not for correctness — they belong in v2 once the application protocol is stable.
+
+### 6.5 TCP+TLS 1.3 Transport Implementation (v1)
+
+**Connection model:**
+- Node listens on a configurable TCP port (default: 18000, same as local web UI port — separate socket)
+- Clients connect outbound; nodes behind NAT connect outbound to other nodes via hole-punching signaling (see §7.1)
+- TLS 1.3 mandatory; TLS 1.2 rejected
+- Node presents a self-signed Ed25519 certificate pinned to its `PK_node` (registered on hub)
+- Client validates certificate against `PK_node` retrieved from hub — not against a CA chain
+
+**Handshake sequence:**
+```
+Client → Node: TCP SYN
+Node → Client: TLS ServerHello (self-signed cert, PK_node)
+Client: verify cert against hub-fetched PK_node
+Client → Node: TLS ClientFinished
+Node → Client: MNP handshake request (version negotiation)
+Client → Node: MNP handshake response (JWT access token, version)
+Node: verify JWT offline (Ed25519, hub public key)
+Node → Client: session established
+```
+
+**Message framing over TCP:**
+- Length-prefixed frames: `[4-byte big-endian length][msgpack payload]`
+- Maximum frame size: 2 MB (prevents memory exhaustion; larger transfers use chunked `file_chunk` messages)
+- Each frame carries the MNP `version` field in its header
+
+**QUIC migration path (v2):**
+- Replace TCP length-framing with QUIC streams (one stream per logical exchange)
+- MNP handshake maps 1:1 to a QUIC handshake stream
+- File transfer maps to a dedicated QUIC stream per file (multiplexed, no head-of-line blocking)
+- Chat messages map to a persistent QUIC stream
+- No changes to JWT verification, GEK decryption, or Index sync logic
+
+**Port allocation:**
+- `18000/tcp` — local web UI (loopback only, not exposed externally)
+- `18001/tcp` — MNP P2P listener (exposed externally, TLS required)
+- Configurable via `~/.config/meshbay/node.toml`
+
+### 6.6 Chat Encryption
+
+Group chat is a **core feature** (not an extension module). Uses the **Double Ratchet algorithm** (as in Signal):
+- Forward secrecy and break-in recovery per message
+- Each message independently encrypted
+- Attachment files: encrypted with the current Double Ratchet message key, hash included in message
+- Python implementation: [TBD — evaluate existing libraries]
+
+---
+
+## 7. Network and Connectivity
+
+### 7.1 NAT Traversal — Attempt Order
+
+```
+1. IPv6 available on both sides → direct connection (preferred)
+2. STUN / ICE + UDP hole punching → ~80–85% success rate (Cone NAT confirmed in Spike 4)
+3. UPnP / NAT-PMP on router → port mapping if available (NOT reliable — disabled on tested SFR box)
+4. Mesh Relay (TURN) → [future feature] — symmetric NAT, CGNAT mobile
+```
+
+> **Correction from v2:** UPnP was listed as step 2 in v2. Spike 4 showed UPnP disabled on the tested SFR residential gateway. STUN + hole-punching (step 2) is more reliable and does not require router cooperation. UPnP is demoted to step 3 as a best-effort supplement, not a dependency.
+
+**Spike 4 findings:**
+- Cone NAT confirmed on SFR residential (same external port 51250 for two different STUN servers)
+- UDP hole punching functional: bidirectional echo received from OVH VPS
+- STUN servers tested: `stun.cloudflare.com`, `stun.l.google.com` — both returned consistent results
+- No CGNAT: stable public IPv4 (81.220.170.32)
+
+Without step 4 (Mesh Relay), approximately 15% of connections between symmetric-NAT peers will fail. This is documented behavior until Mesh Relay is implemented.
+
+Signaling for hole punching: coordinated via hub WebSocket endpoint, <1 KB per attempt, no persistent state.
+
+### 7.2 MNP — Mesh Node Protocol
+
+Application-level protocol over TCP+TLS 1.3 (v1) or QUIC (v2). All messages carry a `version` field. The protocol is transport-agnostic — the `Transport` abstraction layer handles framing differences.
+
+**Defined message types:**
+
+| Type | Description |
+|---|---|
+| `handshake` | Key exchange, JWT presentation, version negotiation |
+| `index_sync` | Encrypted Mesh Group Index delta |
+| `file_request` | Request chunk(s) of a file by hash + chunk index |
+| `file_chunk` | Chunk data + Ed25519 signature |
+| `stream_segment` | HLS/DASH segment (VOD), encrypted with GEK-derived key |
+| `chat_message` | Double Ratchet encrypted message frame |
+| `chat_attachment` | Attachment metadata + key; data transferred as file chunks |
+| `ephemeral_stream` | [reserved, future] Ephemeral video with TTL metadata |
+
+### 7.3 Public Content Delivery — Swarm
+
+Public files identified by `blake3` hash. Multiple nodes can serve the same file:
+
+1. Any node that has a public file and chooses to mirror it registers: `{ hash → node_address }` with the hub
+2. Hub maintains a source table: `{ blake3_hash → [node_A, node_B, ...] }`
+3. Client requests file → hub returns source list → client fetches chunks in parallel from multiple nodes
+4. Integrity verified by blake3 hash on each chunk
+
+**Transport:** TLS only for public content (no GEK). Content signed with the original node's Ed25519 key — clients verify authenticity even when served from a mirror.
+
+---
+
+## 8. Indexes
+
+### 8.1 Mesh Directory (hub level)
+
+Public registry of groups, exchanged between hubs via MHP.
+
+Format: `msgpack`, signed with hub's Ed25519 key, carries `version` field.
+
+Fields per entry: group name, `PK_group`, hosting hub, description, content type tags, join policy, creation date.
+
+### 8.2 Mesh Group Index (node level)
+
+File listing for a group. Generated and maintained by the hosting node.
+
+Format: `msgpack` → `zstd` → GEK-encrypted (private groups) or plaintext + Ed25519 signature (public groups).
+
+Entry structure:
+```python
+{
+ "version": 1,
+ "id": "<blake3_hash>",
+ "name": "filename.mkv",
+ "path": "Movies/2024/",
+ "size": 4294967296,
+ "type": "video", # video | audio | image | document | archive | other
+ "duration": 7245, # seconds, for media
+ "thumb_hash": "<blake3>", # thumbnail also GEK-encrypted
+ "added_at": 1720000000
+}
+```
+
+Delta updates: `{ base_version, additions, deletions }` — no full re-encryption on each change.
+
+Transit: nodes push index deltas to connected members on change; members pull full index on first connection. Hub stores no index content.
+
+### 8.3 Search
+
+**Private groups:** entirely local on the client device. Client maintains a local encrypted cache of all group indexes it has received. No network call, no hub involvement, instant.
+
+**Public groups:** client queries nodes directly at request time. Hub provides routing only.
+
+**Hub web UI search:** delegates query to relevant nodes at request time. Hub stores nothing from this. In-memory micro-cache: **60-second TTL, RAM only, never persisted to disk, public content only.** Qualifies as technical caching under EU DSA Article 13 — not indexing.
+
+---
+
+## 9. Hub Federation (MHP)
+
+### 9.1 Hub Hierarchy
+
+```
+Root Hub (meshbay.org)
+ ├── Full Hub (self-hosted, delegated CA)
+ │ └── issues user credentials, manages own groups
+ │ └── federates with other Full Hubs via MHP
+ └── Mirror Hub
+ └── hosts public Mesh Directory only (no user accounts, no key issuance)
+```
+
+A Full Hub receives a certificate signed by the Root Hub (or a parent Full Hub). Mirror Hubs can only replicate public directory data. Promotion/demotion is possible without breaking the protocol.
+
+### 9.2 MHP Design
+
+- Explicit peer selection: each hub maintains an allowlist of trusted peers
+- No automatic hub discovery
+- Exchanged: Mesh Directory (public groups), revocation lists, cross-hub user authentication data
+- All MHP messages carry `version` field
+
+### 9.3 Cross-Hub Client Access
+
+1. Client (Hub A user) discovers a group on Hub B via Mesh Directory or direct link
+2. Client presents Hub A JWT directly to Hub B
+3. Hub B verifies JWT using Hub A's public key (fetched once, cached)
+4. Hub B issues short-lived local session token
+5. Client connects to node as normal
+
+---
+
+## 10. Moderation
+
+### 10.1 Public Content
+
+```
+Report #1 → automatic suspension of public access
+ → node operator notified
+One republication allowed
+Report #2 → escalated to hub moderators
+Confirmed → group revoked on local hub
+ → revocation propagated to federated hubs via MHP
+```
+
+Mechanism: `blake3` hash added to hub blocklist. Signed revocation token sent to node.
+
+### 10.2 CSAM
+
+Hash matching against NCMEC/IWF database on public content at registration time. No scanning of private/encrypted content. Participation is mandatory for hub operators and reduces legal exposure.
+
+### 10.3 Copyright
+
+DMCA/legal notice framework. Takedown on notification. No automated technical blocking (false positive risk, fair use). Hub can revoke on confirmed legal request.
+
+### 10.4 Private Content
+
+Not directly moderatable (E2E encrypted). Action available: revoke user or group at hub level on formal legal request. Hub issues Ed25519-signed revocation token verifiable by all member nodes offline.
+
+---
+
+## 11. Python Extension Module System
+
+The node loads extension modules (Python) in a sandboxed subprocess. **Chat is a core built-in feature, not a module.**
+
+**Module manifest:**
+```python
+{
+ "name": "my-extension",
+ "version": "1.0.0",
+ "mnp_version": ">=1.0",
+ "permissions": ["read_index", "send_message", "receive_events"]
+}
+```
+
+**Available APIs:**
+- `read_index()` — read current group index (read-only)
+- `send_message(content)` — post to group thread
+- `receive_events(handler)` — subscribe to group events
+
+**Unavailable:** arbitrary network, filesystem access outside group context, system calls.
+
+---
+
+## 12. Legal Framework
+
+**Node operator:** primary legal host of content. Fully responsible for what they share. Node setup communicates this explicitly.
+
+**Hub operator (meshbay.org):** registrar, not content host. Stores minimal data. Operates takedown mechanism. Participates in CSAM hash matching. Legal exposure analogous to a domain registrar.
+
+**Protocol/software author:** protected by substantial non-infringing uses.
+
+**Hub data:**
+- Email and optional phone: kept for account recovery and legal compliance
+- Password: Argon2id hash, never stored in cleartext
+- Connection logs: retained per legal requirements (minimum 1 year)
+- Content metadata: never stored
+- Node current IP: not persisted (signaling is ephemeral)
+- GEK bundles: opaque 48-byte ciphertext blobs; hub cannot decrypt them
+
+---
+
+## 13. Future Features
+
+- **Mesh Relay:** community TURN relays, relay registration protocol via hub, E2E encrypted traffic. Necessary for symmetric NAT (CGNAT mobile, some professional ISPs).
+- **QUIC transport (v2):** replace TCP+TLS 1.3 with QUIC once application protocol is stable. Transport abstraction layer makes this a drop-in replacement.
+- **Content replication between nodes:** node-to-node, admin-authorized, no hub involvement
+- **Hub mirror (load balancing):** full hub replication (user DB, group registry, GEK bundles) for load distribution. Requires distributed DB strategy (PostgreSQL streaming replication or equivalent). Complex — design when needed.
+- **Mobile video push → node:** mobile films → pushes to hosting node → ephemeral stream with TTL to group members. MNP `ephemeral_stream` type reserved.
+- **Node–mobile pairing:** QR code from local web UI
+- **Multi-source download:** parallel chunk fetching from swarm for public files
+- **iOS client**
+- **At-rest encryption on node:** optional for server-deployed nodes
+- **OS keychain integration for keystore unlock**
+- **WebRTC:** `aiortc` for browser-native P2P (no node required for clients)
+
+---
+
+## 14. Open Questions [TBD]
+
+**Resolved by POC (no longer open):**
+
+| # | Question | Resolution | Source |
+|---|---|---|---|
+| R1 | Argon2id parameters: what values target ~500ms? | `iterations=4, memory_cost=262144` (256 MB). Use `meshbay-node --calibrate-argon2` for hardware-specific tuning. | Spike 1 |
+| R2 | JWT payload claims: what fields for offline node verification? | `jti` (UUID4), `user_id`, `PK_user`, `PK_user_x25519`, `hub_id`, `issued_at`, `expires_at`, group membership claim. `jti` is mandatory (prevents replay, enables revocation). | Spike 3 |
+| R3 | GEK wrapping protocol: exact algorithm? | ECIES-like: ephemeral X25519 + HKDF(salt=pk_eph, info="meshbay:gek_wrap:v1") + ChaCha20-Poly1305(aad=pk_recipient). Hub stores opaque 48-byte blobs. | Spike 6 |
+| R4 | NAT traversal: is STUN/hole-punching sufficient for residential users? | Yes for Cone NAT (SFR, Orange, Free). Relay needed only for symmetric NAT (CGNAT mobile). UPnP unreliable — demoted to step 3. | Spike 4 |
+| R5 | Transport: QUIC or TCP+TLS 1.3 for v1? | TCP+TLS 1.3 for v1 (lower complexity, works everywhere). QUIC for v2 via `Transport` abstraction. | Spike 5 |
+| R6 | Hub API: which endpoints for GEK distribution? | `GET /v1/users/{username}/pubkeys`, `POST /v1/groups`, `POST /v1/groups/{group_id}/members/{username}/gek`, `GET /v1/groups/{group_id}/gek` | Spike 6 |
+| R7 | Package structure? | 3 packages: `meshbay-common`, `meshbay-hub`, `meshbay-node`. uv workspace monorepo. | POC |
+
+**Still open:**
+
+1. **Refresh token validity:** 30 or 90 days?
+2. **Group address scheme:** final URL format confirmation
+3. **Double Ratchet library:** identify best Python implementation (evaluate `python-doubleratchet`, `axolotl`, or custom)
+4. **GEK bundle location for groups with mixed access** (public-restricted): hub or node?
+5. **MHP federation sync frequency and conflict resolution**
+6. **Hub mirror replication strategy** (when implemented)
+7. **Chat attachment storage:** stored on node like regular files, or separate store?
+8. **Relay registration protocol design** (when implemented)
+9. **Token denylist distribution:** how do nodes fetch and cache the `jti` denylist? Push (hub WebSocket) or pull (periodic poll)? Cache TTL?
+10. **QUIC migration timeline:** when is the application protocol considered stable enough to begin v2 transport implementation?
+11. **Port configuration conflict:** `18000` used for both local web UI and (in some proposals) MNP listener — needs final port allocation decision (proposed split: 18000 for web UI, 18001 for MNP).