summaryrefslogtreecommitdiffstats
path: root/packages
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-10 17:30:00 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-10 17:30:00 +0200
commitefc93c187dba9027292b51ff0e9caa29349c953e (patch)
tree9e5a397a6ddb5cd52279861750f70357dfb1047a /packages
parent4753c67816c774323e3ab4efc76d3259e8ded40d (diff)
downloadmeshbay-efc93c187dba9027292b51ff0e9caa29349c953e.tar.gz
refactor(node): QUIC does not relay chat it cannot check
The QUIC handler stored `payload` as it arrived and broadcast it: no envelope, no signature check, no `device_hello` to check one against. A message reaching a group's archive that way is a plaintext row in an encrypted history, and it would be indistinguishable from one somebody actually wrote. Removed rather than gated. The transport implements neither the per-device sealing nor the device identification the WebRTC path requires, so refusing here would mean maintaining a second, weaker set of rules for a transport with no client; an unimplemented type is logged and dropped, like every other message this transport does not have. The comment on the peer registry loses its chat fan-out aside for the same reason. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AsoWC3GmhNdwVFomW3QjH3
Diffstat (limited to 'packages')
-rw-r--r--packages/meshbay-node/src/meshbay_node/transport/quic_server.py66
1 files changed, 14 insertions, 52 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/transport/quic_server.py b/packages/meshbay-node/src/meshbay_node/transport/quic_server.py
index e34153c..7b9d094 100644
--- a/packages/meshbay-node/src/meshbay_node/transport/quic_server.py
+++ b/packages/meshbay-node/src/meshbay_node/transport/quic_server.py
@@ -235,8 +235,14 @@ class _MNPServerProtocol(QuicConnectionProtocol):
self._do_index_sync_sync(stream_id)
elif mtype == MNP.FILE_REQUEST:
self._do_file_request_sync(stream_id, msg)
- elif mtype == MNP.CHAT_MESSAGE:
- self._do_chat_message_sync(stream_id, msg)
+ # No chat here. A message is sealed under a per-device subkey and
+ # signed by the device the *connection* proved it is, and this
+ # transport implements neither `device_hello` nor the envelope
+ # checks that go with it. Relaying one unchecked would put a
+ # plaintext row in the group's archive, which is the one thing chat
+ # encryption is for; an unimplemented type is logged and dropped
+ # instead, which is what every other unimplemented message here
+ # does.
elif mtype == MNP.PING:
# Liveness is transport-agnostic, and a native client over QUIC
# has the same half-open problem a DataChannel does.
@@ -389,12 +395,12 @@ class _MNPServerProtocol(QuicConnectionProtocol):
task.add_done_callback(self._tasks.discard)
def _peer_registry(self) -> dict:
- """QUIC peers for THIS connection's group, keyed per group so a message
- never crosses into another group on a multi-group node (findings M2b /
- H1). Deliberately separate from the WebRTC registry that also lives in
- the group context: the two transports' session objects have different
- `_send` signatures, and cross-transport chat fan-out is not wired (no
- QUIC client ships yet)."""
+ """QUIC peers for THIS connection's group, keyed per group so nothing a
+ node pushes can cross into another group on a multi-group node.
+
+ Deliberately separate from the WebRTC registry that also lives in the
+ group context: the two transports' session objects have different
+ `_send` signatures, so nothing may iterate both as one set."""
return self._group_ctx().setdefault("_quic_peers", {})
def _do_index_sync_sync(self, stream_id: int) -> None:
@@ -421,50 +427,6 @@ class _MNPServerProtocol(QuicConnectionProtocol):
ctx["gek"], file_path, chunk_index, file_hash, entry.id)
self._send(stream_id, chunk_data)
- def _do_chat_message_sync(self, stream_id: int, msg: dict) -> None:
- """
- Store a chat message and broadcast it to the rest of THIS group.
-
- `sender_id` is the authenticated session's, never the wire's — a peer
- must not be able to post as someone else (NS6 / finding M2a). The store
- and the peer set come from the group context, not a connection-global
- one, so a message never crosses into another group on a multi-group node
- (findings M2b / H1). The WebRTC path has done both since Phase 11.5.
- """
- gctx = self._group_ctx()
- payload = msg.get("payload", b"")
- if isinstance(payload, str):
- payload = payload.encode()
-
- chat_store = gctx.get("chat_store")
- if chat_store:
- self._spawn(chat_store.save_message(
- sender_id=self._user_id,
- iteration=msg.get("iteration", 0),
- payload=payload,
- thread_id=msg.get("thread_id"),
- ))
-
- broadcast = {
- "type": MNP.CHAT_MESSAGE,
- "v": MNP_VERSION,
- "sender_id": self._user_id,
- "iteration": msg.get("iteration", 0),
- "payload": msg.get("payload", ""),
- "thread_id": msg.get("thread_id"),
- "group_id": self._group_id or "",
- }
- # Per connection, not per account — see the WebRTC path and
- # docs/chat-sender-keys.md F7. A person's other devices are recipients.
- for proto in list(self._peer_registry().values()):
- if proto is not self:
- try:
- proto._send(0, broadcast)
- except Exception:
- pass
-
- self._send(stream_id, {"type": "ack", "v": MNP_VERSION})
-
def connection_lost(self, exc) -> None:
if self._user_id:
self._peer_registry().pop(self._registry_key, None)