summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-07 17:50:28 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-07 17:50:28 +0200
commit36cebf25d0e0f24cf63be4380ccb5d03da726a74 (patch)
tree8509ec4cf68a058f7383299e11bdea97ab06cadf /packages/meshbay-hub/src/meshbay_hub/static/locales/en.js
parent8883d60d0afa2ed9dd1ef68bc21fe1b9a65a59ff (diff)
downloadmeshbay-36cebf25d0e0f24cf63be4380ccb5d03da726a74.tar.gz
feat(chat): encrypt group chat under per-device epoch keys (MNP 2.0)
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TZZxYjz8YeWRz13xDi8LJr
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/locales/en.js')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/en.js17
1 files changed, 17 insertions, 0 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js
index 6dbb70f..996d5b6 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js
@@ -640,6 +640,9 @@ export default {
'settings_app.chat_no_writable_root': 'This group has no read-write directory, so attachments are off.',
'settings_app.chat_link_preview_label': 'Link previews',
'settings_app.chat_link_preview_hint': 'When a member posts a link, the node fetches the page\'s title and image. That is a request from your machine to a site somebody else chose.',
+ 'settings_app.chat_encrypted_always': 'Chat in this group is always encrypted. It cannot be turned off.',
+ 'settings_app.chat_rotate_epoch': 'Move the chat key on',
+ 'settings_app.chat_rotate_epoch_hint': 'Members keep reading the whole history. Anyone removed from the group cannot read what is written from now on. Removing a member or a device already does this by itself.',
'settings_app.tmdb_token_prompt': 'Sign up on TMDB to generate your own API key.',
'settings_app.tmdb_token_link': 'Get a key',
'settings_node.roots_offline_hint': 'Not connected to the node — changes go through the local node instead, and take effect on its next reload.',
@@ -760,6 +763,20 @@ export default {
'chat.unread': 'New messages',
'chat.jump_latest': 'Jump to latest',
'chat.jump_new': 'New messages',
+ // A message this client could not open. Each one names what actually went
+ // wrong, because "could not read this message" sends an operator nowhere:
+ // a signature failure is somebody impersonating a member, a missing epoch key
+ // is a device that joined after the message was written, and a decryption
+ // failure is neither.
+ 'chat.unreadable': 'This message could not be read',
+ 'chat.unreadable_signature': 'Unverified message — the signature does not match the sender',
+ 'chat.unreadable_epoch': 'Written before this device could read this conversation',
+ 'chat.unreadable_keys': 'Chat keys unavailable — reconnect to read this',
+ 'chat.unreadable_decrypt': 'This message could not be decrypted',
+ 'chat.unreadable_envelope': 'This message arrived incomplete',
+ 'chat.unreadable_format': 'This message needs a newer version of MeshBay',
+ 'chat.encrypted_needs_newer': 'This conversation is encrypted and this client cannot read it — update MeshBay',
+ 'chat.encrypted_cannot_send': 'This conversation is encrypted and this device cannot post to it yet',
'group.leave': 'Leave group',
'group.leave_confirm': 'Leave “{name}”? You will lose access to its files and chat. Files you uploaded stay on the node, and the node keeps the identity it pinned for you until its operator removes it.',
'group.mute': 'Mute notifications',