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 +34 -7
- package/README.pt-BR.md +34 -7
- package/bin/ciphermesh.js +5 -0
- package/docs/ARCHITECTURE.md +58 -10
- package/docs/PLUGINS.md +69 -0
- package/examples/plugins/poll.js +28 -0
- package/examples/plugins/roll.js +24 -0
- package/package.json +2 -1
- package/src/client/ChatController.js +429 -14
- package/src/client/UI.js +128 -4
- package/src/client/index.js +50 -7
- package/src/crypto/RoomKey.js +162 -0
- package/src/crypto/TrustStore.js +47 -0
- package/src/p2p/P2PChatController.js +9 -1
- package/src/protocol/messages.js +38 -4
- package/src/protocol/validators.js +31 -25
- package/src/server/MessageRouter.js +3 -1
- package/src/server/SessionManager.js +26 -2
- package/src/server/WebSocketServer.js +143 -3
- package/src/shared/AuditLog.js +3 -0
- package/src/shared/PluginManager.js +9 -6
- package/src/shared/config.js +27 -2
- package/src/shared/constants.js +9 -1
- package/src/shared/lastSession.js +55 -0
- package/src/shared/onboarding.js +111 -0
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** |
|
|
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
|
|
135
|
-
| `/
|
|
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
|
-
|
|
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
|
-
|
|
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** |
|
|
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
|
|
135
|
-
| `/
|
|
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
|
-
|
|
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
|
-
|
|
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];
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -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
|
-
| `
|
|
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":
|
|
541
|
+
"version": 2,
|
|
537
542
|
"timestamp": 1739800001000,
|
|
538
|
-
"from": "550e8400-e29b-41d4-a716-446655440000",
|
|
539
543
|
"to": "660e8400-e29b-41d4-a716-446655440001",
|
|
540
|
-
"
|
|
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 `
|
|
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
|
-
- **
|
|
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.
|
package/docs/PLUGINS.md
ADDED
|
@@ -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": "
|
|
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"
|