ciphermesh 2.13.0 → 2.14.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 +69 -0
- package/README.md +4 -3
- package/README.pt-BR.md +4 -3
- package/docs/ARCHITECTURE.md +321 -225
- package/docs/SETUP.md +18 -0
- package/docs/commands.json +2 -2
- package/package.json +2 -2
- package/src/client/ChatController.js +22 -4
- package/src/client/UI.js +617 -124
- package/src/client/keyboard.js +388 -0
- package/src/p2p/P2PChatController.js +21 -4
- package/src/shared/desktopNotify.js +204 -0
- package/src/shared/notifyWorker.js +46 -0
- package/src/shared/tips.js +1 -0
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
1
|
+
# CipherMesh — Complete Technical Documentation
|
|
2
2
|
|
|
3
3
|
> Secure chat for a local area network (LAN) with real end-to-end encryption (E2EE).
|
|
4
4
|
> The server **never** has access to the content of messages.
|
|
@@ -26,18 +26,18 @@
|
|
|
26
26
|
|
|
27
27
|
### What it is
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
CipherMesh is an instant messaging system designed to operate **exclusively within a local area network (LAN)**. It uses end-to-end encryption (E2EE) based on **Curve25519 + XSalsa20-Poly1305** (via libsodium), ensuring that the server acts only as a **blind relay** — it forwards bytes it cannot read.
|
|
30
30
|
|
|
31
31
|
### Core Principle
|
|
32
32
|
|
|
33
33
|
```
|
|
34
|
-
|
|
34
|
+
Client A Server Client B
|
|
35
35
|
| | |
|
|
36
|
-
|--- payload
|
|
37
|
-
| |--- payload
|
|
36
|
+
|--- encrypted payload --->| |
|
|
37
|
+
| |--- encrypted payload --->|
|
|
38
38
|
| | |
|
|
39
|
-
|
|
|
40
|
-
|
|
|
39
|
+
| The server does NOT | |
|
|
40
|
+
| hold the key to open it | |
|
|
41
41
|
```
|
|
42
42
|
|
|
43
43
|
The server knows **only**:
|
|
@@ -55,15 +55,15 @@ The server **never** knows:
|
|
|
55
55
|
|
|
56
56
|
```
|
|
57
57
|
┌──────────────┐
|
|
58
|
-
│
|
|
59
|
-
│
|
|
58
|
+
│ Server │
|
|
59
|
+
│ (Relay) │
|
|
60
60
|
│ :3600 │
|
|
61
61
|
└──────┬───────┘
|
|
62
62
|
│ WebSocket
|
|
63
63
|
┌────────────┼────────────┐
|
|
64
64
|
│ │ │
|
|
65
65
|
┌─────┴─────┐ ┌───┴───┐ ┌─────┴─────┐
|
|
66
|
-
│
|
|
66
|
+
│ Client A │ │ ... │ │ Client N │
|
|
67
67
|
│ (Terminal) │ │ │ │ (Terminal) │
|
|
68
68
|
└───────────┘ └───────┘ └───────────┘
|
|
69
69
|
```
|
|
@@ -74,14 +74,14 @@ This is the **star** topology (star topology) — the default mode. All clients
|
|
|
74
74
|
|
|
75
75
|
```
|
|
76
76
|
┌───────────┐
|
|
77
|
-
│
|
|
77
|
+
│ Client A │
|
|
78
78
|
│ (Terminal) │
|
|
79
79
|
└─────┬─────┘
|
|
80
80
|
│ WebSocket direto
|
|
81
81
|
┌─────────┼──────────┐
|
|
82
82
|
│ │
|
|
83
83
|
┌───┴───┐ ┌────┴────┐
|
|
84
|
-
│ ... │ │
|
|
84
|
+
│ ... │ │ Client N │
|
|
85
85
|
│ │ │(Terminal)│
|
|
86
86
|
└───────┘ └─────────┘
|
|
87
87
|
```
|
|
@@ -94,13 +94,24 @@ In P2P mode (`npm run p2p`), peers discover each other via mDNS on the LAN and c
|
|
|
94
94
|
|
|
95
95
|
### Production Dependencies
|
|
96
96
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
|
103
|
-
|
|
|
97
|
+
<!-- The package list is checked against package.json by
|
|
98
|
+
test/architecture-doc.test.js. Versions deliberately live only in
|
|
99
|
+
package.json: a number copied here is a number that goes stale, and
|
|
100
|
+
pinning it would put a doc edit in the way of every dependency bump. -->
|
|
101
|
+
|
|
102
|
+
| Package | Role |
|
|
103
|
+
| --- | --- |
|
|
104
|
+
| **@noble/post-quantum** | ML-KEM-768 for the post-quantum hybrid handshake (`src/crypto/PQHybrid.js`). libsodium has no ML-KEM, and this one is audited. |
|
|
105
|
+
| **blessed** | Terminal UI — boxes, scrolling, colours, borders. ncurses-shaped, in JS. Its key parser is old enough to need a shim (`src/client/keyboard.js`). |
|
|
106
|
+
| **bonjour-service** | mDNS service discovery, so P2P peers find each other on the LAN with no relay (`src/p2p/Discovery.js`). |
|
|
107
|
+
| **boxen** | The framed server and P2P banners (`src/shared/banner.js`, `src/p2p/index.js`). |
|
|
108
|
+
| **chalk** | Coloured output outside the blessed UI — banners, prompts, the boot sequence. ESM-only in v5, which suits `"type": "module"`. |
|
|
109
|
+
| **gradient-string** | The gradient across the startup banner (`src/shared/banner.js`). |
|
|
110
|
+
| **jimp** | Decodes received images so they can be drawn as half-blocks in the chat (`src/client/ImagePreview.js`). |
|
|
111
|
+
| **node-notifier** | Desktop notifications. Driven only from `src/shared/notifyWorker.js` — see §4.2 for why it is kept at arm’s length. |
|
|
112
|
+
| **qrcode-terminal** | Renders `/invite` as a QR code in the terminal. |
|
|
113
|
+
| **sodium-native** | Native binding of **libsodium**. Runs in compiled C, not pure JS, so the security is audited and the secret memory is real — see the comparison below. |
|
|
114
|
+
| **ws** | The most mature and performant WebSocket implementation for Node.js. Zero dependencies. Natively supports binary frames (essential for encrypted payloads). |
|
|
104
115
|
|
|
105
116
|
### Why sodium-native and not tweetnacl?
|
|
106
117
|
|
|
@@ -121,11 +132,12 @@ In P2P mode (`npm run p2p`), peers discover each other via mDNS on the LAN and c
|
|
|
121
132
|
### Development Dependencies
|
|
122
133
|
|
|
123
134
|
| Package | Role |
|
|
124
|
-
|
|
125
|
-
|
|
|
126
|
-
|
|
|
127
|
-
| **
|
|
128
|
-
| **
|
|
135
|
+
| --- | --- |
|
|
136
|
+
| **@eslint/js** | ESLint's recommended base configuration. |
|
|
137
|
+
| **eslint** | Linting. |
|
|
138
|
+
| **figlet** | Not used at runtime: `test/banner.test.js` pins the committed ASCII banner against figlet’s own output, so the art cannot drift. |
|
|
139
|
+
| **globals** | Globals definitions (node) for ESLint. |
|
|
140
|
+
| **prettier** | Automatic formatting — consistent code with no style debates. |
|
|
129
141
|
|
|
130
142
|
### Node.js >= 20
|
|
131
143
|
|
|
@@ -140,71 +152,112 @@ Minimum requirement: Node.js 20 LTS. Reasons:
|
|
|
140
152
|
|
|
141
153
|
## 3. Directory Structure
|
|
142
154
|
|
|
155
|
+
<!-- Checked by test/architecture-doc.test.js: every module under src/ has to
|
|
156
|
+
appear here, and nothing here may name a file that does not exist. -->
|
|
157
|
+
|
|
143
158
|
```
|
|
144
|
-
|
|
159
|
+
ciphermesh/
|
|
160
|
+
│
|
|
161
|
+
├── bin/
|
|
162
|
+
│ └── ciphermesh.js # CLI entry point (npx ciphermesh)
|
|
145
163
|
│
|
|
146
164
|
├── docs/
|
|
147
|
-
│
|
|
165
|
+
│ ├── ARCHITECTURE.md
|
|
166
|
+
│ ├── PLUGINS.md
|
|
167
|
+
│ ├── PROTOCOL.md
|
|
168
|
+
│ ├── SETUP.md
|
|
169
|
+
│ └── commands.json
|
|
148
170
|
│
|
|
149
171
|
├── src/
|
|
150
172
|
│ ├── server/
|
|
151
|
-
│ │ ├── index.js
|
|
152
|
-
│ │ ├──
|
|
153
|
-
│ │ ├──
|
|
154
|
-
│ │ ├──
|
|
155
|
-
│ │ ├──
|
|
156
|
-
│ │
|
|
173
|
+
│ │ ├── index.js # Server entry point
|
|
174
|
+
│ │ ├── CertManager.js # TLS certificate generation and loading
|
|
175
|
+
│ │ ├── config.js # Server configuration and env vars
|
|
176
|
+
│ │ ├── ConnectionGuard.js # Rate limits and connection abuse guards
|
|
177
|
+
│ │ ├── MessageRouter.js # Routes encrypted payloads between clients
|
|
178
|
+
│ │ ├── OfflineQueue.js # Queue for messages to offline peers
|
|
179
|
+
│ │ ├── preflight.js # Startup checks before the port is opened
|
|
180
|
+
│ │ ├── presence.js # Presence/hub counters
|
|
181
|
+
│ │ ├── SessionManager.js # Active sessions (connected clients)
|
|
182
|
+
│ │ └── WebSocketServer.js # WebSocket connection handling
|
|
157
183
|
│ │
|
|
158
184
|
│ ├── client/
|
|
159
|
-
│ │ ├── index.js
|
|
160
|
-
│ │ ├──
|
|
161
|
-
│ │ ├── Connection.js
|
|
162
|
-
│ │ ├──
|
|
163
|
-
│ │
|
|
185
|
+
│ │ ├── index.js # Client entry point
|
|
186
|
+
│ │ ├── ChatController.js # Core logic: UI + Connection + Crypto
|
|
187
|
+
│ │ ├── Connection.js # WebSocket connection to the server
|
|
188
|
+
│ │ ├── FileTransfer.js # Encrypted file send/receive (chunks)
|
|
189
|
+
│ │ ├── ImagePreview.js # Half-block image previews
|
|
190
|
+
│ │ ├── keyboard.js # Keyboard-protocol shim (Shift+Enter)
|
|
191
|
+
│ │ └── UI.js # Blessed interface (layout, wrapping, rendering)
|
|
164
192
|
│ │
|
|
165
193
|
│ ├── crypto/
|
|
166
|
-
│ │ ├──
|
|
167
|
-
│ │ ├──
|
|
168
|
-
│ │ ├──
|
|
169
|
-
│ │ ├──
|
|
170
|
-
│ │ ├──
|
|
171
|
-
│ │ ├──
|
|
172
|
-
│ │
|
|
194
|
+
│ │ ├── CertPinStore.js # TLS certificate pinning
|
|
195
|
+
│ │ ├── DeniableEncrypt.js # Deniable (symmetric) message mode
|
|
196
|
+
│ │ ├── DeviceIdentity.js # Ed25519 identity and the signed device list
|
|
197
|
+
│ │ ├── DoubleRatchet.js # PFS via Double Ratchet (DH ratchet + KDF chains)
|
|
198
|
+
│ │ ├── Handshake.js # Public-key exchange protocol
|
|
199
|
+
│ │ ├── HistoryStore.js # Encrypted local history on disk
|
|
200
|
+
│ │ ├── IdentityBackup.js # Encrypted identity + trust export/import
|
|
201
|
+
│ │ ├── KeyManager.js # Key pairs (in memory)
|
|
202
|
+
│ │ ├── MessageCrypto.js # Encrypt/decrypt (crypto_box_easy)
|
|
203
|
+
│ │ ├── NonceManager.js # Nonce generation and replay checks
|
|
204
|
+
│ │ ├── PQHybrid.js # Post-quantum hybrid (X25519 + ML-KEM-768)
|
|
205
|
+
│ │ ├── RoomKey.js # Room key derivation and rotation
|
|
206
|
+
│ │ ├── SealedSender.js # Sealed sender — the relay cannot see who sent what
|
|
207
|
+
│ │ ├── SenderKey.js # Sender keys for group messages
|
|
208
|
+
│ │ ├── StateManager.js # Encrypted state persistence (Argon2id + secretbox)
|
|
209
|
+
│ │ └── TrustStore.js # TOFU + SAS (fingerprint persistence)
|
|
173
210
|
│ │
|
|
174
211
|
│ ├── p2p/
|
|
175
|
-
│ │ ├── index.js
|
|
176
|
-
│ │ ├── Discovery.js
|
|
177
|
-
│ │ ├──
|
|
178
|
-
│ │ ├── PeerConnectionManager.js #
|
|
179
|
-
│ │ └──
|
|
212
|
+
│ │ ├── index.js # P2P mode entry point
|
|
213
|
+
│ │ ├── Discovery.js # mDNS discovery via bonjour-service
|
|
214
|
+
│ │ ├── P2PChatController.js # P2P orchestrator (crypto + UI + peers)
|
|
215
|
+
│ │ ├── PeerConnectionManager.js # Outbound/inbound connection management
|
|
216
|
+
│ │ └── PeerServer.js # Local WebSocket server (random port)
|
|
180
217
|
│ │
|
|
181
218
|
│ ├── protocol/
|
|
182
|
-
│ │ ├──
|
|
183
|
-
│ │
|
|
219
|
+
│ │ ├── capabilities.js # Feature negotiation between versions
|
|
220
|
+
│ │ ├── messages.js # Protocol message types
|
|
221
|
+
│ │ └── validators.js # Payload structure validation
|
|
184
222
|
│ │
|
|
185
223
|
│ └── shared/
|
|
186
|
-
│ ├──
|
|
187
|
-
│
|
|
188
|
-
│
|
|
189
|
-
├──
|
|
190
|
-
│
|
|
191
|
-
│
|
|
192
|
-
│
|
|
193
|
-
│
|
|
194
|
-
│
|
|
195
|
-
│
|
|
196
|
-
│
|
|
197
|
-
│
|
|
224
|
+
│ ├── AuditLog.js # Local audit trail
|
|
225
|
+
│ ├── banner.js # Startup banners
|
|
226
|
+
│ ├── commandSuggest.js # Did-you-mean for mistyped commands
|
|
227
|
+
│ ├── config.js # Client config file
|
|
228
|
+
│ ├── constants.js # Global constants (ports, limits, version)
|
|
229
|
+
│ ├── coverTraffic.js # Cover traffic (anti-metadata)
|
|
230
|
+
│ ├── desktopNotify.js # Desktop notifications — breaker, throttle, isolation
|
|
231
|
+
│ ├── deviceProvisioning.js # Multi-device request/grant/accept
|
|
232
|
+
│ ├── dnd.js # Do-not-disturb / mentions-only gating
|
|
233
|
+
│ ├── doctor.js # Connection diagnosis for /doctor
|
|
234
|
+
│ ├── emoji.js # `:shortcode:` map
|
|
235
|
+
│ ├── fuzzy.js # Fuzzy matching for the palette and pickers
|
|
236
|
+
│ ├── invite.js # Invite strings and QR payloads
|
|
237
|
+
│ ├── keyArt.js # Fingerprint art
|
|
238
|
+
│ ├── lastSession.js # Last-session hints
|
|
239
|
+
│ ├── logger.js # Structured logger (levels and timestamps)
|
|
240
|
+
│ ├── notifyWorker.js # One-shot notification helper, spawned console-less
|
|
241
|
+
│ ├── onboarding.js # First-run setup wizard
|
|
242
|
+
│ ├── panic.js # Duress wipe
|
|
243
|
+
│ ├── pluginCommand.js # The /plugins command
|
|
244
|
+
│ ├── PluginManager.js # Plugin loading and sandboxing
|
|
245
|
+
│ ├── prompt.js # Readline prompts
|
|
246
|
+
│ ├── terminalGraphics.js # kitty/iTerm2 inline image protocols
|
|
247
|
+
│ ├── themes.js # Nick colour themes
|
|
248
|
+
│ ├── tips.js # Security/UX tips
|
|
249
|
+
│ ├── trust.js # Trust badges
|
|
250
|
+
│ └── voiceNote.js # Voice note record/playback
|
|
198
251
|
│
|
|
252
|
+
├── test/ # 90 suites, run with `npm test`
|
|
199
253
|
├── scripts/
|
|
200
|
-
│
|
|
254
|
+
│ ├── generate-commands.mjs # Regenerates docs/commands.json from the code
|
|
255
|
+
│ └── ...
|
|
201
256
|
│
|
|
202
|
-
├── .
|
|
203
|
-
├── .
|
|
204
|
-
├── .gitignore
|
|
205
|
-
├── .npmrc
|
|
257
|
+
├── Formula/ciphermesh.rb # Homebrew formula
|
|
258
|
+
├── eslint.config.js
|
|
206
259
|
├── .prettierrc
|
|
207
|
-
├──
|
|
260
|
+
├── CHANGELOG.md
|
|
208
261
|
├── package.json
|
|
209
262
|
└── README.md
|
|
210
263
|
```
|
|
@@ -263,33 +316,67 @@ securelan-chat/
|
|
|
263
316
|
- Connects to the server and starts the UI
|
|
264
317
|
|
|
265
318
|
#### `src/client/UI.js` — Blessed Interface
|
|
266
|
-
- Layout divided into
|
|
319
|
+
- Layout divided into 4 areas:
|
|
267
320
|
|
|
268
321
|
```
|
|
269
|
-
|
|
270
|
-
│
|
|
271
|
-
|
|
272
|
-
│
|
|
273
|
-
│
|
|
274
|
-
│
|
|
275
|
-
│
|
|
276
|
-
│
|
|
277
|
-
│
|
|
278
|
-
|
|
279
|
-
│
|
|
280
|
-
|
|
322
|
+
┌──────────────────────────────────────────────────────┐
|
|
323
|
+
│ ● CipherMesh ▏ felipe ● 3 online ▏ E2E │ <- Header
|
|
324
|
+
├──────────────────────────────────────────────────────┤
|
|
325
|
+
│ 10:30 🦊 ana │ <- Chat area
|
|
326
|
+
│ Hi! This message wraps well short of the │ (scrollable)
|
|
327
|
+
│ window, not at the border │
|
|
328
|
+
│ │
|
|
329
|
+
│ 10:31 🐧 felipe ✓✓ │
|
|
330
|
+
│ ▎ Oi ana │
|
|
331
|
+
│ │
|
|
332
|
+
│ 10:32 * bob entrou no chat │
|
|
333
|
+
├──────────────────────────────────────────────────────┤
|
|
334
|
+
│ #general Tab ~ Ctrl+K commands ~ /help ~ ^C │ <- Status bar
|
|
335
|
+
├──────────────────────────────────────────────────────┤
|
|
336
|
+
│ > Type your message... │ <- Input box
|
|
337
|
+
└──────────────────────────────────────────────────────┘
|
|
281
338
|
```
|
|
282
339
|
|
|
283
|
-
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
340
|
+
- **Messages are blocks, not lines.** A header naming the sender, then the text
|
|
341
|
+
wrapped at 65 % of the window (78 columns at most) and indented under it. Each
|
|
342
|
+
entry is one `'\n'`-joined string so it stays a single addressable log line —
|
|
343
|
+
reactions, read receipts, edits and the ephemeral burn all still address it by
|
|
344
|
+
index.
|
|
345
|
+
- What distinguishes a message is a coloured rule down the left of the body,
|
|
346
|
+
not its alignment: yellow when it mentions you, magenta for a DM, the accent
|
|
347
|
+
for your own, nothing for a plain incoming one.
|
|
348
|
+
- Runs from one sender fold under a single header, but only inside the same
|
|
349
|
+
minute, so folding never costs the reader a timestamp.
|
|
350
|
+
- `wrapTagged` wraps text that already carries blessed tags: it closes the open
|
|
351
|
+
tag stack at each break and reopens it after, because blessed carries its
|
|
352
|
+
attribute state across the whole content and a tag left open would bleed into
|
|
353
|
+
the next line's gutter. Wrapping before the markdown pass would be simpler and
|
|
354
|
+
would split `**bold**` spans in half.
|
|
355
|
+
- Everything is laid out again on resize, active buffer and stored ones alike,
|
|
356
|
+
from a per-entry recipe kept alongside the rendered string. Entries with no
|
|
357
|
+
recipe — image previews, animation frames — keep exactly what they were given.
|
|
358
|
+
- The status bar shows the room, buffer tabs, and the key shortcuts
|
|
359
|
+
- Chat area with automatic and manual scroll, plus a "new messages ↓" pill
|
|
360
|
+
- Distinct colours per user, with an emoji avatar derived from the nickname
|
|
288
361
|
- Animated "typing..." indicator with support for multiple peers
|
|
289
362
|
- Sound notifications (toggle via `/sound on|off`)
|
|
290
363
|
- Progress bar for file transfers
|
|
291
364
|
- Notifications for users joining/leaving
|
|
292
365
|
|
|
366
|
+
#### `src/client/keyboard.js` — Keyboard protocols
|
|
367
|
+
- A terminal cannot tell Shift+Enter from Enter unless the application asks it
|
|
368
|
+
to, so startup requests the kitty keyboard protocol (`CSI > 1 u`) and xterm's
|
|
369
|
+
`modifyOtherKeys` level 1 (`CSI > 4 ; 1 m`), and undoes both on the way out.
|
|
370
|
+
- blessed's key parser cannot read what comes back — neither a `u` final byte
|
|
371
|
+
after two parameters nor a `~` after three — and would emit `13;2u` as five
|
|
372
|
+
typed characters. So the reports are decoded on the raw byte stream ahead of
|
|
373
|
+
it, installed as `blessed.screen({ input })`.
|
|
374
|
+
- Enter with any modifier becomes a newline; every other enhanced report is
|
|
375
|
+
rewritten to the legacy encoding blessed already understands; anything with no
|
|
376
|
+
legacy equivalent is dropped rather than typed; arrows, function keys, mouse
|
|
377
|
+
reports and bracketed pastes pass through untouched.
|
|
378
|
+
- `CIPHERMESH_LEGACY_KEYS=1` skips the shim and the negotiation entirely.
|
|
379
|
+
|
|
293
380
|
#### `src/client/Connection.js` — WebSocket Client
|
|
294
381
|
- Connects to the server via `ws`
|
|
295
382
|
- Automatic reconnect with exponential backoff (1s, 2s, 4s, 8s, max 30s)
|
|
@@ -306,6 +393,12 @@ securelan-chat/
|
|
|
306
393
|
- Validates fingerprints
|
|
307
394
|
- Manages file transfers via FileTransfer
|
|
308
395
|
- Sound notification when text messages are received
|
|
396
|
+
- Desktop notifications through `src/shared/desktopNotify.js`: rate-limited to
|
|
397
|
+
one per 3 s, muted for the session on the first refusal with one line in the
|
|
398
|
+
chat saying why, and on Windows delivered by a detached, console-less helper
|
|
399
|
+
(`src/shared/notifyWorker.js`) because SnoreToast writes its diagnostics to the
|
|
400
|
+
attached console — the one blessed is drawing on — when notifications are
|
|
401
|
+
disabled for the application
|
|
309
402
|
|
|
310
403
|
#### `src/client/FileTransfer.js` — File Transfer
|
|
311
404
|
- Sending: reads the file, splits it into 48KB chunks, encrypts each chunk E2E via broadcast
|
|
@@ -433,9 +526,9 @@ securelan-chat/
|
|
|
433
526
|
Defines the protocol's message types. All messages have:
|
|
434
527
|
```js
|
|
435
528
|
{
|
|
436
|
-
type: string, //
|
|
437
|
-
version: 1, //
|
|
438
|
-
timestamp: number // Date.now()
|
|
529
|
+
type: string, // message type
|
|
530
|
+
version: 1, // protocol version
|
|
531
|
+
timestamp: number // Date.now() on the sender
|
|
439
532
|
}
|
|
440
533
|
```
|
|
441
534
|
|
|
@@ -520,7 +613,7 @@ export const FILE_CHUNK_SIZE = 49152; // 48KB
|
|
|
520
613
|
"version": 1,
|
|
521
614
|
"timestamp": 1739800000000,
|
|
522
615
|
"nickname": "Alice",
|
|
523
|
-
"publicKey": "base64(32
|
|
616
|
+
"publicKey": "base64(32-byte Curve25519 public key)",
|
|
524
617
|
"caps": ["sk1"]
|
|
525
618
|
}
|
|
526
619
|
```
|
|
@@ -541,7 +634,7 @@ client's.
|
|
|
541
634
|
{
|
|
542
635
|
"sessionId": "660e8400-e29b-41d4-a716-446655440001",
|
|
543
636
|
"nickname": "Bob",
|
|
544
|
-
"publicKey": "base64(
|
|
637
|
+
"publicKey": "base64(Bob's public key)",
|
|
545
638
|
"caps": ["sk1"]
|
|
546
639
|
}
|
|
547
640
|
],
|
|
@@ -666,7 +759,7 @@ Besides text messages, the encrypted payload may contain commands (the `action`
|
|
|
666
759
|
"peer": {
|
|
667
760
|
"sessionId": "770e8400-e29b-41d4-a716-446655440002",
|
|
668
761
|
"nickname": "Charlie",
|
|
669
|
-
"publicKey": "base64(
|
|
762
|
+
"publicKey": "base64(Charlie's public key)"
|
|
670
763
|
}
|
|
671
764
|
}
|
|
672
765
|
```
|
|
@@ -719,12 +812,12 @@ This combination (known as **NaCl crypto_box**) was chosen because:
|
|
|
719
812
|
### 6.3 Key Generation
|
|
720
813
|
|
|
721
814
|
```
|
|
722
|
-
1.
|
|
723
|
-
2. sodium.crypto_box_keypair()
|
|
724
|
-
- publicKey: 32 bytes (
|
|
725
|
-
- secretKey: 32 bytes (
|
|
726
|
-
3.
|
|
727
|
-
4. Fingerprint = SHA256(publicKey)
|
|
815
|
+
1. The client starts
|
|
816
|
+
2. sodium.crypto_box_keypair() produces:
|
|
817
|
+
- publicKey: 32 bytes (safe to share)
|
|
818
|
+
- secretKey: 32 bytes (NEVER leaves the process's memory)
|
|
819
|
+
3. Both held in sodium.sodium_malloc() (secure memory)
|
|
820
|
+
4. Fingerprint = SHA256(publicKey), formatted as XXXX:XXXX:XXXX:XXXX
|
|
728
821
|
```
|
|
729
822
|
|
|
730
823
|
### 6.4 Authenticated Encryption with crypto_box_easy
|
|
@@ -734,12 +827,12 @@ This combination (known as **NaCl crypto_box**) was chosen because:
|
|
|
734
827
|
```
|
|
735
828
|
crypto_box_easy(ciphertext, plaintext, nonce, recipientPublicKey, senderSecretKey)
|
|
736
829
|
|
|
737
|
-
|
|
738
|
-
1. X25519 DH:
|
|
830
|
+
Internally:
|
|
831
|
+
1. X25519 DH: sharedSecret = ECDH(recipientPub, senderSec)
|
|
739
832
|
2. Key derivation: encKey = HSalsa20(sharedSecret, zeros)
|
|
740
|
-
3.
|
|
741
|
-
4. MAC:
|
|
742
|
-
5. Output:
|
|
833
|
+
3. Encrypt: XSalsa20(plaintext, nonce, encKey) -> ciphertext
|
|
834
|
+
4. MAC: Poly1305(ciphertext) -> 16-byte tag
|
|
835
|
+
5. Output: tag || ciphertext (authenticated)
|
|
743
836
|
```
|
|
744
837
|
|
|
745
838
|
The shared key is derived implicitly on each call. The DH guarantees
|
|
@@ -749,38 +842,38 @@ that both sides (Alice and Bob) arrive at the same secret without exchanging it
|
|
|
749
842
|
|
|
750
843
|
```
|
|
751
844
|
Input:
|
|
752
|
-
- plaintext: Buffer (
|
|
753
|
-
- nonce: 24 bytes (
|
|
754
|
-
- recipientPublicKey: 32 bytes (
|
|
755
|
-
- senderSecretKey: 32 bytes (
|
|
845
|
+
- plaintext: Buffer (UTF-8 message)
|
|
846
|
+
- nonce: 24 bytes (from the NonceManager)
|
|
847
|
+
- recipientPublicKey: 32 bytes (recipient's public key)
|
|
848
|
+
- senderSecretKey: 32 bytes (sender's secret key)
|
|
756
849
|
|
|
757
|
-
|
|
850
|
+
Process:
|
|
758
851
|
ciphertext = crypto_box_easy(plaintext, nonce, recipientPublicKey, senderSecretKey)
|
|
759
852
|
|
|
760
853
|
Output:
|
|
761
|
-
- ciphertext: Buffer (plaintext.length + 16 bytes
|
|
762
|
-
- nonce: 24 bytes (
|
|
854
|
+
- ciphertext: Buffer (plaintext.length + 16 bytes of MAC)
|
|
855
|
+
- nonce: 24 bytes (sent alongside; it is not a secret)
|
|
763
856
|
|
|
764
|
-
Total
|
|
857
|
+
Total on the wire: ciphertext (N+16 bytes) + nonce (24 bytes)
|
|
765
858
|
```
|
|
766
859
|
|
|
767
860
|
### 6.6 Message Decryption
|
|
768
861
|
|
|
769
862
|
```
|
|
770
863
|
Input:
|
|
771
|
-
- ciphertext: Buffer (
|
|
772
|
-
- nonce: 24 bytes (
|
|
773
|
-
- senderPublicKey: 32 bytes (
|
|
774
|
-
- recipientSecretKey: 32 bytes (
|
|
864
|
+
- ciphertext: Buffer (off the network)
|
|
865
|
+
- nonce: 24 bytes (off the network)
|
|
866
|
+
- senderPublicKey: 32 bytes (sender's public key)
|
|
867
|
+
- recipientSecretKey: 32 bytes (recipient's secret key)
|
|
775
868
|
|
|
776
|
-
|
|
777
|
-
1. NonceManager
|
|
869
|
+
Process:
|
|
870
|
+
1. NonceManager checks the nonce has not been seen before (anti-replay)
|
|
778
871
|
2. plaintext = crypto_box_open_easy(ciphertext, nonce, senderPublicKey, recipientSecretKey)
|
|
779
|
-
3.
|
|
780
|
-
4.
|
|
872
|
+
3. MAC invalid -> reject (the message was tampered with)
|
|
873
|
+
4. MAC valid -> parse the inner JSON
|
|
781
874
|
|
|
782
875
|
Output:
|
|
783
|
-
- plaintext: Buffer (
|
|
876
|
+
- plaintext: Buffer (the original message)
|
|
784
877
|
```
|
|
785
878
|
|
|
786
879
|
### 6.7 Nonce Structure (24 bytes)
|
|
@@ -788,12 +881,12 @@ Output:
|
|
|
788
881
|
```
|
|
789
882
|
┌──────────────────┬──────────────┬──────────────────────┐
|
|
790
883
|
│ Timestamp (8B) │ Counter (4B) │ Random (12B) │
|
|
791
|
-
│
|
|
884
|
+
│ ms since epoch │ sequential │ sodium.randombytes │
|
|
792
885
|
└──────────────────┴──────────────┴──────────────────────┘
|
|
793
886
|
|
|
794
|
-
- Timestamp:
|
|
795
|
-
- Counter:
|
|
796
|
-
- Random:
|
|
887
|
+
- Timestamp: blocks replay across different sessions
|
|
888
|
+
- Counter: guarantees ordering and uniqueness within a session
|
|
889
|
+
- Random: guarantees uniqueness even with synchronised clocks
|
|
797
890
|
```
|
|
798
891
|
|
|
799
892
|
### 6.8 Fingerprint Verification
|
|
@@ -801,16 +894,16 @@ Output:
|
|
|
801
894
|
The fingerprint lets users verify each other's identity **out of band** (for example, in person or by phone):
|
|
802
895
|
|
|
803
896
|
```
|
|
804
|
-
1. Alice
|
|
805
|
-
2. Bob
|
|
806
|
-
3. Bob
|
|
807
|
-
4.
|
|
897
|
+
1. Alice reads her own fingerprint: A1B2:C3D4:E5F6:7890
|
|
898
|
+
2. Bob reads Alice's fingerprint: A1B2:C3D4:E5F6:7890
|
|
899
|
+
3. Bob confirms with Alice, in person, that the values match
|
|
900
|
+
4. They do not match -> MITM detected
|
|
808
901
|
```
|
|
809
902
|
|
|
810
903
|
The fingerprint is computed like this:
|
|
811
904
|
```
|
|
812
905
|
fingerprint = SHA-256(publicKey)
|
|
813
|
-
=
|
|
906
|
+
= first 8 bytes, hex, ':'-separated
|
|
814
907
|
= "A1B2:C3D4:E5F6:7890"
|
|
815
908
|
```
|
|
816
909
|
|
|
@@ -948,7 +1041,7 @@ multiply the fallback's cost by the number of devices per peer.
|
|
|
948
1041
|
### 7.1 Full Diagram
|
|
949
1042
|
|
|
950
1043
|
```
|
|
951
|
-
|
|
1044
|
+
Client A Server Client B
|
|
952
1045
|
│ │ │
|
|
953
1046
|
│ 1. JOIN(nick, pubKeyA) │ │
|
|
954
1047
|
│ ──────────────────────────>│ │
|
|
@@ -964,7 +1057,7 @@ multiply the fallback's cost by the number of devices per peer.
|
|
|
964
1057
|
│ usando pubKeyB + secKeyA │ usando pubKeyA + secKeyB
|
|
965
1058
|
│ │ │
|
|
966
1059
|
│ 6. ENCRYPTED_MSG ─────────│────────────────────────> │
|
|
967
|
-
│ │
|
|
1060
|
+
│ │ 7. Decrypt with sharedKey │
|
|
968
1061
|
│ │ │
|
|
969
1062
|
```
|
|
970
1063
|
|
|
@@ -995,24 +1088,24 @@ multiply the fallback's cost by the number of devices per peer.
|
|
|
995
1088
|
|
|
996
1089
|
## 8. Step-by-Step Communication Flow
|
|
997
1090
|
|
|
998
|
-
### 8.1 Full Scenario: Alice sends "
|
|
1091
|
+
### 8.1 Full Scenario: Alice sends "Hi" to Bob
|
|
999
1092
|
|
|
1000
1093
|
```
|
|
1001
|
-
|
|
1094
|
+
TIME ACTION
|
|
1002
1095
|
───── ──────────────────────────────────────────────────────
|
|
1003
|
-
t0 Alice
|
|
1096
|
+
t0 Alice types "Hi" in the composer and presses Enter
|
|
1004
1097
|
|
|
1005
|
-
t1 ChatController
|
|
1006
|
-
ChatController
|
|
1007
|
-
|
|
1098
|
+
t1 ChatController receives the text from the UI
|
|
1099
|
+
ChatController checks it has a sharedKey with Bob
|
|
1100
|
+
If it does not -> error "Handshake not completed with Bob"
|
|
1008
1101
|
|
|
1009
1102
|
t2 MessageCrypto.encrypt():
|
|
1010
|
-
- NonceManager
|
|
1011
|
-
-
|
|
1103
|
+
- NonceManager produces a 24-byte nonce
|
|
1104
|
+
- Serialises the inner payload: { text: "Hi", sentAt: t2, messageId: "a1b2" }
|
|
1012
1105
|
- crypto_box_easy_afternm(payload, nonce, sharedKeyAB)
|
|
1013
|
-
-
|
|
1106
|
+
- Returns { ciphertext: Buffer, nonce: Buffer }
|
|
1014
1107
|
|
|
1015
|
-
t3 Connection
|
|
1108
|
+
t3 Connection sends to the server:
|
|
1016
1109
|
{
|
|
1017
1110
|
type: "encrypted_message",
|
|
1018
1111
|
from: "alice-session-id",
|
|
@@ -1020,27 +1113,27 @@ t3 Connection envia ao servidor:
|
|
|
1020
1113
|
payload: { ciphertext: "base64(...)", nonce: "base64(...)" }
|
|
1021
1114
|
}
|
|
1022
1115
|
|
|
1023
|
-
t4
|
|
1024
|
-
-
|
|
1025
|
-
-
|
|
1026
|
-
-
|
|
1027
|
-
-
|
|
1116
|
+
t4 Server (MessageRouter):
|
|
1117
|
+
- Validates the structure (has type, from, to, payload)
|
|
1118
|
+
- Does NOT open the payload
|
|
1119
|
+
- Finds Bob's WebSocket by sessionId
|
|
1120
|
+
- Forwards the whole JSON to Bob
|
|
1028
1121
|
|
|
1029
|
-
t5 Bob (Connection)
|
|
1030
|
-
ChatController
|
|
1122
|
+
t5 Bob (Connection) receives the JSON
|
|
1123
|
+
ChatController identifies it: encrypted_message from Alice
|
|
1031
1124
|
|
|
1032
1125
|
t6 MessageCrypto.decrypt():
|
|
1033
|
-
-
|
|
1034
|
-
- NonceManager
|
|
1126
|
+
- Extracts ciphertext and nonce from the payload
|
|
1127
|
+
- NonceManager checks the nonce (not repeated, counter valid)
|
|
1035
1128
|
- crypto_box_open_easy_afternm(ciphertext, nonce, sharedKeyAB)
|
|
1036
|
-
-
|
|
1037
|
-
-
|
|
1129
|
+
- MAC fails -> reject (corrupted or tampered with)
|
|
1130
|
+
- MAC passes -> parse the inner JSON
|
|
1038
1131
|
|
|
1039
|
-
t7 ChatController
|
|
1040
|
-
|
|
1132
|
+
t7 ChatController receives { text: "Hi", sentAt: t2, messageId: "a1b2" }
|
|
1133
|
+
Checks sentAt is reasonable (not far in the past or future)
|
|
1041
1134
|
|
|
1042
|
-
t8 UI.
|
|
1043
|
-
Bob
|
|
1135
|
+
t8 UI.addMessage("Alice", "Hi")
|
|
1136
|
+
Bob sees the message under an "10:30 🦊 Alice" header
|
|
1044
1137
|
```
|
|
1045
1138
|
|
|
1046
1139
|
### 8.2 Scenario: Group Chat (broadcast)
|
|
@@ -1082,45 +1175,47 @@ a server change and is not wired up there yet.)
|
|
|
1082
1175
|
### 9.1 Server
|
|
1083
1176
|
|
|
1084
1177
|
```
|
|
1085
|
-
1.
|
|
1086
|
-
2.
|
|
1087
|
-
3.
|
|
1088
|
-
4.
|
|
1089
|
-
5.
|
|
1178
|
+
1. Load configuration and constants (config.js, constants.js)
|
|
1179
|
+
2. Run the preflight checks (port free, certs readable, limits sane)
|
|
1180
|
+
3. Load or generate the TLS certificate (CertManager) — wss:// by default
|
|
1181
|
+
4. Create the WebSocketServer on the configured port
|
|
1182
|
+
5. Create the SessionManager (empty session map) and the ConnectionGuard
|
|
1183
|
+
6. Create the MessageRouter (holding a reference to the SessionManager)
|
|
1184
|
+
7. Register handlers:
|
|
1090
1185
|
- on('connection') -> SessionManager.handleConnection()
|
|
1091
1186
|
- on('close') -> SessionManager.handleDisconnection()
|
|
1092
1187
|
- on('message') -> MessageRouter.route()
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
-
|
|
1096
|
-
-
|
|
1097
|
-
-
|
|
1098
|
-
|
|
1099
|
-
- IP local (todas as interfaces de rede)
|
|
1100
|
-
- Porta
|
|
1101
|
-
- "Servidor pronto. Clientes podem conectar em ws://<IP>:3600"
|
|
1188
|
+
8. Start the heartbeat interval (ping every client every 30s)
|
|
1189
|
+
9. Register SIGINT/SIGTERM for a graceful shutdown:
|
|
1190
|
+
- Notify every client
|
|
1191
|
+
- Close the connections
|
|
1192
|
+
- Release resources
|
|
1193
|
+
10. Print the banner: every local IP, the port, and the wss:// URLs
|
|
1102
1194
|
```
|
|
1103
1195
|
|
|
1104
1196
|
### 9.2 Client
|
|
1105
1197
|
|
|
1106
1198
|
```
|
|
1107
|
-
1.
|
|
1108
|
-
2.
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
-
|
|
1199
|
+
1. Show the CipherMesh banner
|
|
1200
|
+
2. First run only: the setup wizard (nickname, server, theme), saved to the
|
|
1201
|
+
config file so it never asks twice
|
|
1202
|
+
3. Ask for the nickname (1-20 chars, alphanumeric + underscore) unless saved
|
|
1203
|
+
4. Ask for the server address, or accept an invite string (default: localhost:3600)
|
|
1204
|
+
5. Restore the encrypted session state if there is one (StateManager)
|
|
1205
|
+
6. Generate or load the key pair (KeyManager) and the device identity
|
|
1206
|
+
7. Show the public-key fingerprint and its key art
|
|
1207
|
+
8. Connect over WebSocket, pinning the certificate (CertPinStore)
|
|
1208
|
+
9. Send JOIN (nickname + public key); wait for JOIN_ACK
|
|
1209
|
+
10. On error (duplicate nickname) -> ask for another one
|
|
1210
|
+
11. Receive the peer list and run the handshake with each, deriving a shared key
|
|
1211
|
+
12. Start the blessed UI: request the keyboard protocols, draw the header,
|
|
1212
|
+
the chat log, the status bar and the composer
|
|
1213
|
+
13. Enter the input loop
|
|
1214
|
+
14. Register a SIGINT handler to:
|
|
1215
|
+
- sodium_memzero() every key
|
|
1216
|
+
- Persist the encrypted state
|
|
1217
|
+
- Close the WebSocket
|
|
1218
|
+
- Restore the terminal (keyboard protocols, bracketed paste) and destroy the UI
|
|
1124
1219
|
```
|
|
1125
1220
|
|
|
1126
1221
|
---
|
|
@@ -1268,7 +1363,7 @@ routing). With sealed sender, the *sender* side of the social graph stays hidden
|
|
|
1268
1363
|
```
|
|
1269
1364
|
Startup:
|
|
1270
1365
|
1. Se existe estado salvo → prompt passphrase → loadState() → restaura KeyManager, Handshake, peers
|
|
1271
|
-
2.
|
|
1366
|
+
2. If there is none → optional passphrase prompt (to protect a future session)
|
|
1272
1367
|
|
|
1273
1368
|
Shutdown (Ctrl+C, /quit):
|
|
1274
1369
|
Se passphrase definida → serializeState() → saveState() cifrado
|
|
@@ -1316,48 +1411,49 @@ Shutdown (Ctrl+C, /quit):
|
|
|
1316
1411
|
**Future evolution** — P2P with a DHT (for larger networks):
|
|
1317
1412
|
```
|
|
1318
1413
|
1. Distributed Hash Table para discovery
|
|
1319
|
-
2.
|
|
1320
|
-
3.
|
|
1321
|
-
4.
|
|
1414
|
+
2. Each node keeps a partial routing table
|
|
1415
|
+
3. Messages can be routed over multiple hops
|
|
1416
|
+
4. Redundancy and fault tolerance
|
|
1322
1417
|
```
|
|
1323
1418
|
|
|
1324
|
-
### 11.6
|
|
1419
|
+
### 11.6 Project Infrastructure
|
|
1420
|
+
|
|
1421
|
+
Most of this section used to be a wishlist. It is now a status list, which is a
|
|
1422
|
+
better thing for it to be — what is left is the short part.
|
|
1423
|
+
|
|
1424
|
+
**In place**:
|
|
1325
1425
|
|
|
1326
|
-
**Repository structure**:
|
|
1327
1426
|
```
|
|
1328
|
-
|
|
1329
|
-
├──
|
|
1330
|
-
│ ├──
|
|
1331
|
-
│
|
|
1332
|
-
│
|
|
1333
|
-
│
|
|
1334
|
-
│ ├──
|
|
1335
|
-
│
|
|
1336
|
-
│
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
├── docs/
|
|
1340
|
-
│ ├── ARCHITECTURE.md
|
|
1341
|
-
│ ├── SECURITY.md # Politica de seguranca
|
|
1342
|
-
│ ├── CONTRIBUTING.md # Guia de contribuicao
|
|
1343
|
-
│ └── PROTOCOL.md # Especificacao do protocolo
|
|
1344
|
-
├── LICENSE # MIT ou Apache-2.0
|
|
1345
|
-
├── CHANGELOG.md # Historico de mudancas (semver)
|
|
1346
|
-
├── CODE_OF_CONDUCT.md
|
|
1347
|
-
└── SECURITY.md # Como reportar vulnerabilidades
|
|
1427
|
+
.github/
|
|
1428
|
+
├── workflows/
|
|
1429
|
+
│ ├── ci.yml # lint + format + tests, Node 20 and 22, on every PR
|
|
1430
|
+
│ ├── codeql.yml # CodeQL static analysis
|
|
1431
|
+
│ ├── release.yml # validate -> npm publish (OIDC) -> GitHub Release -> relay deploy
|
|
1432
|
+
│ ├── binaries.yml # standalone binaries
|
|
1433
|
+
│ ├── docker-publish.yml # image to GHCR
|
|
1434
|
+
│ ├── deploy.yml # site deploy, called by docker-publish
|
|
1435
|
+
│ └── hub-monitor.yml # scheduled hub health checks
|
|
1436
|
+
├── dependabot.yml
|
|
1437
|
+
└── CODEOWNERS
|
|
1348
1438
|
```
|
|
1349
1439
|
|
|
1350
|
-
|
|
1351
|
-
-
|
|
1352
|
-
|
|
1353
|
-
-
|
|
1354
|
-
-
|
|
1355
|
-
-
|
|
1356
|
-
|
|
1357
|
-
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1440
|
+
- Semantic versioning, and conventional commits enforced by commitlint
|
|
1441
|
+
- `LICENSE` (MIT), `SECURITY.md` with a disclosure process, `CONTRIBUTING.md`,
|
|
1442
|
+
`TERMS.md`
|
|
1443
|
+
- `CHANGELOG.md`, an entry per release
|
|
1444
|
+
- Badges in the README
|
|
1445
|
+
- npm publishing over **OIDC Trusted Publishing** — no long-lived token exists to
|
|
1446
|
+
leak
|
|
1447
|
+
- A Homebrew formula (`Formula/ciphermesh.rb`), whose digest is filled in after
|
|
1448
|
+
the tag publishes the tarball
|
|
1449
|
+
|
|
1450
|
+
**Not done, and worth doing**:
|
|
1451
|
+
|
|
1452
|
+
- Issue and pull-request templates
|
|
1453
|
+
- A code of conduct
|
|
1454
|
+
- GPG-signed release artefacts (the npm provenance attestation covers part of
|
|
1455
|
+
this, but not the GitHub Release assets)
|
|
1456
|
+
- Published API documentation
|
|
1361
1457
|
|
|
1362
1458
|
### 11.7 Other Improvements
|
|
1363
1459
|
|