| Commit message (Collapse) | Author | Age | Files | Lines |
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Phase 11.5 — findings H1, C5a, H2, C5b, H5 (see second-review.md).
Batched together because the node-side changes share webrtc_server.py and
cannot be separated into working commits.
H1 — cross-group chat leak. chat_store, the peer registry and the display-name
cache were read from the shared transport context, and daemon.py hoisted the
FIRST group's chat store onto it. On a node hosting several groups every
group's messages went to one database, chat_history served them back to members
of every other group, and chat broadcast reached all peers regardless of group.
All three now resolve through _group_ctx().
C5a — upload confinement. Uploads landed in the shared root under a
client-chosen name and overwrote whatever was there. Any member could destroy
the operator's files, and by becoming the recorded uploader of the replaced
file could then delete it through the uploader path, bypassing the Ed25519
admin challenge. Uploads now go to a per-user quarantine (.uploads/{user_id}/),
refuse to overwrite, and enforce chunk ordering, a filename allowlist and a
size cap.
H2 — stored XSS in the node admin UI. Filenames chosen by any group member were
interpolated raw into the localhost UI, which has no authentication, so script
execution there equals control of the node admin API. Now html.escape()
throughout, textContent in the audit table, plus CSP/nosniff/no-referrer. The
CSP contains exfiltration but cannot stop injected inline script — escaping is
the fix.
C5b — group key seizure. gek_bundle_store wrote whatever any member sent and
auto-activated bundles addressed to the node operator. The operator's X25519
public key is public (the node publishes it in handshake_ack), so any member
could wrap a key of their choosing for it and take over the group, locking
every legitimate member out. Storing now requires an operator signature and
_try_activate_gek is removed: nothing arriving over MNP can set a live GEK.
H5 — unbound signing oracle. The node challenged with 32 raw random bytes and
the client signed them blind, so a signature named no operation, subject, node
or time. New meshbay_common/adminop.py defines a length-prefixed,
domain-separated transcript; both sides build it independently and the client
refuses to sign when the announced op/subject do not match its request.
BREAKING: a group admin who does not operate the node can no longer store GEK
bundles on it. Invites must be performed by the node operator.
Adds tests/test_security_regressions.py. Verified against pre-fix source via
git stash. Three pre-existing tests asserted the vulnerable behaviour as a
feature and were inverted: gek auto-activation, and the transport-wide
chat_store in test_daemon.
Tests: 109 node, 132 hub+common.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Phase 11.5.A — findings C1 and C6 (see second-review.md).
C1: the per-group HTTP file API bound 0.0.0.0 for every configured group,
private ones included, and served two endpoints with no authentication at all:
GET /index (full Mesh Group Index) and GET /file/{id} (raw plaintext file via
FileResponse). Anyone able to reach the port — LAN, forwarded port, permissive
IPv6 — read every private file. This bypassed the entire GEK-proof and node
sovereignty layer. Deleted rather than patched: it duplicated MNP without any
of its controls.
C6: the TCP+TLS chunk server accepted a bare JWT with no GEK proof, leaving a
second non-compliant handshake path. Deleted; QUIC remains and will be brought
to parity with WebRTC by the unified handshake in 11.5.4.
Transport decision recorded in transport/__init__.py: WebRTC/ICE is primary for
browser and native clients (the only NAT traversal validated here — 2 ISPs,
IPv4 STUN + IPv6, 4G CGNAT); QUIC is kept for LAN, port-forwarded and hub-less
group:// access. punch_nat() is a direct-connection helper, not a traversal
stack.
Also removed server_ssl_context()/client_ssl_context() from tls_cert.py (no
remaining callers) and a dead import of the former in quic_server.py.
generate_self_signed_cert() stays: QUIC uses it, and the certificate hash is
the intended channel-binding anchor for 11.5.6, since QUIC has no DTLS
fingerprint to bind the GEK proof to.
BREAKING CHANGE: node.toml keys `port` and `http_port` are gone. Regenerate
config with `meshbay-node init`. Env var MESHBAY_PORT -> MESHBAY_QUIC_PORT.
Tests: 198 passed (209 - 7 test_http_server - 4 test_transport). No other test
changed status. Net -1300 lines.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Baseline commit capturing in-progress Phase 12 work that was already present
in the working tree (uncommitted) before the Phase 11.5 security remediation
begins. Committed as-is, without review or modification, so that remediation
changes arrive as a separable diff.
Contents: BundleStore (P2P GEK + keypair bundles), password split
(auth_key / bundle_key), node Ed25519 auth (POST /v1/nodes/auth, node-scoped
JWT), GEK-HMAC handshake proof with DTLS channel binding, Ed25519 admin
challenge-response, node local admin UI rewrite, browser key persistence.
Not authored in this session — captured to establish a baseline.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Add SQLite audit store for legal compliance (LCEN/DSA): logs user IP,
actions (handshake, file download/upload/delete, stream, chat), and
timestamps. Retention: 1 year, with cleanup method.
WebRTC transport now logs all user actions to the audit store with
remote IP extraction from the ICE transport.
Local web UI rewritten as a proper admin dashboard:
- Stats cards (groups, files, peers)
- Connected peers table with IP, username, group, state
- Group cards with file listings and shared directory info
- Audit log page with event/user filtering
- Dark theme, responsive, auto-refresh
- JSON API: /api/status, /api/groups, /api/peers, /api/audit, /api/config
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
| |
When watchdog detects file changes, the daemon now:
- Pushes INDEX_SYNC to all connected WebRTC peers in that group
- Registers file hashes with hub /v1/swarm/register endpoint
Also registers all file hashes on startup for initial discovery.
hub_client: add register_swarm() method for bulk hash registration.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The node daemon was previously a skeleton that only started QUIC/TCP
servers and the local web UI. All browser-facing functionality (WebRTC,
hub WebSocket, chat store, HTTP file API) lived in QE demo scripts.
This rewrites daemon.py to be fully self-contained:
- WebRTC transport for browser clients (aiortc DataChannel)
- Hub WebSocket task (signaling, revocations, WebRTC offers)
- ChatStore per group (SQLite in ~/.local/share/meshbay/)
- HTTP file API per group (create_http_app on configured port)
- Graceful shutdown (all transports, stores, tasks)
- hub_client: _ws tracking + send_ws() for chat notifications
- config: data_dir field for persistent state
- systemd: security hardening (ProtectSystem, StateDirectory)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
| |
Replace download-then-play VideoPlayer with MSE (MediaSource Extensions)
streaming. Node remuxes to fMP4 via ffmpeg, probes codecs with ffprobe,
and sends encrypted segments over DataChannel. Browser decrypts and
appends to SourceBuffer — playback starts within seconds.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
inline thumbnails
Backend:
- Chat store persists sender_name (SQLite migration, no more UUID display)
- FILE_DELETE / FILE_DELETE_ACK MNP types — node admin can delete files
- Node sends chat_notify to hub WS — hub creates notifications for offline members
- Hub revocation.py handles chat_notify, creates per-member notifications
Frontend:
- Upload chunk size 64KB (was 1MB) — fixes WebRTC DataChannel max-message-size
- Show cached files immediately while WebRTC connects
- ChatImage component — inline image thumbnails in chat (download+decrypt)
- File delete action in menu (group admin, with confirm dialog)
- Member panel: "Owner" label instead of confusing "Group admin"
- Create group page: hint about needing a node
- Refresh index after chat attachment upload
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
inline thumbnails
- Upload chunks capped at 64KB to avoid WebRTC DataChannel max-message-size
- Show cached files immediately while WebRTC connects (tabs visible during connection)
- Persist sender_name in chat store (SQLite) — no more UUID display in history
- File delete action in menu (node admin only, enforced server-side)
- FILE_DELETE / FILE_DELETE_ACK MNP message types
- Inline image thumbnails in chat attachments (download+decrypt, Signal-style)
- Member panel: "Owner" label instead of "Group admin" to avoid hub/group admin confusion
- Create group page: hint about needing a node
- Refresh index after chat file attachment upload
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
| |
|
|
|
|
|
| |
Concurrent peer disconnects could mutate _peers/_sessions during
iteration, causing RuntimeError: dictionary changed size during iteration.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Bug fixes:
- Group creation "[object Object]" error: removed dead pkcs8 import code
that threw before GEK wrapping, added array detail handling in hubFetch
- Chat shows usernames instead of UUIDs (sender_name passed through node)
- Join button: navigate to group on "Already a member" instead of error
UI improvements:
- Loading spinner animation for async states (connecting, fetching)
- File action menu (3-dot dropdown: View, Download, Play)
- Click filename to preview inline (images, text/code files)
- FilePreview overlay for images and text files
- Chat file attachment button (upload to node + structured message)
- Chat attachment display (icon, filename, size)
- Member panel: "Group admin" badge instead of plain "Admin" text
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
search)
Six self-service features for the web SPA:
- Group creation UI with GEK auto-generation (AES-256-GCM ECIES)
- Member management + invite by username (GEK wrapping for invitee)
- Open group self-join flow (POST /v1/groups/{id}/join)
- File upload client→node (FILE_UPLOAD MNP type, .uploads/ staging)
- IndexedDB caching of group file indexes (instant display on revisit)
- Cross-group file search (SearchPage, pure client-side on cached indexes)
11 new tests (166 total): 8 group self-service + 3 AES GEK wrap/unwrap.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Complete browser-based client: Preact SPA with login, group file browser,
encrypted download, video playback, group chat, i18n, and dark/light theme.
Browser connects P2P to nodes behind residential NAT via WebRTC DataChannel
(aiortc). Hub handles signaling only — all data flows E2E.
Performance: pipelined downloads (8-chunk sliding window), binary msgpack
wire format (no base64), redundant I/O elimination. Large file downloads
stream to disk via File System Access API (showSaveFilePicker).
Validated on SFR + Orange residential NATs, Chrome + Firefox, IPv4/IPv6.
132 tests passing. Deployed to meshbay.org + Orange node.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Browser clients can now connect P2P to nodes behind residential NAT via
WebRTC DataChannel with ICE/STUN. Validated on SFR Port-Restricted Cone
NAT + 4G CGNAT across three scenarios (WiFi LAN, 4G IPv6, 4G IPv4 STUN).
No TURN relay needed. Hub serves only as signaling relay (<1 KB).
New files:
- webrtc_server.py: aiortc-based WebRTC transport (node side)
- signaling.py: SDP/ICE relay endpoint (hub side)
- transport.js: browser WebRTC client with msgpack framing
- webrtc-test.html: spike test page for browser→NAT→node validation
- test_webrtc_transport.py: 4 tests (handshake, file transfer, auth, guard)
- meshbay-draft-v4.md: architecture spec updated for web client
Modified:
- hub_client.py: WebRTC offer handling via hub WebSocket
- revocation.py: node_id from WS auth + webrtc_answer routing
- pyproject.toml: aiortc>=1.9 dependency
123 tests passing (117 existing + 6 new).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Implements all 8 milestones (7.0-7.7):
- 7.0: JWT carries `groups` claim; node verifies group membership at
MNP handshake (QUIC + TCP+TLS). Resolves security review C2.
- 7.1: QUIC 0-RTT session resumption via stored session tickets
(17-21ms reconnect vs 47ms cold).
- 7.2: Hub→node WebSocket signaling for NAT punch coordination
(`client_incoming`/`punch_ready`) + jti denylist push. Denylist
class blocks revoked users/jtis at handshake.
- 7.3: Multi-group daemon — one QUIC port serves N groups with
per-group GEK, shared_root, and index routing.
- 7.4: HLS streaming via QUIC (STREAM_SEGMENT message type, ffmpeg
segment extraction).
- 7.5: Sender Keys protocol for group chat (Signal Groups approach).
Each member has own sending chain key, HKDF chain ratchet, AES-256-GCM
encryption, Ed25519 signing. Resolves security review C1.
- 7.6: Chat store (SQLite via aiosqlite), CHAT_MESSAGE MNP wire type
with peer broadcast, web UI with WebSocket push.
- 7.7: Argon2id calibration CLI.
First security review included (first-review.md). 109 tests, demo-v3
validated against meshbay.org production hub.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
SFR residential NAT is Port-Restricted Cone: inbound is only allowed
from (peer_ip, peer_port) if the node previously sent a packet TO
(peer_ip, peer_port) from the SAME socket.
punch_nat(peer_ip, peer_port): sends a probe UDP packet from the
QUIC server's own transport (_transport.sendto), creating the correct
NAT entry. Used after server.start() to enable direct QUIC connections
through SFR NAT without UPnP or relay.
demo-v2 result: QUIC/UDP direct Fedora→SFR NAT→meshbay.org validated.
Connection time 12.7s (QUIC handshake through NAT). File transfer 700B.
QuicChunkClient local_port param: ensures client binds to same port
as punch_nat destination (Port-Restricted Cone requirement).
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
| |
local_port=0 param on QuicChunkClient — specify for Port-Restricted Cone NAT
hole punching (client must send from the same port the node probed to).
QuicChunkServer default host '::' for IPv4+IPv6 dual-stack on Linux.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
1. indexer.py: streaming blake3 (8MB chunks) instead of read_bytes().
Large files (initrd.img, ISOs, VM images) no longer load into RAM.
2. QE/demo-v1/run_node.py: call indexer.start() not initial_scan().
initial_scan() alone never starts the watchdog observer — files added
after startup were silently ignored. Added indexer.stop() on shutdown.
3. USERGUIDE.md §8: clarify symmetric vs asymmetric.
Ed25519/X25519 = asymmetric (key pairs). ChaCha20-Poly1305 and
AES-256-GCM = symmetric AEAD 256-bit (content encryption).
ChaCha20 is PRIMARY; AES-GCM is optional browser-compat variant only.
4. pyproject.toml: aioquic, websockets, aiosqlite, slowapi added to
proper package deps (were installed manually, now declared).
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
meshbay-node/pyproject.toml: add aioquic>=1.0 (was commented 'v2'),
websockets>=12.0 (revocation push). Both are production code since Phase 5.
meshbay-hub/pyproject.toml: add aiosqlite (tests without PostgreSQL),
slowapi (rate limiting), websockets (revocation push), PyJWT (explicit).
transport/__init__.py: QUIC imports wrapped in try/except — node works
without aioquic (TCP+TLS + HTTP fallback). QUIC_AVAILABLE flag exported.
QUICKSTART.md: replace manual pip list with 'pip install -e' that pulls
all deps from pyproject.toml automatically. Add dependency table.
CLAUDE.md: clarify that all deps go in pyproject.toml, not manual installs.
81/81 tests.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
6.1 Double Ratchet (meshbay_common/ratchet.py):
Forward secrecy, break-in recovery, out-of-order delivery.
Signal-spec KDF_RK/KDF_CK via HKDF-SHA256. 11/11 tests.
6.2 Multi-group node (config.py):
[[groups]] TOML array, per-group ports, back-compat [group].
6.3 MHP federation persistence (db/models.py FederatedGroup + SwarmSource):
receive_directory() now persists to federated_groups table.
list_public_groups() includes federated results with source attribution.
6.4 Content replication (node/replication.py + hub SwarmSource):
ContentReplicator: fetch-index, download, hash-verify, register-swarm.
Hub: POST /v1/swarm/register, GET /v1/swarm/{hash} for multi-source.
6.5 Browser private group (webcrypto.py + static/crypto.js):
AES-256-GCM variant of GEK for WebCrypto-compatible groups.
crypto.js: SubtleCrypto importGEK + deriveChunkKey + decryptChunk.
Keys distinct from ChaCha20 via :aes HKDF info suffix. 4/4 tests.
74/74 tests total.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
| |
Config now supports [[groups]] array (N groups) alongside back-compat
[group] single section. Each group has independent port/quic_port/http_port
and visibility. GroupConfig gains quic_port, http_port, visibility fields.
NodeConfig gains quic_port and http_port defaults. 70/70 tests.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
| |
Hub: /v1/nodes/ws WebSocket endpoint for persistent node connections.
/v1/admin/revoke marks user/group revoked in DB, signs JWT revocation
token (EdDSA), broadcasts to all connected nodes.
Node: RevocationSubscriber maintains WS connection, verifies
incoming revocation tokens offline (hub Ed25519 PK), adds to
local blocklist (_revoked_users/_revoked_groups sets).
53/53 tests.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
| |
QuicChunkServer/QuicChunkClient: same MNP protocol over QUIC/UDP.
Enables hole-punching (Spike 4 Cone NAT validated). Uses aioquic 1.3.0.
Bug found+fixed: asyncio.Event race condition in client recv loop
(quic_event_received overwrote _stream_events[0] after _recv
created it). Fixed with asyncio.Queue (no shared mutable state).
Server uses synchronous handlers in quic_event_received (avoids
ensure_future transmit timing issue). 3/3 tests. Full suite: 50/50.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
| |
FastAPI app on port 19001: GET / (node info), GET /index (public
group index JSON), GET /file/{id} (full download), GET /file/{id}/{n}
(encrypted or plaintext chunk), GET /hls/{id}/playlist.m3u8 +
GET /hls/{id}/{n}.ts (HLS streaming via ffmpeg).
Public groups: index browsable without auth, files downloadable.
Private groups: chunks encrypted with GEK, auth required.
7/7 tests passing. Full suite: 47/47.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
| |
config.py: TOML + env var overrides, sane defaults.
daemon.py: full startup sequence (keystore→hub→GEK→indexer→
server→UI), SIGINT/SIGTERM shutdown, calibrate-argon2 command.
ui/app.py: FastAPI on localhost:18000, status+files JSON API,
HTML status page (auto-refresh 10s). All bound to 127.0.0.1.
Full suite: 29/29 tests.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
| |
Self-signed TLS cert (RSA-2048, TLS 1.3 min). Server: JWT offline
verify, index sync, file_request → encrypt+sign chunk pipeline.
Client: handshake, fetch_index, fetch_chunk with Ed25519 verify +
blake3 hash check + GEK decrypt. Integration test: 2MB file served
in 2 chunks, reassembled == original. 3/3 tests. Full suite: 29/29.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
| |
GroupIndex: msgpack→zstd→GEK-encrypt→sign for private groups,
plaintext+sign for public groups. DirectoryIndexer: watchdog-based
watcher, async initial scan via thread pool, on_change callback.
Delta support (diff between versions). 10/10 tests passing.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
| |
Register/login/announce/token-refresh/GEK-unwrap. JWT jti and
pk_user verified at login. Hub PK cached after first fetch.
6/6 tests passing with httpx.MockTransport (no network).
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
| |
Argon2id + AES-256-GCM encryption at rest. Unlock via env var
(MESHBAY_UNLOCK_KEY), unlock.key file (chmod 600), or interactive
getpass. 10/10 tests passing.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
|
|
|
3-package layout: meshbay-common (shared crypto/protocol),
meshbay-hub (FastAPI server), meshbay-node (local daemon).
Includes validated POC spikes 1-6 in poc/, architecture drafts
v1/v2 in docs/, and CLAUDE.md project conventions.
All cryptographic primitives extracted from POC into
meshbay_common/crypto.py (GEK wrap/unwrap, chunk key derivation,
keystore encryption, chunk signing).
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
|