aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-common/src/meshbay_common/__init__.py
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-07 18:03:52 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-07 18:03:52 +0200
commite1383e1d545b994f4ad61694f868339defb0bdef (patch)
tree67a3b1933f2a9c5caf107d01d9ff91c44a975df6 /packages/meshbay-common/src/meshbay_common/__init__.py
parent36cebf25d0e0f24cf63be4380ccb5d03da726a74 (diff)
parent8980a8e42d94ab7c0bc9739283d39f938f8402b0 (diff)
downloadmeshbay-e1383e1d545b994f4ad61694f868339defb0bdef.tar.gz
Merge origin/main into the chat encryption work
Both sides landed a breaking MNP change and both called it 2.0, which is right: the sealed upload, the removal of `stream_seg` and mandatory chat encryption share one flag day. They are recorded as one version in `__init__.py` rather than as a race between two. The resolutions that were decisions rather than mechanics: * **`MNP_MIN_SUPPORTED` moves to "2.0".** The sealed upload alone was a *confined* break — a 1.x peer could still connect, browse, download, stream and chat, with only its uploads refused by `upload_not_sealed` — so the floor deliberately stayed at "1.0". Mandatory chat encryption ends that confinement: a 1.x peer can neither produce a sealed chat message nor read one, so it would connect, look fine, and be unable to say anything. Refusing it at the handshake is the honest form. The per-message `upload_not_sealed` path is untouched and still right if the floor is ever lowered. * **`sendChat` throws on an `error` reply**, from origin, applied to the sealed send. It matters more after this change, not less: the node now refuses a stale epoch, a malformed envelope and a device claim that is not the connection's own, so there are three new ways for a message to be rejected and none of them may look like a message that was sent. * **`req_id` supersedes the per-type routing** this branch added for `chat_keys_resp` and `device_hello_ack`. Both blocks are kept beside the existing `chat_hist_resp` one, for the same stated reason — a node too old to stamp — and their comments no longer claim to be the mechanism that closes the class. `req_id` is. * **`chat_send_probe.py` is rebuilt on origin's structure**, not beside it: two scenarios, a stub that stamps `req_id`, `music_meta_req` as the older pending request. The encrypted path is layered on — a real Ed25519 device key generated in the page, and a `chat_keys_resp` sealed by the shipped Python, because a payload the page built itself would prove only that the page agrees with the page. * **`test_reply_correlation.py` now sends a sealed message.** Its subject is which of the two messages leaving that handler carries the id; plaintext chat was only the fixture, and the node refuses one now. * `groupbox` keeps both new purposes (`upload`, `chat_keys`); `protocol.py` keeps origin's removal of `STREAM_SEGMENT` and this branch's correction of the "Double Ratchet message" comment on `CHAT_MESSAGE`, which was wrong when it was written and is wrong differently now. Full suite on the merged tree: 1993 passed, 11 failed — the same 11 that fail on a pristine checkout (2 Windows service tests, 1 apps-enabled policy, 7 transcode tests that pass in isolation, and the WebRTC invite test that hangs on its own). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TZZxYjz8YeWRz13xDi8LJr
Diffstat (limited to 'packages/meshbay-common/src/meshbay_common/__init__.py')
-rw-r--r--packages/meshbay-common/src/meshbay_common/__init__.py53
1 files changed, 41 insertions, 12 deletions
diff --git a/packages/meshbay-common/src/meshbay_common/__init__.py b/packages/meshbay-common/src/meshbay_common/__init__.py
index ca4c6eb..cb6ad15 100644
--- a/packages/meshbay-common/src/meshbay_common/__init__.py
+++ b/packages/meshbay-common/src/meshbay_common/__init__.py
@@ -90,19 +90,48 @@ __version__ = "0.11.0"
# makes the *next* breaking change cost a refusal message instead of a second
# flag day. `MNP_MIN_SUPPORTED` in `handshake.py` is the other half.
#
-# **2.0 (2026-09-07): chat is encrypted, and there is no way to turn it off.**
-# A MAJOR bump because it is a real break: a 1.x peer cannot produce a sealed
-# chat message and cannot read one, so it is refused at the handshake with
-# `version_too_old` rather than connecting and then failing to speak. Expressing
-# the break in the version is what makes it a stated refusal instead of a
-# conversation that silently does not work — `MNP_MIN_SUPPORTED` moves with it.
+# **2.0 (2026-09-07): the write path is sealed, chat is encrypted, and the last
+# unencrypted content message is gone.** Three changes that landed together and
+# share one version, because they share one flag day.
#
-# There is deliberately no per-group switch. Every node in existence is a test
-# node, so an opt-in flag would buy nothing and cost a compatibility path to
-# maintain; existing node data is migrated by `QE/migrate-chat-encryption.py`.
+# - `file_upload` and `file_upload_ack` travel sealed under a GEK-derived
+# subkey (`groupbox.PURPOSE_UPLOAD`). The filename, the destination folder
+# and the bytes all ride inside the seal; `upload_id` — a fresh
+# client-chosen correlation id — and `chunk_index` stay in clear because
+# the node routes and orders on them. `filename` used to be the
+# correlation key and cannot be any more, which is what forced `upload_id`.
+# - `chat_msg` is sealed under a per-device subkey of the group's chat epoch
+# key (`meshbay_common.chatbox`) and signed over the ciphertext with the
+# device key the node pinned. There is deliberately **no per-group switch**:
+# every node in existence is a test node, so an opt-in flag would buy
+# nothing and leave a plaintext branch reachable. Existing node data is
+# migrated by `QE/migration/migrate_chat_encryption.py`.
+# - `stream_seg` is **removed**. It answered with an MPEG-TS segment as
+# base64 with no encryption at all, on both transports, to any
+# authenticated member — the one content-plane message that never went
+# through a GEK-derived key. `stream_data` has done the job properly since
+# Phase 12, and `fetchStreamSegment`, its only browser caller, was defined
+# and never once invoked.
#
-# The index at rest, `index_progress` (counters only, never a path — see
-# `groupbox.py` and daemon.py `_push_index_progress`), and file content on the
-# operator's disk are all deliberately unchanged.
+# **Breaking, on the wire every deployed client speaks**, and MAJOR by the same
+# rule 1.0 was.
+#
+# **`MNP_MIN_SUPPORTED` moves to "2.0" with it, and that is a change of plan
+# worth reading.** The sealed upload alone was a *confined* break: a 1.x peer
+# could still connect, browse, download, stream and chat, and only its uploads
+# were refused — so the floor stayed at "1.0" and the refusal was per message
+# (`upload_not_sealed`). Mandatory chat encryption ends that confinement. A 1.x
+# peer can neither produce a sealed chat message nor read one, so it would
+# connect, appear to work, and then be unable to say anything or read anything
+# anyone else said. Refusing it at the handshake with `version_too_old` and a
+# sentence saying so is the honest form: a stated refusal is a bug report, a
+# chat that quietly does not work is a support case. The per-message
+# `upload_not_sealed` path stays, unchanged — it is still the right answer if
+# the floor is ever lowered again.
+#
+# Still deliberately in clear, and none of it is content: the handshake itself,
+# `index_progress` (counters only — see daemon.py `_push_index_progress`), the
+# admin and configuration acks, and the media-metadata replies. The index at
+# rest and file content on the operator's disk are unchanged.
MNP_VERSION = "2.0"
MHP_VERSION = "0.1"