ciphermesh 1.2.1 โ†’ 2.1.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/README.md CHANGED
@@ -42,7 +42,7 @@ forwarding, survives CGNAT).
42
42
  |-----|---------|----------|
43
43
  | ๐Ÿ” | **Real E2EE** | Curve25519 + XSalsa20-Poly1305 via libsodium, keys in `sodium_malloc` โ€” never touch disk |
44
44
  | ๐Ÿ”„ | **Perfect Forward Secrecy** | Double Ratchet: one key per message, compromise today โ‰  read yesterday |
45
- | ๐Ÿ•ถ๏ธ | **Metadata resistance** | Fixed-bucket length padding on every ciphertext + opt-in cover traffic (`/cover`) to blur when you chat |
45
+ | ๐Ÿ•ถ๏ธ | **Metadata resistance** | **Sealed sender** โ€” the relay never sees who sent a message โ€” plus fixed-bucket length padding on every ciphertext and opt-in cover traffic (`/cover`) |
46
46
  | ๐Ÿ•ต๏ธ | **TOFU + SAS** | Key-change detection (MITM alarm), 6-digit voice-verifiable codes, and inline **โœ“/โœ—** trust badges next to names |
47
47
  | ๐ŸŒ | **LAN & internet** | Auto-detects Tailscale, shows the reachable address in the banner |
48
48
  | ๐Ÿ“จ | **Invites with QR** | `/invite` prints a `ciphermesh://` string + QR โ€” paste it, you're in the right room |
@@ -53,7 +53,9 @@ forwarding, survives CGNAT).
53
53
  | ๐Ÿ’ฌ | **Modern chat feel** | Right-aligned own messages, per-user emoji avatars, replies with quotes, `:fire:` โ†’ ๐Ÿ”ฅ |
54
54
  | ๐ŸŽž๏ธ | **Animated UI** | Splash intro, reconnect spinner, live transfer bars (shimmer + ETA), a lock-closing handshake on connect, and a pulsing "new messages โ†“" pill |
55
55
  | ๐Ÿ‘ป | **Deniable & ephemeral** | Symmetric-crypto deniable mode; ephemeral messages *burn away* char-by-char when they expire |
56
+ | ๐Ÿ”’ | **Private rooms** | `/create <room> <password>` โ€” zero-knowledge: the password never leaves your machine (Argon2id โ†’ Ed25519 challenge-response) and room content gets an extra symmetric layer the relay can't fake its way into |
56
57
  | ๐Ÿ›ฐ๏ธ | **Serverless P2P mode** | mDNS peer discovery on the LAN โ€” no relay at all |
58
+ | ๐Ÿงฉ | **Plugins** | Drop a JS file in `~/.ciphermesh/plugins` and get new slash-commands โ€” `/roll` and `/poll` examples included ([Plugin API](docs/PLUGINS.md)) |
57
59
 
58
60
  ## ๐Ÿš€ Quick start
59
61
 
@@ -131,12 +133,21 @@ QR code) to whoever you want to pull in.
131
133
 
132
134
  | Command | Description |
133
135
  |---------|-------------|
134
- | `/join <room>` | Enter/create a room |
135
- | `/rooms` | List rooms |
136
+ | `/join <room> [password]` | Enter/create a room (password if it's private) |
137
+ | `/create <room> <password>` | Create a **private room** ๐Ÿ”’ โ€” see below |
138
+ | `/rooms` | List rooms (๐Ÿ”’ marks private ones) |
136
139
  | `/room` | Current room |
137
140
  | `/owner` | Room owner |
138
141
  | `/kick` `/mute` `/ban` | Owner moderation |
139
142
 
143
+ **Private rooms** are zero-knowledge: the password never leaves your machine.
144
+ Joining derives an Ed25519 key from the password (Argon2id) and answers a
145
+ server challenge with a signature โ€” the server stores only a verifier, in
146
+ memory, and it dies when the last member leaves (rooms are always ephemeral).
147
+ On top of that, everything said in a private room carries an extra symmetric
148
+ layer keyed by the password, so even a malicious relay that let someone in
149
+ without verifying couldn't read a word. Share the password out-of-band.
150
+
140
151
  </details>
141
152
 
142
153
  <details>
@@ -148,8 +159,10 @@ QR code) to whoever you want to pull in.
148
159
  | `/verify <nick>` | SAS code (~40-bit) + QR + key randomart for out-of-band verification |
149
160
  | `/verify-confirm <nick>` | Mark peer as verified |
150
161
  | `/trust <nick>` / `/trustlist` | Accept new key / trust status |
162
+ | `/contacts [add\|remove\|all]` | Contact book โ€” persistent aliases on trust records ("this fingerprint is Joรฃo"); shows in `/users`, rides along in identity backups |
151
163
  | `/backup [path]` | Encrypted backup of identity + verified peers (restore at startup) |
152
164
  | `/deniable [on\|off]` | Plausible-deniability mode |
165
+ | `/lock` / `/autolock <min\|off>` | Lock the screen behind the session passphrase โ€” manually or after idle time (`autoLock` in config). Privacy for the "stepped away" moment; `/panic` is for the worst one |
153
166
  | `/panic [yes]` | Duress wipe โ€” securely erase all on-disk secrets (session, history, trust, keys) and exit |
154
167
  | `/cover [on\|constant\|off]` | Cover traffic โ€” `on` = jittered decoys, `constant` = steady-rate paced channel |
155
168
  | `/theme [name]` | Nick colour theme: neon, matrix, mono, sunset, ocean |
@@ -183,7 +196,8 @@ A green **โœ“** next to a name marks a SAS-verified peer; a red **โœ—** flags a
183
196
 
184
197
  | Command | Description |
185
198
  |---------|-------------|
186
- | `/away [reason]` / `/back` | Mark yourself away |
199
+ | `/away [reason]` / `/back` | Mark yourself away โ€” while away, unreads are counted (`[away ยท N new]`) and `/back` shows a summary |
200
+ | `/mentions [n]` | Recent mentions of you this session (who, where, when) |
187
201
  | `/status <text\|off>` | Free-form status โ€” emojis welcome (`/status :fire: coding`) |
188
202
  | `/react <emoji>` | React to the last message |
189
203
  | `/edit` `/delete` | Edit/delete your last message |
@@ -194,11 +208,23 @@ A green **โœ“** next to a name marks a SAS-verified peer; a red **โœ—** flags a
194
208
 
195
209
  </details>
196
210
 
197
- Typing `:fire:` anywhere becomes ๐Ÿ”ฅ (Tab autocompletes shortcodes). **Ctrl+K** opens a fuzzy command palette, **Ctrl+E** an emoji picker. PageUp/PageDown scroll the history. **Alt+Enter** (or Shift+Enter where the terminal supports it, plus Ctrl+J) inserts a newline for multi-line messages; Enter sends. Markdown works: \`code\`, **bold**, *italic*, links, plus fenced \`\`\` code blocks and | tables |. Received images preview inline (half-blocks) and render full-res with `/img` on kitty/iTerm2. Day separators and message grouping keep the log clean.
211
+ Typing `:fire:` anywhere becomes ๐Ÿ”ฅ (Tab autocompletes shortcodes). **Ctrl+K** opens a fuzzy command palette, **Ctrl+E** an emoji picker. PageUp/PageDown scroll the history. **Alt+Enter** (or Shift+Enter where the terminal supports it, plus Ctrl+J) inserts a newline for multi-line messages; Enter sends. Pasting multi-line text (code included) keeps its line breaks โ€” paste, check, Enter. Markdown works: \`code\`, **bold**, *italic*, links, plus fenced \`\`\` code blocks and | tables |. Received images preview inline (half-blocks) and render full-res with `/img` on kitty/iTerm2. Day separators and message grouping keep the log clean.
212
+
213
+ ### First run & config file
214
+
215
+ On your very first run, a **30-second setup wizard** walks you through nickname,
216
+ colour theme and default server (with a 3-line crash course on how the
217
+ encryption works), then saves everything so later runs go straight to the chat.
218
+ Re-run it anytime with `ciphermesh --setup`; skip it in scripts/CI with
219
+ `--no-onboard`.
198
220
 
199
- ### Config file
221
+ CipherMesh also remembers your **last session**: the server prompt defaults to
222
+ where you were, and after connecting you land back in your last room
223
+ automatically (private rooms excluded โ€” their names never touch disk). Start
224
+ clean with `ciphermesh --fresh`.
200
225
 
201
- Drop a `~/.ciphermesh/config.json` to set defaults and skip the prompts. All keys are optional (unknown keys are ignored):
226
+ The wizard writes `~/.ciphermesh/config.json` โ€” you can also edit it by hand.
227
+ All keys are optional (unknown keys are ignored):
202
228
 
203
229
  ```json
204
230
  {
@@ -211,6 +237,7 @@ Drop a `~/.ciphermesh/config.json` to set defaults and skip the prompts. All key
211
237
  "cover": "constant",
212
238
  "theme": "matrix",
213
239
  "autoAway": 10,
240
+ "autoLock": 5,
214
241
  "dnd": "22:00-08:00"
215
242
  }
216
243
  ```
package/README.pt-BR.md CHANGED
@@ -42,7 +42,7 @@ forwarding, imune a CGNAT).
42
42
  |-----|---------|--------|
43
43
  | ๐Ÿ” | **E2EE de verdade** | Curve25519 + XSalsa20-Poly1305 via libsodium, chaves em `sodium_malloc` โ€” nunca tocam o disco |
44
44
  | ๐Ÿ”„ | **Perfect Forward Secrecy** | Double Ratchet: uma chave por mensagem โ€” comprometer hoje โ‰  ler ontem |
45
- | ๐Ÿ•ถ๏ธ | **Resistรชncia a metadados** | Padding de comprimento em buckets fixos em todo ciphertext + cover traffic opcional (`/cover`) pra borrar quando vocรช conversa |
45
+ | ๐Ÿ•ถ๏ธ | **Resistรชncia a metadados** | **Sealed sender** โ€” o relay nunca vรช quem enviou a mensagem โ€” + padding de comprimento em buckets fixos em todo ciphertext e cover traffic opcional (`/cover`) |
46
46
  | ๐Ÿ•ต๏ธ | **TOFU + SAS** | Alarme de troca de chave (MITM), cรณdigo de 6 dรญgitos verificรกvel por voz e badges de confianรงa **โœ“/โœ—** inline ao lado dos nomes |
47
47
  | ๐ŸŒ | **LAN e internet** | Detecta Tailscale sozinho e mostra o endereรงo alcanรงรกvel no banner |
48
48
  | ๐Ÿ“จ | **Convites com QR** | `/invite` gera uma string `ciphermesh://` + QR โ€” colou, caiu na sala certa |
@@ -53,7 +53,9 @@ forwarding, imune a CGNAT).
53
53
  | ๐Ÿ’ฌ | **Cara de app moderno** | Suas mensagens ร  direita, avatar de emoji por usuรกrio, reply com citaรงรฃo, `:fire:` โ†’ ๐Ÿ”ฅ |
54
54
  | ๐ŸŽž๏ธ | **Interface animada** | Splash na abertura, spinner de reconexรฃo, barra de transferรชncia viva (shimmer + ETA), cadeado fechando no handshake e um selo pulsante "novas mensagens โ†“" |
55
55
  | ๐Ÿ‘ป | **Deniable e efรชmeras** | Modo de negaรงรฃo plausรญvel (crypto simรฉtrica); mensagens efรชmeras *queimam* caractere a caractere ao expirar |
56
+ | ๐Ÿ”’ | **Salas privadas** | `/create <sala> <senha>` โ€” zero-knowledge: a senha nunca sai da sua mรกquina (Argon2id โ†’ challenge-response Ed25519) e o conteรบdo da sala ganha uma camada simรฉtrica extra que nem um relay malicioso atravessa |
56
57
  | ๐Ÿ›ฐ๏ธ | **Modo P2P sem servidor** | Descoberta de peers via mDNS na LAN โ€” sem relay nenhum |
58
+ | ๐Ÿงฉ | **Plugins** | Solta um arquivo JS em `~/.ciphermesh/plugins` e ganha comandos novos โ€” exemplos `/roll` e `/poll` inclusos ([API de plugins](docs/PLUGINS.md)) |
57
59
 
58
60
  ## ๐Ÿš€ Comeรงando
59
61
 
@@ -131,12 +133,21 @@ quem vocรช quiser puxar pra conversa.
131
133
 
132
134
  | Comando | Descriรงรฃo |
133
135
  |---------|-----------|
134
- | `/join <sala>` | Entra/cria uma sala |
135
- | `/rooms` | Lista salas |
136
+ | `/join <sala> [senha]` | Entra/cria uma sala (senha se for privada) |
137
+ | `/create <sala> <senha>` | Cria uma **sala privada** ๐Ÿ”’ โ€” veja abaixo |
138
+ | `/rooms` | Lista salas (๐Ÿ”’ marca as privadas) |
136
139
  | `/room` | Sala atual |
137
140
  | `/owner` | Dono da sala |
138
141
  | `/kick` `/mute` `/ban` | Moderaรงรฃo (dono da sala) |
139
142
 
143
+ **Salas privadas** sรฃo zero-knowledge: a senha nunca sai da sua mรกquina. Ao
144
+ entrar, o cliente deriva uma chave Ed25519 da senha (Argon2id) e responde um
145
+ desafio do servidor com uma assinatura โ€” o servidor guarda sรณ um verificador,
146
+ em memรณria, que morre quando a รบltima pessoa sai (salas sรฃo sempre efรชmeras).
147
+ Alรฉm disso, tudo que รฉ dito numa sala privada carrega uma camada simรฉtrica
148
+ extra derivada da senha: mesmo um relay malicioso que deixasse alguรฉm entrar
149
+ sem verificar nรฃo leria uma palavra. Combine a senha por outro canal.
150
+
140
151
  </details>
141
152
 
142
153
  <details>
@@ -149,7 +160,9 @@ quem vocรช quiser puxar pra conversa.
149
160
  | `/verify-confirm <nick>` | Marca o peer como verificado |
150
161
  | `/backup [caminho]` | Backup cifrado da identidade + peers verificados (restaura no startup) |
151
162
  | `/trust <nick>` / `/trustlist` | Aceita chave nova / status de confianรงa |
163
+ | `/contacts [add\|remove\|all]` | Agenda โ€” apelidos persistentes nos registros de confianรงa ("esse fingerprint รฉ o Joรฃo"); aparece no `/users` e viaja no backup de identidade |
152
164
  | `/deniable [on\|off]` | Modo de negaรงรฃo plausรญvel |
165
+ | `/lock` / `/autolock <min\|off>` | Trava a tela atrรกs da passphrase da sessรฃo โ€” na mรฃo ou apรณs inatividade (`autoLock` no config). Privacidade para o "saรญ um minuto"; o `/panic` รฉ para o pior minuto |
153
166
  | `/panic [sim]` | Wipe de coaรงรฃo โ€” apaga com seguranรงa todos os segredos do disco (sessรฃo, histรณrico, confianรงa, chaves) e sai |
154
167
  | `/cover [on\|constant\|off]` | Cover traffic โ€” `on` = iscas com jitter, `constant` = canal de taxa constante |
155
168
  | `/theme [nome]` | Tema de cores dos nicks: neon, matrix, mono, sunset, ocean |
@@ -183,7 +196,8 @@ Um **โœ“** verde ao lado de um nome indica um peer verificado por SAS; um **โœ—*
183
196
 
184
197
  | Comando | Descriรงรฃo |
185
198
  |---------|-----------|
186
- | `/away [motivo]` / `/back` | Marca/remove ausรชncia |
199
+ | `/away [motivo]` / `/back` | Marca/remove ausรชncia โ€” enquanto ausente, nรฃo-lidas sรฃo contadas (`[away ยท N new]`) e o `/back` mostra um resumo |
200
+ | `/mentions [n]` | Menรงรตes recentes a vocรช na sessรฃo (quem, onde, quando) |
187
201
  | `/status <texto\|off>` | Status livre โ€” emoji ร  vontade (`/status :fire: codando`) |
188
202
  | `/react <emoji>` | Reage ร  รบltima mensagem |
189
203
  | `/edit` `/delete` | Edita/apaga sua รบltima mensagem |
@@ -195,11 +209,23 @@ Um **โœ“** verde ao lado de um nome indica um peer verificado por SAS; um **โœ—*
195
209
  </details>
196
210
 
197
211
  Digitar `:fire:` em qualquer lugar vira ๐Ÿ”ฅ (Tab autocompleta shortcodes).
198
- **Ctrl+K** abre uma paleta de comandos fuzzy, **Ctrl+E** um seletor de emoji. PageUp/PageDown rolam o histรณrico. **Alt+Enter** (ou Shift+Enter onde o terminal suporta, alรฉm de Ctrl+J) insere uma nova linha para mensagens de vรกrias linhas; Enter envia. Markdown funciona: \`cรณdigo\`, **negrito**, *itรกlico*, links, alรฉm de blocos de cรณdigo \`\`\` e | tabelas |. Imagens recebidas tรชm preview inline (half-blocks) e renderizam em alta resoluรงรฃo com `/img` no kitty/iTerm2. Separadores de dia e agrupamento de mensagens deixam o log limpo.
212
+ **Ctrl+K** abre uma paleta de comandos fuzzy, **Ctrl+E** um seletor de emoji. PageUp/PageDown rolam o histรณrico. **Alt+Enter** (ou Shift+Enter onde o terminal suporta, alรฉm de Ctrl+J) insere uma nova linha para mensagens de vรกrias linhas; Enter envia. Colar texto multi-linha (cรณdigo incluso) preserva as quebras โ€” cola, confere, Enter. Markdown funciona: \`cรณdigo\`, **negrito**, *itรกlico*, links, alรฉm de blocos de cรณdigo \`\`\` e | tabelas |. Imagens recebidas tรชm preview inline (half-blocks) e renderizam em alta resoluรงรฃo com `/img` no kitty/iTerm2. Separadores de dia e agrupamento de mensagens deixam o log limpo.
213
+
214
+ ### Primeira execuรงรฃo & arquivo de config
215
+
216
+ Na primeirรญssima execuรงรฃo, um **wizard de 30 segundos** te guia por nickname,
217
+ tema de cores e servidor padrรฃo (com um mini-curso de 3 linhas de como a
218
+ criptografia funciona) e salva tudo โ€” das prรณximas vezes vocรช cai direto no
219
+ chat. Refaรงa quando quiser com `ciphermesh --setup`; pule em scripts/CI com
220
+ `--no-onboard`.
199
221
 
200
- ### Arquivo de config
222
+ O CipherMesh tambรฉm lembra a sua **รบltima sessรฃo**: o prompt de servidor passa
223
+ a ter como padrรฃo onde vocรช estava, e depois de conectar vocรช volta sozinho
224
+ para a รบltima sala (salas privadas ficam de fora โ€” o nome delas nunca toca o
225
+ disco). Para comeรงar do zero: `ciphermesh --fresh`.
201
226
 
202
- Crie um `~/.ciphermesh/config.json` para definir padrรตes e pular os prompts. Todas as chaves sรฃo opcionais (chaves desconhecidas sรฃo ignoradas):
227
+ O wizard grava o `~/.ciphermesh/config.json` โ€” que vocรช tambรฉm pode editar na
228
+ mรฃo. Todas as chaves sรฃo opcionais (chaves desconhecidas sรฃo ignoradas):
203
229
 
204
230
  ```json
205
231
  {
@@ -212,6 +238,7 @@ Crie um `~/.ciphermesh/config.json` para definir padrรตes e pular os prompts. To
212
238
  "cover": "constant",
213
239
  "theme": "matrix",
214
240
  "autoAway": 10,
241
+ "autoLock": 5,
215
242
  "dnd": "22:00-08:00"
216
243
  }
217
244
  ```
package/bin/ciphermesh.js CHANGED
@@ -19,6 +19,11 @@ Usage:
19
19
  Options:
20
20
  -h, --help show this help
21
21
  -v, --version show the version
22
+
23
+ Client options:
24
+ --setup re-run the first-run setup wizard
25
+ --no-onboard skip the first-run wizard (scripts/CI)
26
+ --fresh forget the last session (server/room) and start clean
22
27
  `;
23
28
 
24
29
  const arg = process.argv[2];
@@ -448,7 +448,12 @@ Types:
448
448
  | `peer_left` | Server -> Clients | A peer left |
449
449
  | `key_exchange` | Client -> Server -> Client | Public key exchange between peers |
450
450
  | `encrypted_message` | Client -> Server -> Client | Encrypted message |
451
- | `error` | Server -> Client | Error (duplicate nickname, etc.) |
451
+ | `change_room` | Client -> Server | Enter/create a room (+ optional `roomAuthPk` verifier when creating a private room) |
452
+ | `room_changed` | Server -> Client | Room switch confirmed (+ `private` flag) |
453
+ | `list_rooms` / `room_list` | Client <-> Server | Room list (each room carries a `private` flag) |
454
+ | `room_challenge` | Server -> Client | Target room is private โ€” sign this nonce (see 6.9) |
455
+ | `room_auth` | Client -> Server | Ed25519 signature proving password knowledge (see 6.9) |
456
+ | `error` | Server -> Client | Error (duplicate nickname, wrong room password, etc.) |
452
457
  | `ping` / `pong` | Bidirectional | Heartbeat |
453
458
 
454
459
  #### `src/protocol/validators.js` โ€” Validation
@@ -533,18 +538,14 @@ export const FILE_CHUNK_SIZE = 49152; // 48KB
533
538
  ```json
534
539
  {
535
540
  "type": "encrypted_message",
536
- "version": 1,
541
+ "version": 2,
537
542
  "timestamp": 1739800001000,
538
- "from": "550e8400-e29b-41d4-a716-446655440000",
539
543
  "to": "660e8400-e29b-41d4-a716-446655440001",
540
- "payload": {
541
- "ciphertext": "base64(mensagem cifrada com crypto_box_easy)",
542
- "nonce": "base64(24 bytes do nonce usado)"
543
- }
544
+ "sealed": "base64( crypto_box_seal({ from, payload }, recipientPublicKey) )"
544
545
  }
545
546
  ```
546
547
 
547
- **Note**: The `payload` field is completely opaque to the server. It only reads `from` and `to` for routing.
548
+ **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.)
548
549
 
549
550
  ### 5.4 Decrypted content (never travels in cleartext)
550
551
 
@@ -737,6 +738,41 @@ fingerprint = SHA-256(publicKey)
737
738
  = "A1B2:C3D4:E5F6:7890"
738
739
  ```
739
740
 
741
+ ### 6.9 Private Rooms (password-protected, zero-knowledge)
742
+
743
+ `/create <room> <password>` creates a room the server can gate **without ever
744
+ seeing the password** (`src/crypto/RoomKey.js`):
745
+
746
+ ```
747
+ seed(64B) = Argon2id(password, salt = BLAKE2b("ciphermesh/room-v1:" + room))
748
+ โ”œโ”€โ”€ authSeed (32B) โ†’ Ed25519 keypair (join authentication)
749
+ โ””โ”€โ”€ roomKey (32B) โ†’ secretbox key (room content layer)
750
+ ```
751
+
752
+ **Join authentication (challenge-response):**
753
+ ```
754
+ 1. Creator sends change_room + roomAuthPk (the Ed25519 PUBLIC key).
755
+ The server stores it as the room's verifier โ€” in memory only.
756
+ 2. A joiner sends change_room โ†’ server replies room_challenge { nonce }.
757
+ 3. The joiner signs "ciphermesh/room-auth-v1:room:nonce:sessionId" with the
758
+ password-derived secret key โ†’ room_auth { signature }.
759
+ 4. The server verifies against the stored verifier. Wrong password โ‡’ a
760
+ different keypair โ‡’ invalid signature โ‡’ ROOM_AUTH_FAILED.
761
+ ```
762
+
763
+ The signature binds room, nonce **and** sessionId, so it can't be replayed by
764
+ another connection. Wrong-password attempts are throttled per connection, and
765
+ each guess costs the attacker a full Argon2id derivation client-side. When the
766
+ last member leaves, the room dies and the verifier with it โ€” same ephemeral
767
+ lifecycle as public rooms.
768
+
769
+ **Content layer:** every payload sent in a private room is wrapped as
770
+ `{ rk: 1, n, c }` โ€” `crypto_secretbox(payload, roomKey)` โ€” *before* the normal
771
+ pairwise encryption (ratchet/sealed sender). So even a malicious relay that
772
+ skipped verification and injected a member could not read the room: reading
773
+ requires the password. The outer layer already pads ciphertexts, so the
774
+ wrapper adds no metadata leak.
775
+
740
776
  ---
741
777
 
742
778
  ## 7. Handshake Protocol
@@ -939,17 +975,29 @@ a server change and is not wired up there yet.)
939
975
  | **Denial of Service** | Rate limiting + maxPayload | Partial |
940
976
  | **Forward secrecy** | Double Ratchet โ€” a unique key per message | Mitigated |
941
977
  | **Metadata analysis** | Message padding + fixed sizes | Partial |
978
+ | **Relay learns who SENT each message** | Sealed sender โ€” the sender + payload are sealed to the recipient; the relay routes by recipient only and never stamps, stores, or logs a sender | Partial (honest relay) |
942
979
 
943
980
  ### 10.2 What the server CAN deduce (metadata)
944
981
 
945
982
  Even without reading content, the server knows:
946
- - **Who** is online
947
- - **Who** talks to whom
983
+ - **Who** is online (the roster: nicknames + public keys, from JOIN)
984
+ - **To whom** each message is routed (the recipient) โ€” *the **sender** is hidden by sealed sender; an honest relay never learns who sent a given message*
948
985
  - **When** messages are sent โ€” *mitigated by cover traffic (optional)*
949
986
  - **Approximate size** of messages โ€” *mitigated: only the padding bucket leaks*
950
987
  - **Frequency** of communication โ€” *mitigated by cover traffic (optional)*
951
988
 
952
989
  Implemented mitigations:
990
+ - **Sealed sender** (`crypto/SealedSender.js`, protocol v2): every
991
+ `encrypted_message` carries only `to` + an opaque `sealed` blob. The sender's
992
+ identity and the inner payload are wrapped in a libsodium `crypto_box_seal`
993
+ (anonymous box) to the recipient's key; the relay routes by recipient and
994
+ **never stamps, stores, or logs who sent a message**. *Honest guarantee &
995
+ limits:* this protects against an honest-but-curious relay, offline-queue
996
+ compromise, and network observers โ€” the same model as Signal's sealed sender.
997
+ It does **not** anonymise the sender against a *malicious* relay that
998
+ correlates the sending socket (inherent to a persistent authenticated
999
+ connection), the **recipient** is still visible (routing needs it), and P2P
1000
+ mode has no relay at all.
953
1001
  - **Length padding** (`MessageCrypto.padMessage`, buckets
954
1002
  `[128..32768]`): applied on all three encryption paths (static, ratchet, and
955
1003
  deniable) before encrypting. The server sees only which bucket, not the real size.
@@ -0,0 +1,69 @@
1
+ # CipherMesh Plugin API
2
+
3
+ CipherMesh loads user plugins at startup from `~/.ciphermesh/plugins/*.js` and
4
+ routes unknown slash-commands to them. Plugins work in both relay and P2P mode.
5
+
6
+ ## Quick start
7
+
8
+ ```bash
9
+ mkdir -p ~/.ciphermesh/plugins
10
+ cp examples/plugins/roll.js examples/plugins/poll.js ~/.ciphermesh/plugins/
11
+ ```
12
+
13
+ Restart the client โ€” `/plugins` lists what loaded, and `/roll 2d20+3` /
14
+ `/poll Pizza tonight? | yes | obviously` just work.
15
+
16
+ ## Plugin format
17
+
18
+ A plugin is an ES module whose **default export** is:
19
+
20
+ ```js
21
+ export default {
22
+ name: 'roll', // required, unique
23
+ description: 'Roll dice', // optional, shown by /plugins
24
+ commands: {
25
+ // key = command name (with or without the leading slash)
26
+ roll(args) {
27
+ // args: string[] โ€” everything typed after the command, split on spaces
28
+ return { send: '๐ŸŽฒ 4' };
29
+ },
30
+ },
31
+ };
32
+ ```
33
+
34
+ Files that fail to import, or lack `name`/`commands`, are skipped silently.
35
+
36
+ ## Handler return values
37
+
38
+ | Return | Effect |
39
+ |--------|--------|
40
+ | `{ send: '<text>' }` | The text is **sent to the current room** as a normal end-to-end-encrypted message (and echoed locally). Markdown and multi-line text work. |
41
+ | `{ info: '<text>' }` | Shown **only locally** as an info line. |
42
+ | `'<text>'` (plain string) | Same as `{ info }` โ€” the original API, still supported. |
43
+ | `null` / `undefined` / throw | Treated as "not handled": the user sees *Unknown command*. |
44
+
45
+ Handlers are synchronous โ€” return the final value directly.
46
+
47
+ ## Precedence and naming
48
+
49
+ Built-in commands always win: a plugin command named `/help` or `/join` is
50
+ never reached. Plugin commands are only tried when no built-in matches. If two
51
+ plugins register the same command, the one loaded last wins (load order is the
52
+ directory listing).
53
+
54
+ ## Security model โ€” read this
55
+
56
+ A plugin is **arbitrary JavaScript running inside your chat process**, with
57
+ your privileges and full access to your keys in memory. There is no sandbox.
58
+
59
+ - Only install plugins you wrote or read line-by-line.
60
+ - Treat a plugin file like you treat `curl | sh`.
61
+ - Plugins are never synced, auto-updated or downloaded by CipherMesh โ€” the
62
+ only way code gets into `~/.ciphermesh/plugins/` is you putting it there.
63
+
64
+ ## Included examples
65
+
66
+ - [`examples/plugins/roll.js`](../examples/plugins/roll.js) โ€” dice roller,
67
+ D&D notation (`/roll 2d20+3`), result goes to the room.
68
+ - [`examples/plugins/poll.js`](../examples/plugins/poll.js) โ€” quick poll
69
+ (`/poll question | opt A | opt B`), voted with the built-in `/react`.
@@ -0,0 +1,28 @@
1
+ // CipherMesh example plugin: quick poll voted with reactions.
2
+ // Install: cp poll.js ~/.ciphermesh/plugins/
3
+ const NUMBERS = ['1๏ธโƒฃ', '2๏ธโƒฃ', '3๏ธโƒฃ', '4๏ธโƒฃ', '5๏ธโƒฃ', '6๏ธโƒฃ', '7๏ธโƒฃ', '8๏ธโƒฃ', '9๏ธโƒฃ'];
4
+
5
+ export default {
6
+ name: 'poll',
7
+ description: 'Create a poll: /poll <question> | <option> | <option> [| โ€ฆ]',
8
+ commands: {
9
+ poll(args) {
10
+ const raw = args.join(' ');
11
+ if (!raw.includes('|')) {
12
+ return { info: 'Usage: /poll <question> | <option> | <option> [| โ€ฆ]' };
13
+ }
14
+ const [question, ...options] = raw
15
+ .split('|')
16
+ .map((s) => s.trim())
17
+ .filter(Boolean);
18
+ if (!question || options.length < 2 || options.length > NUMBERS.length) {
19
+ return { info: `A poll needs a question and 2-${NUMBERS.length} options.` };
20
+ }
21
+ const lines = [`๐Ÿ“Š **${question}**`];
22
+ options.forEach((opt, i) => lines.push(`${NUMBERS[i]} ${opt}`));
23
+ lines.push(`Vote by reacting โ€” e.g. /react ${NUMBERS[0]}`);
24
+ // `send` โ†’ delivered to the whole room as a normal E2EE message.
25
+ return { send: lines.join('\n') };
26
+ },
27
+ },
28
+ };
@@ -0,0 +1,24 @@
1
+ // CipherMesh example plugin: dice roller (D&D notation).
2
+ // Install: cp roll.js ~/.ciphermesh/plugins/
3
+ export default {
4
+ name: 'roll',
5
+ description: 'Roll dice: /roll [NdM+K] โ€” e.g. /roll, /roll d20, /roll 2d20+3',
6
+ commands: {
7
+ roll(args) {
8
+ const spec = (args[0] || 'd6').toLowerCase();
9
+ const m = spec.match(/^(\d{0,2})d(\d{1,4})([+-]\d{1,4})?$/);
10
+ if (!m) {
11
+ return { info: 'Usage: /roll [NdM(+K)] โ€” e.g. /roll 2d20+3' };
12
+ }
13
+ const count = Math.min(parseInt(m[1] || '1', 10) || 1, 20);
14
+ const sides = Math.max(2, parseInt(m[2], 10));
15
+ const modifier = parseInt(m[3] || '0', 10);
16
+ const rolls = Array.from({ length: count }, () => 1 + Math.floor(Math.random() * sides));
17
+ const total = rolls.reduce((a, b) => a + b, 0) + modifier;
18
+ const detail =
19
+ count > 1 || modifier ? ` (${rolls.join(' + ')}${modifier ? ` ${m[3]}` : ''})` : '';
20
+ // `send` โ†’ delivered to the whole room as a normal E2EE message.
21
+ return { send: `๐ŸŽฒ rolled ${spec}: **${total}**${detail}` };
22
+ },
23
+ },
24
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ciphermesh",
3
- "version": "1.2.1",
3
+ "version": "2.1.0",
4
4
  "description": "Secure terminal chat for the local network (LAN) with real end-to-end encryption (E2EE) using libsodium",
5
5
  "type": "module",
6
6
  "main": "src/client/index.js",
@@ -11,6 +11,7 @@
11
11
  "bin",
12
12
  "src",
13
13
  "docs",
14
+ "examples",
14
15
  "README.md",
15
16
  "README.pt-BR.md",
16
17
  "LICENSE"