<feed xmlns='http://www.w3.org/2005/Atom'>
<title>meshbay.git/packages/meshbay-node/src/meshbay_node/chat, branch 0.14</title>
<subtitle>MeshBay — read-only public mirror</subtitle>
<id>https://git.meshbay.org/meshbay.git/atom?h=0.14</id>
<link rel='self' href='https://git.meshbay.org/meshbay.git/atom?h=0.14'/>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/'/>
<updated>2026-09-10T15:30:22Z</updated>
<entry>
<title>refactor(common): delete the sender-key implementation nothing uses</title>
<updated>2026-09-10T15:30:22Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-10T15:30:22Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=6cf21a019963468cb853e5c763ef0097115efa46'/>
<id>urn:sha1:6cf21a019963468cb853e5c763ef0097115efa46</id>
<content type='text'>
`senderkeys.py` and its 13 tests implemented Signal-style sender keys, and
production has never called them: chat is a key per group, per epoch, per
device, derived by name. The reasoning that ruled the ratchet out stays where it
belongs — in `chatbox.py`, at the top of the module that replaced it — because
the argument is the useful part, and it now stands on its own instead of
pointing at a file to compare against.

Kept code that nothing calls is worse than absent code: it reads as an
alternative somebody may reach for, and it has to be maintained past every
refactor to stay compiling, which is maintenance spent on a decision already
made.

The three comments naming `GroupSenderKeyStore` are rewritten to say the thing
they were illustrating.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01AsoWC3GmhNdwVFomW3QjH3
</content>
</entry>
<entry>
<title>feat(chat): encrypt group chat under per-device epoch keys (MNP 2.0)</title>
<updated>2026-09-07T15:50:28Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-07T15:50:28Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=36cebf25d0e0f24cf63be4380ccb5d03da726a74'/>
<id>urn:sha1:36cebf25d0e0f24cf63be4380ccb5d03da726a74</id>
<content type='text'>
Chat messages are sealed with AES-256-GCM under a key derived per group, per
epoch, per *device*, and signed over the ciphertext with the device key the
node pinned. The node relays and archives; it cannot read a message.

There is no switch. MNP goes to 2.0 and MNP_MIN_SUPPORTED moves with it, so a
1.x peer is refused at the handshake with `version_too_old` rather than
admitted and then unable to speak. An opt-in flag was designed and rejected:
every node is a test node, so it would have bought nothing and left a plaintext
branch reachable — C6's lesson one feature later. A test reads the source and
refuses any code that consults a `chat_encrypted` setting.

Not Sender Keys, and `senderkeys.py` is now documented as unused. With
distribution under the group key and a node that serves history to devices
which were not present, the node must retain each chain's earliest key, and a
chain key at iteration i yields every message key from i on by pure HKDF —
forward secrecy is zero either way. What the ratchet was left buying was
stateful client code with silent failure modes, three of them reproduced: any
member could sign as any other, a second device dropped the first's chain, and
the skipped-key cache grew without bound. The reasoning is in
docs/chat-sender-keys.md, which is the specification and the decision record.

Epochs, not rotation: the epoch key is wrapped under the group key at delivery
and never stored under it, so `gek_rotate` is a re-wrap. A group-key-derived
archive key would have made every message ever sent unreadable on the first
`member unpin`, which is the documented step after removing a member. A new
epoch opens on member revoke/unpin, device revoke and `gek_rotate`; old epochs
are kept and still delivered, so history stays readable to everyone who could
already read it, and nothing anywhere deletes one.

Three prerequisites this needed, each a live defect on its own:

* The peer registry was keyed by user_id, so one account's second device
  evicted the first and the broadcast skipped recipients by account — a
  person's phone never saw what they typed on their laptop.
* The handshake authenticated an account, never a device. `device_hello`
  (additive, signed, refused unless the key is a live device of this account in
  the node's own roster) is what lets the node refuse a member claiming
  somebody else's key.
* `_admin_exec_file_delete` authorized against the exact uploading key, so
  device linking had already broken deleting your own file from your other
  device. It now authorizes against any non-revoked device of `uploader_id`.

Found by driving the real panel over the real transport, not by reading source:
`chat_keys_resp` was routed by arrival order and handed to an unanswered
`media_meta_req` — the original frozen-tab defect in a message type that did
not exist when that probe was written. And `_asText` had been deleted with an
unrelated helper beside it; its only caller sits inside a promise the panel
catches, so every conversation rendered empty with nothing in the console.

Existing node data is migrated by QE/migration/migrate_chat_encryption.py
(not versioned, per the QE rule), run with the node stopped.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01TZZxYjz8YeWRz13xDi8LJr
</content>
</entry>
<entry>
<title>feat(common): MNP 0.2 — liveness, and chat history read the way it is written</title>
<updated>2026-08-16T13:28:53Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-16T13:28:53Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=fd770dc6293be67298f582de5800f2ca6fe24a8b'/>
<id>urn:sha1:fd770dc6293be67298f582de5800f2ca6fe24a8b</id>
<content type='text'>
`get_messages` pages forward from the oldest message. That is the right shape
for "what happened since I last looked" and the wrong one for opening a
conversation, and the browser asked it for `since=0, limit=200` — so a group
with more than two hundred messages showed its first two hundred and the
exchange anyone came for was unreachable. Demonstrated on 300 messages: the
newest was simply absent from the answer.

`get_recent` and `get_before` page backwards, cursored on the row id rather than
the timestamp. Nothing makes a `time.time()` float unique, and a cursor on a
value two rows can share eventually skips a message or repeats it.

PING/PONG covers liveness on an already-open channel: a DataChannel whose peer
vanished without closing still reads as connected, and nothing noticed until a
real request hung. It is not a discovery mechanism — opening a connection to
ping costs a full ICE/DTLS handshake, measured at 0.6-7 s across two ISPs — so
presence in the group list comes from the hub's registry instead.

Both additions are backward compatible: an 0.1 peer sends no `before` and is
answered with the newest page, which is what it wanted.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>fix(ui): upload chunk size, cached file display, chat names, file delete, inline thumbnails</title>
<updated>2026-08-11T14:12:16Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-11T14:12:16Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=b50750466622dd6cda0fc084d39dfce000ad0081'/>
<id>urn:sha1:b50750466622dd6cda0fc084d39dfce000ad0081</id>
<content type='text'>
- 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 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>feat: Phase 7 — Node v2 (multi-group, Sender Keys, 0-RTT, chat, denylist)</title>
<updated>2026-08-10T01:07:56Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-10T01:07:56Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=4b3e8c3b8b9d10c8ac333dd8db614a7569052472'/>
<id>urn:sha1:4b3e8c3b8b9d10c8ac333dd8db614a7569052472</id>
<content type='text'>
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 &lt;noreply@anthropic.com&gt;
</content>
</entry>
</feed>
