aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/src/meshbay_node/daemon.py
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-03 16:16:55 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-03 16:16:55 +0200
commit675beed6ff688733a9598f9d82d41578f48316be (patch)
tree78dd4f8dff312f0ad99bd63bc679bf402591c5ed /packages/meshbay-node/src/meshbay_node/daemon.py
parent15087b0e8fdb872602310119f14680aaa443fd93 (diff)
downloadmeshbay-675beed6ff688733a9598f9d82d41578f48316be.tar.gz
feat!: MNP 1.0 — seal index and handshake_ack under the group key
`index_sync`, `index_delta` and the `handshake_ack` config payload now travel sealed under a GEK-derived subkey (`meshbay_common/groupbox.py`, mirrored by `sealGroup`/`openGroup` in `crypto.js`). Only `type`, `v`, `group_id` and the ack's `node_pk`/`proof`/`sig` stay in clear — a receiver must route and authenticate before it would trust a decryption. Verify, then decrypt. The ack line is integrity, not confidentiality: the signed handshake transcript names no ack field, so `is_node_admin`, `enabled_apps`, `video_root` and the rest were authenticated by the DTLS channel alone. The index line is defence in depth against a repeat of C1/C6 — a peer served before the handshake completes now gets ciphertext, not filenames. Nothing against an observer, the hub, or a member; that is the whole claim. `index_progress` stays clear (D3, counters only). Chat is out of scope. Failure is fatal: a payload that does not open ends the session naming the message type — never an empty index or an empty `enabled_apps`, both of which are legitimate states. Version negotiation ships here too (phase 15.6, brought forward): `v` + `v_min` on `handshake` and `handshake_challenge`, refused with `version_too_old` / `version_too_new` / `version_unreadable`. The flag day was already being paid for; the next breaking change now costs a refusal message. BREAKING CHANGE: breaks the WebRTC wire every deployed client speaks. Hub and every node must deploy together; the SPA is served by the hub, so a browser picks up the new client on reload. See MESHBAY_NODE_PROTOCOL.md §11.1a, §13.1. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HkzbhmMmK8PqQBtGz5zCvY
Diffstat (limited to 'packages/meshbay-node/src/meshbay_node/daemon.py')
-rw-r--r--packages/meshbay-node/src/meshbay_node/daemon.py49
1 files changed, 23 insertions, 26 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/daemon.py b/packages/meshbay-node/src/meshbay_node/daemon.py
index 3a024f0..7341423 100644
--- a/packages/meshbay-node/src/meshbay_node/daemon.py
+++ b/packages/meshbay-node/src/meshbay_node/daemon.py
@@ -37,7 +37,7 @@ from pathlib import Path
import uvicorn
from meshbay_common import MNP_VERSION
-from meshbay_common.protocol import MNP, index_entry_wire
+from meshbay_common.protocol import MNP
from meshbay_node.audit import AuditStore
from meshbay_node.bundle_store import BundleStore
from meshbay_node.chat.store import ChatStore
@@ -58,6 +58,7 @@ from meshbay_node.transport import (
QUIC_AVAILABLE,
WEBRTC_AVAILABLE,
)
+from meshbay_node.transport.wire import index_delta_message, index_sync_message
if QUIC_AVAILABLE:
from meshbay_node.transport import QuicChunkServer
@@ -1003,6 +1004,13 @@ class NodeDaemon:
def _push_index_progress(self, group_id: str, progress) -> None:
if not self._webrtc:
return
+ # Deliberately NOT sealed, unlike index_sync/index_delta (decision D3).
+ # Counters only — never a path, never a filename, see IndexProgress in
+ # indexer.py — pushed every couple of seconds for the whole length of a
+ # scan. Sealing it would buy an attacker's rough estimate of a library's
+ # size and cost a key derivation and a decrypt per push. If a field that
+ # names anything is ever added here, that trade is void and this message
+ # joins the other two.
msg = {
"type": MNP.INDEX_PROGRESS,
"v": MNP_VERSION,
@@ -1135,36 +1143,25 @@ class NodeDaemon:
# 11.5 — Push to connected WebRTC peers in this group
if self._webrtc:
- if delta is not None:
- msg = {
- "type": MNP.INDEX_DELTA,
- "v": MNP_VERSION,
- "group_id": idx.group_id,
- "base_version": delta.base_version,
- "version": delta.version,
- "additions": [index_entry_wire(e) for e in delta.additions],
- "deletions": delta.deletions,
- "updates": [index_entry_wire(e) for e in delta.updates],
- }
- else:
- msg = {
- "type": MNP.INDEX_SYNC,
- "v": MNP_VERSION,
- "group_id": idx.group_id,
- "version": idx.version,
- "entries": [index_entry_wire(e) for e in idx.entries],
- }
- pushed = 0
- for session in list(self._webrtc._sessions.values()):
- if session._group_id == group_id:
+ peers = [s for s in list(self._webrtc._sessions.values())
+ if s._group_id == group_id]
+ # Both messages are sealed under a GEK-derived subkey, so building one
+ # needs a key. A group without one has no peers to push to either — the
+ # node refuses every handshake while the GEK is None (NS8) — so this is
+ # "nobody is listening", not a case to send in clear for.
+ if peers and idx.gek:
+ msg = (index_delta_message(idx, delta) if delta is not None
+ else index_sync_message(idx, indexer.roots))
+ pushed = 0
+ for session in peers:
try:
session._send(msg)
pushed += 1
except Exception:
pass
- if pushed:
- log.info("Index %s pushed to %d WebRTC peers",
- "delta" if delta is not None else "sync", pushed)
+ if pushed:
+ log.info("Index %s pushed to %d WebRTC peers",
+ "delta" if delta is not None else "sync", pushed)
# 11.9 — Register file hashes with hub swarm table (public groups only, H7)
group_cfg = next(