| Commit message (Collapse) | Author | Age | Files | Lines |
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Phase 11.5.4/5/7/8 — findings C6 (WebRTC half), C3, L4, M1, M9.
New meshbay_common/handshake.py is the single implementation of authorization
and proof: JWT verify, scope, denylist, mandatory group_id, membership, hosting.
The handshake previously existed three times over and only the newest copy
enforced the GEK proof.
C3 — mutual authentication. Authentication ran one way: the client proved
itself, the node proved nothing. handshake_ack.node_pk was never verified
against anything and per-chunk signatures had been dropped in Phase 9.15, so a
peer that had hijacked signaling (C2) or been substituted by the hub could
accept the client's proof, ignore it, and serve a forged index, forged chat
history and a forged is_node_admin flag. The client now sends a nonce; the node
answers with its own GEK proof over that nonce AND an Ed25519 signature over
the transcript; the browser verifies both and refuses otherwise. It also
refuses an unchallenged handshake_ack, which previously let a peer skip proving
anything at all.
L4 — the proof was nonce ‖ offer_fp ‖ answer_fp: bare concatenation, and a
missing fingerprint silently degraded it to nonce-only, dropping MitM detection
(NS5). Every field is now length-prefixed and domain-separated, the role is
bound so a client proof cannot be replayed as a node proof, and an absent
channel binding is refused rather than tolerated.
M1 — group_id was optional; omitting it skipped the membership check entirely
and fell back to the node's first group. Now mandatory.
M9 — node-scoped daemon tokens are refused on the client path.
NOT DONE: quic_server.py still runs its own JWT-only handshake, so C6 remains
open — a forged or stolen token reaches a node over QUIC and can inject chat
without holding the GEK. quic_binding() is written and unit-tested but unwired.
11.5.6 (whether the certificate-hash anchor works with aioquic, or an RFC 5705
exporter is reachable) is unproven. 11.5.8 TOFU pinning of pk_node is not done:
the client verifies the node's signature but does not yet remember which key it
saw last.
Adds packages/meshbay-common/tests/test_handshake.py (18 tests) covering the
properties every transport must inherit. WebRTC test helpers rewritten around
the shared module; _make_jwt now defaults to the test group, since group_id is
mandatory.
Tests: 24 webrtc, 176+ node+common.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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>
|
| |
|
|
|
|
|
|
|
|
| |
aiortc advertises maxMessageSize=65536 in SDP. A 64KB data chunk +
msgpack envelope + 4-byte length prefix exceeds this limit, causing
"Trying to send message larger than max-message-size" on upload.
48KB data + overhead stays well under 65536 bytes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Two fixes for the MSE video player:
1. Set mediaSource.duration from the ffprobe-reported duration on
sourceopen, so the seek bar shows the correct video length instead
of NaN/infinite.
2. Use SourceBuffer mode='sequence' for sequential append without
timestamp gaps. Add a seeking handler that clamps currentTime to
the buffered range — seeking beyond buffered data snaps back
instead of freezing the video.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
| |
1. "Connecting..." was shown in 3 places simultaneously (status badge,
cached files area, and general status). Now only the badge shows it
when cached files are visible — the redundant messages are removed.
2. The floating stream progress bar caused constant re-renders during
video streaming (every 256KB segment triggered setState). The fMP4
remux size also differs from the original file size, making the
progress inaccurate. Removed the overlay bar entirely — the video
element's native buffered range indicator is sufficient.
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
- 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>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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>
|
| |
|
|
|
|
|
|
|
|
|
|
| |
- 10.5: Notification model + CRUD API (list, mark read, mark all read)
Triggered on: group invite, role change, suspend/unsuspend
- 10.6: SettingsPage shows role, per-group notification mute (localStorage)
- 10.7: GET /v1/groups?q= search filter (ilike on name)
- 10.8: NotificationFeed on home page + bell with unread badge in navbar
- 10.10: GET /v1/hub/version endpoint for client update checks
- 8 new tests (test_notifications.py), 155 total
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
| |
- Site overlay: landing page, /about, /downloads (dark/light, responsive)
- User role column (user/moderator/admin) with config-based admin sync
- require_moderator dependency + admin API (8 endpoints: stats, users,
groups, audit logs)
- Admin SPA panel at #/admin with 5 tabs (stats, users, groups, logs,
blocklist) — visible only to moderators/admins
- SPA also served at /app/ for Caddy site overlay integration
- GET /v1/users/me returns current user role
- 15 new tests, 147 total passing
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>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
keyderive.py: derive Ed25519+X25519 from username+password via Argon2id.
Same credentials → same keys on any device. Encrypt/decrypt keypair
bundle (AES-256-GCM) for hub storage (web clients).
7/7 tests. Full suite: 81/81.
keyderive.js: browser counterpart using PBKDF2-SHA512 + random keypairs
encrypted for hub storage. Avoids algorithm mismatch with Python.
hub/models.py + users.py: keypair_bundle field added to User, stored on
registration, returned in login response for web client key recovery.
QUICKSTART.md: fully rewritten. 3 operational scripts in QE/demo-v1/:
setup_demo.py — create accounts, group, distribute GEK
run_node.py — start HTTP node (watches shared/ directory)
download.py — bob login → GEK fetch → decrypt → save
All tested locally end-to-end. No invented URLs.
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>
|
|
|
webapp.py: serves / (HTML SPA) and /app.js (JS client).
app.js: login, hub API calls, node HTTP API integration,
file browser, HLS video streaming via <video> tag.
groups.py: GET /v1/groups — public group listing (no auth).
Deployed on https://meshbay.org — 200 OK. 47/47 tests.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
|