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 +142 -0
- package/README.md +1 -1
- package/docs/ARCHITECTURE.md +127 -4
- package/docs/PROTOCOL.md +556 -0
- package/docs/design/sender-keys-on-relay.md +118 -0
- package/package.json +2 -2
- package/src/client/ChatController.js +461 -60
- package/src/crypto/SenderKey.js +201 -10
- package/src/p2p/P2PChatController.js +2 -0
- package/src/protocol/capabilities.js +74 -0
- package/src/protocol/messages.js +50 -4
- package/src/protocol/validators.js +73 -1
- package/src/server/MessageRouter.js +9 -0
- package/src/server/SessionManager.js +3 -1
- package/src/server/WebSocketServer.js +94 -4
- package/src/shared/constants.js +35 -0
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
|
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -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
|