diff options
Diffstat (limited to 'packages/meshbay-common/src/meshbay_common/senderkeys.py')
| -rw-r--r-- | packages/meshbay-common/src/meshbay_common/senderkeys.py | 51 |
1 files changed, 38 insertions, 13 deletions
diff --git a/packages/meshbay-common/src/meshbay_common/senderkeys.py b/packages/meshbay-common/src/meshbay_common/senderkeys.py index 932e2e6..9ad5107 100644 --- a/packages/meshbay-common/src/meshbay_common/senderkeys.py +++ b/packages/meshbay-common/src/meshbay_common/senderkeys.py @@ -1,21 +1,40 @@ """ -MeshBay — Sender Keys protocol for group messaging. +MeshBay — Sender Keys protocol. **Not used by group chat. Not used at all.** -Signal Groups approach: each member maintains their own sending chain. -Advantages over shared Double Ratchet: - - O(N) state per group (one chain per member) vs O(N^2) pairwise - - Single encrypt per message (not N encryptions) - - No key/nonce reuse — each sender has an independent chain +Kept the way `ratchet.py` is kept: a working implementation of a protocol that +may earn a place in a future 1:1 DM, where there is no server-side history to +contradict it. Group chat is `chatbox.py`, and the decision to build that +instead is `docs/chat-sender-keys.md` §4 (operator, 2026-09-07). Do not read a +green test run here as evidence that group chat is encrypted; nothing in +production imports this module. -Key components: - - Chain key ratchet: HKDF per message, provides forward secrecy +**Why it is not what group chat uses.** With sender keys distributed under the +group key, and a node that serves history to devices which were not present when +a message was sent, the node must retain and hand out each chain's *earliest* +key — and a chain key at iteration *i* yields every message key from *i* onward +by pure HKDF. Forward secrecy is then zero, and what is left is a large amount +of stateful client code whose failure modes are silent. Three of them are real +and reproduced in the design document: + + * `GroupSenderKeyStore.add_sender` accepts any distribution for any + `sender_id` and overwrites what is there, and `SenderKeyRecord.create` + invents a signing key bound to nothing — so under group-key distribution any + member can replace another member's chain and sign as them (F1); + * a second device registering under one `sender_id` drops the first device's + chain, and its messages then fail signature verification rather than failing + visibly at registration (F2); + * `SenderKeyState.advance_to` caches every skipped message key and nothing + trims `_skipped_keys` (F3). + +They are findings about a module nothing calls, and are deliberately not fixed +here. Anyone bringing this back for 1:1 DM must fix all three first — and must +bind the distribution to a key the node pinned, which is what F1 is really about. + +Key components, as implemented: + - Chain key ratchet: HKDF per message - Message key derivation: separate HKDF from chain key - Ed25519 signing: each sender signs their ciphertext - AES-256-GCM encryption: browser-compatible symmetric cipher - -Key distribution: - - On join: admin wraps each sender's SenderKeyDistribution with GEK - - On leave: all remaining members rotate their chain keys """ import os @@ -169,7 +188,13 @@ class SenderKeyRecord: # ── Group store ────────────────────────────────────────────────────────────── class GroupSenderKeyStore: - """All sender key states for one group, held by one member.""" + """All sender key states for one group, held by one member. + + One chain per **device**, were this ever used: a shared per-person chain + advanced by two devices produces key and nonce reuse, which is `first- + review.md` C1 one level down. `add_sender` does not enforce that — see the + module docstring, F2. + """ def __init__(self, group_id: str): self.group_id = group_id |