ciphermesh 2.1.0 → 2.3.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,6 +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
+ | 🛡️ | **Hybrid post-quantum** | X25519 **+ ML-KEM-768** folded into the ratchet root — beats "harvest now, decrypt later" while staying ≥ classical security ([details](docs/ARCHITECTURE.md)) |
45
46
  | 🕶️ | **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
47
  | 🕵️ | **TOFU + SAS** | Key-change detection (MITM alarm), 6-digit voice-verifiable codes, and inline **✓/✗** trust badges next to names |
47
48
  | 🌐 | **LAN & internet** | Auto-detects Tailscale, shows the reachable address in the banner |
@@ -94,6 +95,13 @@ Prefer a prebuilt image? Pull the relay from GHCR (published on each release):
94
95
  docker run -p 3600:3600 ghcr.io/felipekreulich/secret-chat-lan:latest
95
96
  ```
96
97
 
98
+ **No Node on the host?** Every release ships a standalone relay binary for
99
+ macOS and Linux (arm64/x64) — download it from the
100
+ [releases page](https://github.com/FelipeKreulich/secret-chat-lan/releases),
101
+ `chmod +x`, run. Nothing to install. (The TUI client still needs Node/npx: it
102
+ depends on blessed, which resolves its widgets at runtime and cannot be
103
+ bundled.)
104
+
97
105
  **Everyone** (including the host):
98
106
 
99
107
  ```bash
@@ -133,13 +141,19 @@ QR code) to whoever you want to pull in.
133
141
 
134
142
  | Command | Description |
135
143
  |---------|-------------|
136
- | `/join <room> [password]` | Enter/create a room (password if it's private) |
144
+ | `/join <room> [password]` | Open a room as a **new buffer** — you stay in your other rooms (IRC style) |
145
+ | `/leave [room]` | Leave a room; its buffer closes (the last room is protected) |
137
146
  | `/create <room> <password>` | Create a **private room** 🔒 — see below |
138
147
  | `/rooms` | List rooms (🔒 marks private ones) |
139
- | `/room` | Current room |
148
+ | `/room` | Current room + your buffer list |
140
149
  | `/owner` | Room owner |
141
150
  | `/kick` `/mute` `/ban` | Owner moderation |
142
151
 
152
+ **Buffers:** be in several rooms at once — **Alt+1..9** switches, and the status
153
+ bar shows `[1:general] [2:dev •3]` with per-room unread badges. Because the
154
+ relay is blind (sealed sender), which room a message belongs to travels
155
+ *inside* the encrypted payload — the server never learns it.
156
+
143
157
  **Private rooms** are zero-knowledge: the password never leaves your machine.
144
158
  Joining derives an Ed25519 key from the password (Argon2id) and answers a
145
159
  server challenge with a signature — the server stores only a verifier, in
package/README.pt-BR.md CHANGED
@@ -42,6 +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
+ | 🛡️ | **Pós-quântico híbrido** | X25519 **+ ML-KEM-768** misturado na raiz do ratchet — vence o "grava hoje, decifra depois" mantendo segurança ≥ à clássica ([detalhes](docs/ARCHITECTURE.md)) |
45
46
  | 🕶️ | **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
47
  | 🕵️ | **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
48
  | 🌐 | **LAN e internet** | Detecta Tailscale sozinho e mostra o endereço alcançável no banner |
@@ -94,6 +95,13 @@ Prefere imagem pronta? Baixe o relay do GHCR (publicado a cada release):
94
95
  docker run -p 3600:3600 ghcr.io/felipekreulich/secret-chat-lan:latest
95
96
  ```
96
97
 
98
+ **Sem Node na máquina?** Todo release traz um binário standalone do relay para
99
+ macOS e Linux (arm64/x64) — baixe da
100
+ [página de releases](https://github.com/FelipeKreulich/secret-chat-lan/releases),
101
+ `chmod +x`, rode. Nada para instalar. (O cliente TUI ainda precisa de Node/npx:
102
+ ele depende do blessed, que resolve os widgets em runtime e não pode ser
103
+ empacotado.)
104
+
97
105
  **Todo mundo** (incluindo quem hospeda):
98
106
 
99
107
  ```bash
@@ -133,13 +141,19 @@ quem você quiser puxar pra conversa.
133
141
 
134
142
  | Comando | Descrição |
135
143
  |---------|-----------|
136
- | `/join <sala> [senha]` | Entra/cria uma sala (senha se for privada) |
144
+ | `/join <sala> [senha]` | Abre a sala como um **novo buffer** — você continua nas outras (estilo IRC) |
145
+ | `/leave [sala]` | Sai de uma sala; o buffer fecha (a última sala é protegida) |
137
146
  | `/create <sala> <senha>` | Cria uma **sala privada** 🔒 — veja abaixo |
138
147
  | `/rooms` | Lista salas (🔒 marca as privadas) |
139
- | `/room` | Sala atual |
148
+ | `/room` | Sala atual + sua lista de buffers |
140
149
  | `/owner` | Dono da sala |
141
150
  | `/kick` `/mute` `/ban` | Moderação (dono da sala) |
142
151
 
152
+ **Buffers:** esteja em várias salas ao mesmo tempo — **Alt+1..9** alterna, e a
153
+ barra de status mostra `[1:general] [2:dev •3]` com não-lidas por sala. Como o
154
+ relay é cego (sealed sender), a qual sala cada mensagem pertence viaja *dentro*
155
+ do payload cifrado — o servidor nunca fica sabendo.
156
+
143
157
  **Salas privadas** são zero-knowledge: a senha nunca sai da sua máquina. Ao
144
158
  entrar, o cliente deriva uma chave Ed25519 da senha (Argon2id) e responde um
145
159
  desafio do servidor com uma assinatura — o servidor guarda só um verificador,
@@ -0,0 +1,7 @@
1
+ // Entry point for the standalone relay binary (`bun build --compile`).
2
+ //
3
+ // Server-only on purpose: the TUI client depends on blessed, which resolves
4
+ // its widgets through dynamic requires that no bundler can follow. The relay
5
+ // has no such dependency, and "run a CipherMesh relay without installing
6
+ // Node" is exactly what self-hosters need.
7
+ import '../src/server/index.js';
@@ -448,8 +448,11 @@ 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
- | `change_room` | Client -> Server | Enter/create a room (+ optional `roomAuthPk` verifier when creating a private room) |
451
+ | `change_room` | Client -> Server | Legacy single-room switch: leave every room, enter one (+ optional `roomAuthPk` when creating a private room) |
452
452
  | `room_changed` | Server -> Client | Room switch confirmed (+ `private` flag) |
453
+ | `join_room` | Client -> Server | Multi-room: join an ADDITIONAL room, keeping current ones (same `roomAuthPk` option) |
454
+ | `room_joined` | Server -> Client | Additive join confirmed (`room`, `peers`, `private`, `roomOwner`) |
455
+ | `leave_room` / `room_left` | Client <-> Server | Leave one room (the last one is refused — a session is always somewhere) |
453
456
  | `list_rooms` / `room_list` | Client <-> Server | Room list (each room carries a `private` flag) |
454
457
  | `room_challenge` | Server -> Client | Target room is private — sign this nonce (see 6.9) |
455
458
  | `room_auth` | Client -> Server | Ed25519 signature proving password knowledge (see 6.9) |
@@ -738,6 +741,42 @@ fingerprint = SHA-256(publicKey)
738
741
  = "A1B2:C3D4:E5F6:7890"
739
742
  ```
740
743
 
744
+ ### 6.10 Hybrid Post-Quantum Handshake (X25519 + ML-KEM-768)
745
+
746
+ Protects against **"harvest now, decrypt later"**: traffic recorded today stays
747
+ unreadable to a future quantum adversary. The construction is **hybrid** — the
748
+ KEM secret is folded INTO the classical root, never replacing it, so security
749
+ is at least that of X25519 even if ML-KEM were broken (`src/crypto/PQHybrid.js`).
750
+
751
+ ```
752
+ Each client publishes an ML-KEM-768 key alongside its X25519 key (join,
753
+ peer lists, peer_joined). Per pair, at ratchet creation:
754
+
755
+ initiator (lower sessionId):
756
+ (ct, ss) = ML-KEM.Encaps(peer.pqPublicKey)
757
+ root' = BLAKE2b(root ‖ ss ‖ "ciphermesh/pq-hybrid-v3")
758
+ → ct rides in the message envelope (sealed to the recipient)
759
+
760
+ responder, on the first envelope carrying ct:
761
+ ss = ML-KEM.Decaps(ct, mySecretKey)
762
+ root' = BLAKE2b(root ‖ ss ‖ "ciphermesh/pq-hybrid-v3") ← same value
763
+ ```
764
+
765
+ **Why this can't desynchronize:** the mix happens exactly once, at ratchet
766
+ initialization, before any chain key is derived — never mid-stream. There is no
767
+ window in which one side has mixed and the other has not while messages are in
768
+ flight: an envelope without the `ct` simply fails its MAC (fails closed, never
769
+ garbles). Once the peer replies successfully, the initiator stops attaching
770
+ `ct` (~1KB saved per envelope).
771
+
772
+ **Compatibility:** a peer without `pqPublicKey` (pre-2.3 client) gets a
773
+ classical-only session — same behaviour as before. `/trustlist` shows `[PQ]`
774
+ next to peers whose session is hybrid.
775
+
776
+ **Not yet hybrid (documented, planned for v3.1):** sealed-sender envelopes and
777
+ the private-room content layer remain classical; they protect metadata and
778
+ room content respectively, not the message stream's forward secrecy.
779
+
741
780
  ### 6.9 Private Rooms (password-protected, zero-knowledge)
742
781
 
743
782
  `/create <room> <password>` creates a room the server can gate **without ever
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ciphermesh",
3
- "version": "2.1.0",
3
+ "version": "2.3.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",
@@ -51,11 +51,12 @@
51
51
  "docker:logs": "docker compose logs -f"
52
52
  },
53
53
  "dependencies": {
54
+ "@noble/post-quantum": "0.6.1",
54
55
  "blessed": "0.1.81",
55
56
  "bonjour-service": "1.4.3",
56
57
  "boxen": "8.0.1",
57
58
  "chalk": "^5.4.1",
58
- "figlet": "1.11.2",
59
+ "figlet": "1.11.3",
59
60
  "gradient-string": "3.0.0",
60
61
  "jimp": "1.6.1",
61
62
  "node-notifier": "10.0.1",
@@ -65,7 +66,7 @@
65
66
  },
66
67
  "devDependencies": {
67
68
  "@eslint/js": "10.0.1",
68
- "eslint": "10.7.0",
69
+ "eslint": "10.8.0",
69
70
  "globals": "^17.7.0",
70
71
  "prettier": "^3.9.5"
71
72
  },