ciphermesh 2.10.0 → 2.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,148 @@
3
3
  Notable changes per release. Older versions are reconstructed from the git
4
4
  history — the commit bodies and pull requests remain the fuller record.
5
5
 
6
+ ## 2.12.0
7
+
8
+ Sender keys now send. 2.11.0 shipped the half that reads a group message; this
9
+ is the half that writes one, and a line in a room of fifty costs one encryption
10
+ instead of fifty.
11
+
12
+ The switch is unchanged and still strict — every member of the room and the hub
13
+ itself must say they can handle it. On a public hub the per-peer path remains
14
+ the common case, and it is untouched.
15
+
16
+ ### Added
17
+
18
+ - **Group sending on the relay.** 2.11.0 shipped the half that reads one; this
19
+ is the half that writes one. A line in a room of fifty cost fifty encryptions
20
+ and fifty envelopes — against the 1 MiB/s byte budget, a padded message to
21
+ fifty people throttled the sender for saying one thing. It now costs one of
22
+ each.
23
+
24
+ The switch is still the strict one: every member of the room advertises
25
+ `sk1`, **and** the hub does, **and** the message is not deniable. Any one
26
+ false and the per-peer path runs exactly as before, which on a public hub is
27
+ the common case rather than the exception. Nothing was removed.
28
+
29
+ Deniable messages stay pairwise permanently. Deniability comes from a key both
30
+ sides could have derived; a group packet is signed by one sender, which is the
31
+ opposite claim.
32
+
33
+ - **Sender keys rotate on every membership change.** Leaving, switching rooms,
34
+ disconnecting, being kicked, being banned. A chain ratchets forward, so the
35
+ copy a member holds opens every message after it: removing someone stopped the
36
+ relay delivering to them and did not stop them reading. Rotation is what
37
+ closes that, and `test/guarantees.test.js` now asserts it from outside the
38
+ code that implements it.
39
+
40
+ ### Fixed
41
+
42
+ - **A kick or a ban is announced as a departure, not only as a notification.**
43
+ Both moved the target out of the room and told the room `peer_kicked`, which
44
+ carries a nickname — and a nickname is not something a client can unwind a
45
+ member by, since `/nick` reassigns them. Every remaining client kept the
46
+ removed peer in its roster indefinitely. They now emit `peer_left` like every
47
+ other way out of a room, which is also what makes rotation reachable for the
48
+ two cases where it matters most.
49
+
50
+ - **Peer capabilities survive a room switch.** `room_changed` and `room_joined`
51
+ carried them and the client dropped them, so after switching rooms every peer
52
+ looked incapable and the room silently never turned the group path on. No
53
+ error, no failure — just an optimisation that quietly never applied.
54
+
55
+ - **Sender-key memory is released, not merely zeroed.** `sodium_malloc` pages are
56
+ `mlock`'d, and an operating system caps how much a process may lock at once.
57
+ Zeroing a spent key left its pages locked until the garbage collector happened
58
+ to run the buffer's finaliser, so a long session derived keys faster than it
59
+ released them. The ceiling is generous on macOS and small on Linux, where a
60
+ busy client would eventually have hit an allocation failure that aborts the
61
+ process rather than returning an error.
62
+
63
+ ## 2.11.0
64
+
65
+ Groundwork for sender keys on the relay. **Nothing sends a group message yet** —
66
+ this release is the half that has to be in the field first, and the reason is in
67
+ "Added" below.
68
+
69
+ ### Added
70
+
71
+ - **Capability negotiation in `JOIN`.** `PROTOCOL_VERSION` is checked for exact
72
+ equality, so it can only say "same" or "refuse to talk". It cannot express
73
+ _newer, but still willing to speak the old way_, which is the only thing that
74
+ makes a protocol change survivable on a public hub where people upgrade
75
+ whenever they upgrade.
76
+
77
+ A client now lists what it can do in its `JOIN`; the relay validates the list,
78
+ stores it, and hands it on verbatim with the peer list, without ever acting on
79
+ it. A feature turns on only when **every member of the room** advertises it, so
80
+ one peer on an older build holds the whole room on the old path — which is the
81
+ outcome that keeps a half-upgraded room readable.
82
+
83
+ The relay advertises its own abilities separately, in `join_ack.serverCaps`. No
84
+ client can promise those on the relay's behalf, and some features need the
85
+ relay to play along, so a capable room sitting on an older hub is still not a
86
+ room that can switch paths.
87
+
88
+ - **Receiving group messages, and the relay fan-out that carries them.** The
89
+ relay path seals one envelope per peer, so a line in a room of fifty costs
90
+ fifty encryptions and fifty envelopes — against a 1 MiB/s byte budget, a padded
91
+ message to fifty people throttles the sender for saying one thing. Sender keys
92
+ make it one of each.
93
+
94
+ Reading ships a release ahead of writing on purpose. The switch is _every
95
+ member agrees_: if both arrived together it would only ever be true in rooms
96
+ where everybody upgraded in the same moment.
97
+
98
+ A room-addressed message carries no `to`, because there is no single recipient,
99
+ and no `from`, because the relay must not become the one place on this wire
100
+ that asserts who is speaking. A packet names its _chain_ instead — an opaque
101
+ label members resolve through the sender key they were handed over the pairwise
102
+ channel, and which the relay cannot invert.
103
+
104
+ - **Per-sender signatures on group messages.** A sender chain is symmetric: every
105
+ member holds the key that decrypts a given sender, and can therefore also
106
+ produce ciphertext on it. Without something asymmetric on top, "Alice said
107
+ this" only ever meant "somebody in this room said this" — tolerable in a small
108
+ P2P mesh, not on a public hub where `general` has no owner and no admission
109
+ control.
110
+
111
+ Each sender now holds an Ed25519 keypair for the life of its chain, rotated
112
+ with it. Recipients verify before touching the ratchet, so an unauthenticated
113
+ packet carrying a large counter cannot make them derive and cache a thousand
114
+ message keys.
115
+
116
+ - **`docs/PROTOCOL.md`.** The wire format field by field, with the limits, the
117
+ failure modes, and the reason attached wherever the relay is forbidden to do
118
+ something. It also states what the relay _does_ learn, because a specification
119
+ that lists only the protections is misleading.
120
+
121
+ ### Changed
122
+
123
+ - **Room-addressed messages are never queued for absent members**, unlike
124
+ unicast. Not policy — arithmetic: a sender key handed over on someone's return
125
+ serialises the chain at its current counter, so the backlog is unreadable to
126
+ them whatever the relay does with it. Queueing would store ciphertext on the
127
+ relay that provably nobody can open.
128
+
129
+ ### Internal
130
+
131
+ - **Frozen test vectors for sender keys.** The previous tests round-tripped a
132
+ sender against a receiver built from that same sender — a mirror, not a
133
+ reference, which agrees with itself however the KDF is defined. Swapping the
134
+ two domain tags left the old suite fully green while making two clients unable
135
+ to read each other. The vectors pin the absolute values, and moving them is a
136
+ protocol version bump rather than a regeneration.
137
+
138
+ - **A guarantees test that lives apart from the features it checks.** In August a
139
+ squash reverted the entire plugin-approval control and nothing failed, because
140
+ that pull request's tests were reverted in the same commit. `test/guarantees.test.js`
141
+ asserts what the project promises — through the surfaces a user goes through,
142
+ and deliberately not next to any feature — so deleting a feature now leaves its
143
+ proof behind, failing. Verified by mutation rather than assumed.
144
+
145
+ - **Branch protection and CODEOWNERS.** `dev` and `master` now require CI, and
146
+ refuse force pushes and deletions.
147
+
6
148
  ## 2.10.0
7
149
 
8
150
  ### Added
package/README.md CHANGED
@@ -19,7 +19,7 @@
19
19
 
20
20
  **[ciphermesh.de](https://ciphermesh.de)** — the website, and the public hub anyone can join.
21
21
 
22
- **[🇧🇷 Leia em Português](README.pt-BR.md)** · [Setup Guide](docs/SETUP.md) · [Architecture](docs/ARCHITECTURE.md) · [Security Policy](SECURITY.md) · [Hub Terms](TERMS.md) · [Contributing](CONTRIBUTING.md)
22
+ **[🇧🇷 Leia em Português](README.pt-BR.md)** · [Setup Guide](docs/SETUP.md) · [Architecture](docs/ARCHITECTURE.md) · [Wire Protocol](docs/PROTOCOL.md) · [Security Policy](SECURITY.md) · [Hub Terms](TERMS.md) · [Contributing](CONTRIBUTING.md)
23
23
 
24
24
  <img src="docs/demo.svg" alt="CipherMesh terminal demo" width="720">
25
25
 
@@ -439,15 +439,21 @@ Defines the protocol's message types. All messages have:
439
439
  }
440
440
  ```
441
441
 
442
+ > The normative description of the wire format lives in
443
+ > **[docs/PROTOCOL.md](PROTOCOL.md)** — field by field, with the limits, the
444
+ > failure modes and what the relay is forbidden to do. This chapter is the tour;
445
+ > that document is the specification.
446
+
442
447
  Types:
443
448
  | Type | Direction | Description |
444
449
  |------|---------|-----------|
445
- | `join` | Client -> Server | The client wants to join (nickname + publicKey) |
450
+ | `join` | Client -> Server | The client wants to join (nickname + publicKey + optional `caps`, see 6.10) |
446
451
  | `join_ack` | Server -> Client | The server confirms the join (sessionId + peer list) |
447
452
  | `peer_joined` | Server -> Clients | A new peer joined (nickname + publicKey) |
448
453
  | `peer_left` | Server -> Clients | A peer left |
449
454
  | `key_exchange` | Client -> Server -> Client | Public key exchange between peers |
450
455
  | `encrypted_message` | Client -> Server -> Client | Encrypted message |
456
+ | `group_message` | Client -> Server -> Room | One ciphertext fanned out to a room's members (sender keys, see 6.11). No `to`, no `from` |
451
457
  | `change_room` | Client -> Server | Legacy single-room switch: leave every room, enter one (+ optional `roomAuthPk` when creating a private room) |
452
458
  | `room_changed` | Server -> Client | Room switch confirmed (+ `private` flag) |
453
459
  | `join_room` | Client -> Server | Multi-room: join an ADDITIONAL room, keeping current ones (same `roomAuthPk` option) |
@@ -514,10 +520,15 @@ export const FILE_CHUNK_SIZE = 49152; // 48KB
514
520
  "version": 1,
515
521
  "timestamp": 1739800000000,
516
522
  "nickname": "Alice",
517
- "publicKey": "base64(32 bytes da chave publica Curve25519)"
523
+ "publicKey": "base64(32 bytes da chave publica Curve25519)",
524
+ "caps": ["sk1"]
518
525
  }
519
526
  ```
520
527
 
528
+ `caps` is optional (see 6.10). It is omitted entirely when the client has
529
+ nothing to advertise, so the message is byte-identical to a pre-capability
530
+ client's.
531
+
521
532
  ### 5.2 Join ACK (server -> client)
522
533
 
523
534
  ```json
@@ -530,12 +541,17 @@ export const FILE_CHUNK_SIZE = 49152; // 48KB
530
541
  {
531
542
  "sessionId": "660e8400-e29b-41d4-a716-446655440001",
532
543
  "nickname": "Bob",
533
- "publicKey": "base64(chave publica do Bob)"
544
+ "publicKey": "base64(chave publica do Bob)",
545
+ "caps": ["sk1"]
534
546
  }
535
- ]
547
+ ],
548
+ "serverCaps": ["sk1"]
536
549
  }
537
550
  ```
538
551
 
552
+ `caps` is what each peer can do; `serverCaps` is what the relay itself can do.
553
+ Both are optional and both matter — see 6.11.
554
+
539
555
  ### 5.3 Encrypted Message (client -> server -> client)
540
556
 
541
557
  ```json
@@ -550,6 +566,63 @@ export const FILE_CHUNK_SIZE = 49152; // 48KB
550
566
 
551
567
  **Note (sealed sender, v2)**: The relay sees only `to` (for routing) and an opaque `sealed` blob. The sender (`from`) *and* the whole already-E2E-encrypted `payload` are sealed to the recipient's public key with libsodium `crypto_box_seal` — an anonymous box only the recipient can open. The relay therefore learns **neither who sent the message nor what's inside**, and it never stamps, stores, or logs a sender. The recipient opens the seal to recover `{ from, payload }`, then decrypts the payload as before. (See §10 for the exact — honest — guarantee and its limits.)
552
568
 
569
+ ### 5.3b Group Message (client -> server -> room)
570
+
571
+ ```json
572
+ {
573
+ "type": "group_message",
574
+ "version": 2,
575
+ "timestamp": 1739800001000,
576
+ "room": "general",
577
+ "keyId": "base64(16 random bytes labelling the sender's chain)",
578
+ "counter": 7,
579
+ "ciphertext": "base64(...)",
580
+ "nonce": "base64(24 bytes)",
581
+ "signature": "base64(64 bytes, Ed25519 over keyId|counter|ciphertext|nonce)"
582
+ }
583
+ ```
584
+
585
+ One ciphertext for the whole room, fanned out by the relay to every member
586
+ except the sender. Note what is **not** here: no `to`, because there is no single
587
+ recipient, and no `from`, because the relay must not be the thing that says who
588
+ is speaking — on this wire, identity only ever travels sealed.
589
+
590
+ Recipients pick the right sender chain with `keyId`, which they learned from the
591
+ `sk_dist` payload that member sent them over the pairwise channel. That envelope
592
+ authenticates the sender; the fan-out does not, and is not asked to. To the relay
593
+ `keyId` is a random string, and it already knows which socket sent the frame, so
594
+ it learns nothing from it.
595
+
596
+ **Why the signature is not optional.** A sender chain is symmetric: every member
597
+ holds the key that decrypts a given sender, which means every member can also
598
+ *produce* ciphertext on it. Without something asymmetric on top, "Alice said
599
+ this" only ever means "somebody in this room said this" — and `general` on a
600
+ public hub has no owner and no admission control, so that is a very wide set of
601
+ somebodies. Each sender therefore also holds an Ed25519 keypair for the life of
602
+ its chain; the public half travels in the distribution, over the pairwise channel
603
+ that already authenticates who sent it. Recipients verify **before** touching the
604
+ ratchet, so an unauthenticated packet carrying a large counter cannot make them
605
+ derive and cache a thousand message keys.
606
+
607
+ The relay checks only that the sender is a member of `room` — without that, one
608
+ connection could inject into every room on the hub at once, which the unicast
609
+ path cannot do because it needs a `sessionId` it could only have been told. It
610
+ cannot verify the signature and does not try: it holds no signing keys, and that
611
+ is the recipient's job.
612
+
613
+ **Not queued for absent members**, unlike the unicast path. A member who was away
614
+ could not read it anyway — a sender key handed over on their return serialises
615
+ the chain at its *current* counter, so anything sent while they were gone stays
616
+ shut. Queueing would store ciphertext on the relay that provably nobody can open.
617
+ The unicast queue survives because an envelope addressed to a peer *is* still
618
+ openable when they come back with the same key; that difference is what decides
619
+ it.
620
+
621
+ **Not yet sent by anything.** As of this release clients can *receive* a group
622
+ message; `#broadcastPayload` still seals one envelope per peer. Receive and
623
+ fan-out ship first so that when the send path lands, the rooms it negotiates
624
+ with can already read it.
625
+
553
626
  ### 5.4 Decrypted content (never travels in cleartext)
554
627
 
555
628
  After decrypting `payload.ciphertext`, the result is:
@@ -814,6 +887,56 @@ wrapper adds no metadata leak.
814
887
 
815
888
  ---
816
889
 
890
+ ### 6.11 Capability Negotiation
891
+
892
+ `PROTOCOL_VERSION` is checked for exact equality, so it can only say "same" or
893
+ "refuse to talk". It cannot express a client that is newer but still willing to
894
+ speak the old way — which is what rolling a protocol change through a public hub
895
+ needs, because people upgrade whenever they upgrade.
896
+
897
+ Capabilities fill that gap:
898
+
899
+ 1. A client lists what it can do in its `JOIN` (`caps: ["sk1"]`).
900
+ 2. The relay validates the list, stores it, and hands it on **verbatim** with
901
+ the peer list (`join_ack`) and with `peer_joined`. The relay never acts on a
902
+ capability itself.
903
+ 3. The relay advertises **its own** abilities separately, in `join_ack.serverCaps`.
904
+ No client can promise these on the relay's behalf, and some features need the
905
+ relay to play along — the group fan-out is its job, not a peer's. A capable
906
+ room sitting on an older hub is still not a room that can switch paths.
907
+ 4. A feature turns on only when **every** member of the room advertises it
908
+ (`roomSupports()` in `src/protocol/capabilities.js`) **and** the relay does
909
+ (`relaySupportsCapability()`). One peer on an older build, or one older hub,
910
+ holds the whole room on the old path.
911
+
912
+ **Compatibility:** the field is optional and omitted when empty, so a
913
+ pre-capability client's `JOIN` is unchanged, and an older relay that does not
914
+ know the field simply drops it — peers then see no capabilities and fall back,
915
+ which is the safe direction.
916
+
917
+ **Bounds:** the list arrives from a public hub, so it is capped at 16 entries of
918
+ up to 24 lowercase characters each. A malformed list gets the whole `JOIN`
919
+ rejected rather than filtered: the relay hands this to other clients, and
920
+ forwarding the good half of a bad list would make a peer look capable of
921
+ something it never claimed.
922
+
923
+ **What a hostile relay can do with it:** stripping capabilities forces the room
924
+ back onto the per-peer path, which is the status quo and reveals nothing new.
925
+ Adding one a peer never claimed makes senders encrypt in a form that peer cannot
926
+ read — denial of service, immediately visible, never a way to read plaintext.
927
+ The all-members rule is what keeps the damage on that side of the line.
928
+
929
+ | Capability | Advertised by | Meaning |
930
+ |---|---|---|
931
+ | `sk1` | client | I can **receive** a group message: I accept a sender key over the pairwise channel and can decrypt what that chain produces. |
932
+ | `sk1` | relay | I can fan a room-addressed `group_message` out to a room's members. |
933
+
934
+ Neither means "I send group messages" — nothing does yet. Receive and fan-out
935
+ are deliberately a release ahead of send, because the switch is *every member
936
+ agrees*: if the ability to read arrived with the ability to write, the switch
937
+ would only ever be true in rooms where everybody upgraded at the same moment,
938
+ which on a public hub is close to never. See `docs/design/sender-keys-on-relay.md`.
939
+
817
940
  ## 7. Handshake Protocol
818
941
 
819
942
  ### 7.1 Full Diagram