ciphermesh 1.0.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/LICENSE +21 -0
- package/README.md +251 -0
- package/README.pt-BR.md +253 -0
- package/bin/ciphermesh.js +34 -0
- package/docs/ARCHITECTURE.md +1188 -0
- package/docs/SETUP.md +305 -0
- package/docs/demo.svg +46 -0
- package/package.json +87 -0
- package/src/client/ChatController.js +2476 -0
- package/src/client/Connection.js +129 -0
- package/src/client/FileTransfer.js +488 -0
- package/src/client/ImagePreview.js +88 -0
- package/src/client/UI.js +1830 -0
- package/src/client/index.js +231 -0
- package/src/crypto/CertPinStore.js +79 -0
- package/src/crypto/DeniableEncrypt.js +53 -0
- package/src/crypto/DoubleRatchet.js +574 -0
- package/src/crypto/Handshake.js +219 -0
- package/src/crypto/HistoryStore.js +241 -0
- package/src/crypto/IdentityBackup.js +70 -0
- package/src/crypto/KeyManager.js +134 -0
- package/src/crypto/MessageCrypto.js +181 -0
- package/src/crypto/NonceManager.js +72 -0
- package/src/crypto/SealedSender.js +58 -0
- package/src/crypto/SenderKey.js +204 -0
- package/src/crypto/StateManager.js +138 -0
- package/src/crypto/TrustStore.js +216 -0
- package/src/p2p/Discovery.js +80 -0
- package/src/p2p/P2PChatController.js +1856 -0
- package/src/p2p/PeerConnectionManager.js +252 -0
- package/src/p2p/PeerServer.js +68 -0
- package/src/p2p/index.js +219 -0
- package/src/protocol/messages.js +138 -0
- package/src/protocol/validators.js +175 -0
- package/src/server/CertManager.js +173 -0
- package/src/server/MessageRouter.js +80 -0
- package/src/server/OfflineQueue.js +124 -0
- package/src/server/SessionManager.js +296 -0
- package/src/server/WebSocketServer.js +632 -0
- package/src/server/index.js +89 -0
- package/src/shared/AuditLog.js +91 -0
- package/src/shared/PluginManager.js +83 -0
- package/src/shared/banner.js +271 -0
- package/src/shared/commandSuggest.js +59 -0
- package/src/shared/config.js +90 -0
- package/src/shared/constants.js +126 -0
- package/src/shared/coverTraffic.js +34 -0
- package/src/shared/dnd.js +60 -0
- package/src/shared/emoji.js +17 -0
- package/src/shared/fuzzy.js +40 -0
- package/src/shared/invite.js +61 -0
- package/src/shared/keyArt.js +66 -0
- package/src/shared/logger.js +38 -0
- package/src/shared/panic.js +38 -0
- package/src/shared/prompt.js +31 -0
- package/src/shared/terminalGraphics.js +72 -0
- package/src/shared/themes.js +36 -0
- package/src/shared/voiceNote.js +128 -0
|
@@ -0,0 +1,1188 @@
|
|
|
1
|
+
# SecureLAN Chat — Complete Technical Documentation
|
|
2
|
+
|
|
3
|
+
> Secure chat for a local area network (LAN) with real end-to-end encryption (E2EE).
|
|
4
|
+
> The server **never** has access to the content of messages.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Table of Contents
|
|
9
|
+
|
|
10
|
+
1. [Overview](#1-overview)
|
|
11
|
+
2. [Technology Stack](#2-technology-stack)
|
|
12
|
+
3. [Directory Structure](#3-directory-structure)
|
|
13
|
+
4. [Responsibility of Each Module](#4-responsibility-of-each-module)
|
|
14
|
+
5. [Data Model and Payloads](#5-data-model-and-payloads)
|
|
15
|
+
6. [Detailed Cryptographic Flow](#6-detailed-cryptographic-flow)
|
|
16
|
+
7. [Handshake Protocol](#7-handshake-protocol)
|
|
17
|
+
8. [Step-by-Step Communication Flow](#8-step-by-step-communication-flow)
|
|
18
|
+
9. [Initialization Strategy](#9-initialization-strategy)
|
|
19
|
+
10. [Security — Threat Analysis](#10-security--threat-analysis)
|
|
20
|
+
11. [Future Improvements](#11-future-improvements)
|
|
21
|
+
12. [Glossary](#12-glossary)
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## 1. Overview
|
|
26
|
+
|
|
27
|
+
### What it is
|
|
28
|
+
|
|
29
|
+
SecureLAN Chat 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
|
+
|
|
31
|
+
### Core Principle
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
Cliente A Servidor Cliente B
|
|
35
|
+
| | |
|
|
36
|
+
|--- payload cifrado ----->| |
|
|
37
|
+
| |--- payload cifrado ----->|
|
|
38
|
+
| | |
|
|
39
|
+
| O servidor NAO possui | |
|
|
40
|
+
| a chave para decifrar | |
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The server knows **only**:
|
|
44
|
+
- Who is connected (nickname + session ID)
|
|
45
|
+
- Who sent to whom (routing)
|
|
46
|
+
- The size of the encrypted payload
|
|
47
|
+
- Connection timestamps
|
|
48
|
+
|
|
49
|
+
The server **never** knows:
|
|
50
|
+
- The content of messages
|
|
51
|
+
- Private keys
|
|
52
|
+
- Derived shared keys
|
|
53
|
+
|
|
54
|
+
### Topology
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
┌──────────────┐
|
|
58
|
+
│ Servidor │
|
|
59
|
+
│ (Relay) │
|
|
60
|
+
│ :3600 │
|
|
61
|
+
└──────┬───────┘
|
|
62
|
+
│ WebSocket
|
|
63
|
+
┌────────────┼────────────┐
|
|
64
|
+
│ │ │
|
|
65
|
+
┌─────┴─────┐ ┌───┴───┐ ┌─────┴─────┐
|
|
66
|
+
│ Cliente A │ │ ... │ │ Cliente N │
|
|
67
|
+
│ (Terminal) │ │ │ │ (Terminal) │
|
|
68
|
+
└───────────┘ └───────┘ └───────────┘
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
This is the **star** topology (star topology) — the default mode. All clients connect to the central server. Messages are encrypted before leaving the client and decrypted only at the destination client.
|
|
72
|
+
|
|
73
|
+
### P2P Topology (alternative mode)
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
┌───────────┐
|
|
77
|
+
│ Cliente A │
|
|
78
|
+
│ (Terminal) │
|
|
79
|
+
└─────┬─────┘
|
|
80
|
+
│ WebSocket direto
|
|
81
|
+
┌─────────┼──────────┐
|
|
82
|
+
│ │
|
|
83
|
+
┌───┴───┐ ┌────┴────┐
|
|
84
|
+
│ ... │ │ Cliente N│
|
|
85
|
+
│ │ │(Terminal)│
|
|
86
|
+
└───────┘ └─────────┘
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
In P2P mode (`npm run p2p`), peers discover each other via mDNS on the LAN and connect directly without a central server. Same E2E encryption.
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## 2. Technology Stack
|
|
94
|
+
|
|
95
|
+
### Production Dependencies
|
|
96
|
+
|
|
97
|
+
| Package | Version | Role | Why this lib? |
|
|
98
|
+
|--------|--------|-------|-------------------|
|
|
99
|
+
| **ws** | ^8.18 | WebSocket server/client | The most mature and performant WebSocket implementation for Node.js. Zero dependencies. Natively supports binary frames (essential for encrypted payloads). Used by thousands of projects in production. |
|
|
100
|
+
| **sodium-native** | ^4.3 | Cryptography | Native binding of **libsodium** for Node.js. Runs in compiled C (not pure JS), offering real performance and audited security. libsodium is considered the most secure and easy-to-use modern cryptographic library. Used by Signal, Discord, Wireguard. |
|
|
101
|
+
| **blessed** | ^0.1.81 | Terminal UI | A library for building rich interfaces in the terminal. Supports layouts with boxes, inputs, scrolling, colors, borders — similar to ncurses but in JS. Lets you create a modern app-style UI without leaving the terminal. |
|
|
102
|
+
| **chalk** | ^5.4 | Terminal colors | Colored text formatting in the terminal. Used to highlight nicks, timestamps, errors, status. ESM-only in v5 (compatible with our `"type": "module"`). |
|
|
103
|
+
| **play-sound** | ^1.1 | Audio | Plays audio files (mp3) for sound notifications. Uses the OS's native players (mplayer, aplay, cmdmp3). |
|
|
104
|
+
|
|
105
|
+
### Why sodium-native and not tweetnacl?
|
|
106
|
+
|
|
107
|
+
| Criterion | sodium-native | tweetnacl |
|
|
108
|
+
|----------|--------------|-----------|
|
|
109
|
+
| Implementation | C (compiled libsodium) | Pure JavaScript |
|
|
110
|
+
| Performance | ~100x faster | Slow |
|
|
111
|
+
| Audit | Formally audited | Audited but pure JS |
|
|
112
|
+
| Secure memory | Yes (`sodium.sodium_malloc`) | No |
|
|
113
|
+
| Constant-time | Guaranteed by the C code | Best-effort in JS |
|
|
114
|
+
| Side-channel resistance | High | Low (the JS engine may optimize) |
|
|
115
|
+
|
|
116
|
+
**sodium-native** allocates secure memory that is:
|
|
117
|
+
- Protected against swap (mlock)
|
|
118
|
+
- Zeroed when freed (sodium_memzero)
|
|
119
|
+
- Protected against reads by other processes
|
|
120
|
+
|
|
121
|
+
### Development Dependencies
|
|
122
|
+
|
|
123
|
+
| Package | Role |
|
|
124
|
+
|--------|-------|
|
|
125
|
+
| **eslint** ^9.17 | Linting — ensures code quality and consistency |
|
|
126
|
+
| **@eslint/js** ^9.17 | ESLint's recommended base configuration |
|
|
127
|
+
| **globals** ^15.14 | Globals definitions (node, browser) for ESLint |
|
|
128
|
+
| **prettier** ^3.4 | Automatic formatting — consistent code with no style debates |
|
|
129
|
+
|
|
130
|
+
### Node.js >= 20
|
|
131
|
+
|
|
132
|
+
Minimum requirement: Node.js 20 LTS. Reasons:
|
|
133
|
+
- Native `node:test` (no Jest/Mocha)
|
|
134
|
+
- `node:crypto` with modern APIs
|
|
135
|
+
- Native `--watch` (no nodemon)
|
|
136
|
+
- Stable ESM
|
|
137
|
+
- Improved V8 performance
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## 3. Directory Structure
|
|
142
|
+
|
|
143
|
+
```
|
|
144
|
+
securelan-chat/
|
|
145
|
+
│
|
|
146
|
+
├── docs/
|
|
147
|
+
│ └── ARCHITECTURE.md # Este documento
|
|
148
|
+
│
|
|
149
|
+
├── src/
|
|
150
|
+
│ ├── server/
|
|
151
|
+
│ │ ├── index.js # Entry point do servidor
|
|
152
|
+
│ │ ├── WebSocketServer.js # Gerencia conexoes WebSocket
|
|
153
|
+
│ │ ├── SessionManager.js # Controla sessoes ativas (clientes conectados)
|
|
154
|
+
│ │ ├── MessageRouter.js # Roteia payloads cifrados entre clientes
|
|
155
|
+
│ │ ├── OfflineQueue.js # Fila de mensagens para peers offline
|
|
156
|
+
│ │ └── CertManager.js # Geracao e carregamento de certs TLS
|
|
157
|
+
│ │
|
|
158
|
+
│ ├── client/
|
|
159
|
+
│ │ ├── index.js # Entry point do cliente
|
|
160
|
+
│ │ ├── UI.js # Interface blessed (layout, rendering)
|
|
161
|
+
│ │ ├── Connection.js # Conexao WebSocket com o servidor
|
|
162
|
+
│ │ ├── ChatController.js # Logica central: conecta UI + Connection + Crypto
|
|
163
|
+
│ │ └── FileTransfer.js # Envio/recepcao de arquivos cifrados (chunks)
|
|
164
|
+
│ │
|
|
165
|
+
│ ├── crypto/
|
|
166
|
+
│ │ ├── KeyManager.js # Gera e gerencia pares de chaves (em memoria)
|
|
167
|
+
│ │ ├── MessageCrypto.js # Cifra e decifra mensagens (crypto_box_easy)
|
|
168
|
+
│ │ ├── Handshake.js # Protocolo de troca de chaves publicas
|
|
169
|
+
│ │ ├── NonceManager.js # Geracao e validacao de nonces
|
|
170
|
+
│ │ ├── DoubleRatchet.js # PFS via Double Ratchet (DH ratchet + KDF chains)
|
|
171
|
+
│ │ ├── TrustStore.js # TOFU + SAS (persistencia de fingerprints)
|
|
172
|
+
│ │ └── StateManager.js # Persistencia cifrada de estado (Argon2id + secretbox)
|
|
173
|
+
│ │
|
|
174
|
+
│ ├── p2p/
|
|
175
|
+
│ │ ├── index.js # Entry point do modo P2P
|
|
176
|
+
│ │ ├── Discovery.js # mDNS discovery via bonjour-service
|
|
177
|
+
│ │ ├── PeerServer.js # WebSocket server local (porta aleatoria)
|
|
178
|
+
│ │ ├── PeerConnectionManager.js # Gerencia conexoes outbound/inbound
|
|
179
|
+
│ │ └── P2PChatController.js # Orquestrador P2P (crypto + UI + peers)
|
|
180
|
+
│ │
|
|
181
|
+
│ ├── protocol/
|
|
182
|
+
│ │ ├── messages.js # Definicao dos tipos de mensagem do protocolo
|
|
183
|
+
│ │ └── validators.js # Validacao de estrutura dos payloads
|
|
184
|
+
│ │
|
|
185
|
+
│ └── shared/
|
|
186
|
+
│ ├── constants.js # Constantes globais (portas, limites, versao)
|
|
187
|
+
│ └── logger.js # Logger estruturado (com niveis e timestamps)
|
|
188
|
+
│
|
|
189
|
+
├── test/
|
|
190
|
+
│ ├── crypto.test.js # Testes do modulo criptografico
|
|
191
|
+
│ ├── protocol.test.js # Testes de validacao do protocolo
|
|
192
|
+
│ ├── nonce.test.js # Testes do gerenciador de nonces
|
|
193
|
+
│ ├── integration.test.js # Testes de integracao E2E
|
|
194
|
+
│ ├── double-ratchet.test.js # Testes do Double Ratchet
|
|
195
|
+
│ ├── trust-store.test.js # Testes do TrustStore + SAS
|
|
196
|
+
│ ├── message-crypto.test.js # Testes do MessageCrypto (padding, encrypt)
|
|
197
|
+
│ └── state-manager.test.js # Testes de persistencia de estado
|
|
198
|
+
│
|
|
199
|
+
├── scripts/
|
|
200
|
+
│ └── generate-fingerprint.js # Utilitario: gera fingerprint de chave publica
|
|
201
|
+
│
|
|
202
|
+
├── .editorconfig
|
|
203
|
+
├── .eslintrc.js
|
|
204
|
+
├── .gitignore
|
|
205
|
+
├── .npmrc
|
|
206
|
+
├── .prettierrc
|
|
207
|
+
├── jsconfig.json
|
|
208
|
+
├── package.json
|
|
209
|
+
└── README.md
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
## 4. Responsibility of Each Module
|
|
215
|
+
|
|
216
|
+
### 4.1 Server
|
|
217
|
+
|
|
218
|
+
#### `src/server/index.js` — Entry Point
|
|
219
|
+
- Initializes the WebSocket server on the configured port (default: 3600)
|
|
220
|
+
- Registers connection/disconnection handlers
|
|
221
|
+
- Graceful shutdown (SIGINT, SIGTERM)
|
|
222
|
+
- Prints local network information (IP, port)
|
|
223
|
+
|
|
224
|
+
#### `src/server/WebSocketServer.js` — WebSocket Management
|
|
225
|
+
- Wrapper over `ws.WebSocketServer`
|
|
226
|
+
- Configures limits: `maxPayload` (64KB), heartbeat (ping/pong every 30s)
|
|
227
|
+
- Detects disconnected clients via heartbeat
|
|
228
|
+
- Emits typed events: `connection`, `message`, `close`, `error`
|
|
229
|
+
- Does not interpret content — treats everything as an opaque `Buffer`
|
|
230
|
+
|
|
231
|
+
#### `src/server/SessionManager.js` — Sessions
|
|
232
|
+
- Maintains a map of active sessions: `Map<sessionId, { ws, nickname, publicKey, connectedAt }>`
|
|
233
|
+
- Generates session IDs with `crypto.randomUUID()`
|
|
234
|
+
- Broadcasts the user list when someone joins/leaves
|
|
235
|
+
- Rejects duplicate nicknames
|
|
236
|
+
- Inactive session timeout (configurable)
|
|
237
|
+
- **Stores the public key only to redistribute it** — it is not a secret, it is an identity
|
|
238
|
+
|
|
239
|
+
#### `src/server/MessageRouter.js` — Routing
|
|
240
|
+
- Receives payloads of type `encrypted_message`
|
|
241
|
+
- Validates structure (has `to`, `from`, `payload`)
|
|
242
|
+
- **Does not open `payload`** — only checks the routing fields
|
|
243
|
+
- Forwards to the recipient's WebSocket
|
|
244
|
+
- If the recipient is offline and was recently disconnected, queues it in the OfflineQueue
|
|
245
|
+
- Returns an error if the recipient is not found and has no recent history
|
|
246
|
+
- Basic rate limiting: max 30 messages/second per client
|
|
247
|
+
|
|
248
|
+
#### `src/server/OfflineQueue.js` — Offline Queue
|
|
249
|
+
- Stores encrypted (opaque) messages for disconnected peers
|
|
250
|
+
- Keyed by nickname + publicKey (ensures it only delivers if the peer reconnects with the same key)
|
|
251
|
+
- Limits: max 100 msgs/peer, max 1000 total, max 1h of age
|
|
252
|
+
- If a peer reconnects with a different key (new client), the queue is discarded
|
|
253
|
+
- Periodic cleanup every 5min
|
|
254
|
+
- The queue is lost when the server restarts (in-memory)
|
|
255
|
+
|
|
256
|
+
### 4.2 Client
|
|
257
|
+
|
|
258
|
+
#### `src/client/index.js` — Entry Point
|
|
259
|
+
- Asks the user for a nickname
|
|
260
|
+
- Asks for the server's IP:port (default: localhost:3600)
|
|
261
|
+
- Initializes the KeyManager (generates a key pair)
|
|
262
|
+
- Displays the public key fingerprint for verification
|
|
263
|
+
- Connects to the server and starts the UI
|
|
264
|
+
|
|
265
|
+
#### `src/client/UI.js` — Blessed Interface
|
|
266
|
+
- Layout divided into 3 areas:
|
|
267
|
+
|
|
268
|
+
```
|
|
269
|
+
┌─────────────────────────────────────────┐
|
|
270
|
+
│ SecureLAN Chat [3 online] E2E │ <- Header/Status bar
|
|
271
|
+
├─────────────────────────────────────────┤
|
|
272
|
+
│ │
|
|
273
|
+
│ [10:30] Alice: Ola! │ <- Chat area (scrollable)
|
|
274
|
+
│ [10:31] Voce: Oi Alice! │
|
|
275
|
+
│ [10:32] * Bob entrou no chat │
|
|
276
|
+
│ [10:32] Bob: Fala galera │
|
|
277
|
+
│ │
|
|
278
|
+
├─────────────────────────────────────────┤
|
|
279
|
+
│ > Digite sua mensagem... │ <- Input box
|
|
280
|
+
└─────────────────────────────────────────┘
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
- The status bar shows: chat name, online users, E2E indicator
|
|
284
|
+
- Chat area with automatic and manual scroll
|
|
285
|
+
- Input with history (up/down arrows)
|
|
286
|
+
- Distinct colors per user (chalk)
|
|
287
|
+
- Special commands: `/quit`, `/users`, `/fingerprint`, `/clear`, `/file`, `/sound`, `/help`
|
|
288
|
+
- Animated "typing..." indicator with support for multiple peers
|
|
289
|
+
- Sound notifications (toggle via `/sound on|off`)
|
|
290
|
+
- Progress bar for file transfers
|
|
291
|
+
- Notifications for users joining/leaving
|
|
292
|
+
|
|
293
|
+
#### `src/client/Connection.js` — WebSocket Client
|
|
294
|
+
- Connects to the server via `ws`
|
|
295
|
+
- Automatic reconnect with exponential backoff (1s, 2s, 4s, 8s, max 30s)
|
|
296
|
+
- Ping/pong to detect disconnection
|
|
297
|
+
- Serializes/deserializes protocol messages (JSON for control, Buffer for encrypted data)
|
|
298
|
+
- Emits events to the ChatController
|
|
299
|
+
|
|
300
|
+
#### `src/client/ChatController.js` — Orchestrator
|
|
301
|
+
- Connects: UI <-> Connection <-> Crypto
|
|
302
|
+
- Send flow: UI input -> encrypt -> Connection send
|
|
303
|
+
- Receive flow: Connection receive -> decrypt -> UI display
|
|
304
|
+
- Manages state: peer list, received public keys, derived shared keys
|
|
305
|
+
- Performs the handshake with each new peer
|
|
306
|
+
- Validates fingerprints
|
|
307
|
+
- Manages file transfers via FileTransfer
|
|
308
|
+
- Sound notification when text messages are received
|
|
309
|
+
|
|
310
|
+
#### `src/client/FileTransfer.js` — File Transfer
|
|
311
|
+
- Sending: reads the file, splits it into 48KB chunks, encrypts each chunk E2E via broadcast
|
|
312
|
+
- Receiving: reassembles chunks, verifies SHA-256, saves to `./downloads/`
|
|
313
|
+
- Throttling: max 25 chunks/sec (below the rate limit of 30/sec)
|
|
314
|
+
- Timeout: 30s for incomplete transfers
|
|
315
|
+
- Support for files up to 50MB
|
|
316
|
+
|
|
317
|
+
### 4.3 Crypto
|
|
318
|
+
|
|
319
|
+
#### `src/crypto/KeyManager.js` — Keys
|
|
320
|
+
- Generates a Curve25519 pair with `sodium.crypto_box_keypair()`
|
|
321
|
+
- Stores it in `sodium.sodium_malloc()` (secure memory, does not go to swap)
|
|
322
|
+
- Exports the public key as a `Buffer` (to send to the server)
|
|
323
|
+
- Generates a fingerprint: `SHA-256(publicKey)` formatted as `XXXX:XXXX:XXXX:XXXX`
|
|
324
|
+
- A `destroy()` method that calls `sodium.sodium_memzero()` on the keys
|
|
325
|
+
- **Never** serializes the private key to disk, log, or network
|
|
326
|
+
|
|
327
|
+
#### `src/crypto/MessageCrypto.js` — Encrypt/Decrypt
|
|
328
|
+
- **Encrypt**: `sodium.crypto_box_easy(message, nonce, theirPublicKey, mySecretKey)`
|
|
329
|
+
- **Decrypt**: `sodium.crypto_box_open_easy(ciphertext, nonce, theirPublicKey, mySecretKey)`
|
|
330
|
+
- The `crypto_box` function internally uses:
|
|
331
|
+
- **X25519**: Diffie-Hellman to derive the shared key
|
|
332
|
+
- **XSalsa20**: Stream cipher for confidentiality
|
|
333
|
+
- **Poly1305**: MAC for authenticity and integrity
|
|
334
|
+
- Returns `{ ciphertext: Buffer, nonce: Buffer }`
|
|
335
|
+
- Validates the minimum ciphertext size (16-byte MAC)
|
|
336
|
+
|
|
337
|
+
#### `src/crypto/Handshake.js` — Peer Management
|
|
338
|
+
- Registers peers' public keys received via the server
|
|
339
|
+
- Validates the public key size (32 bytes, Curve25519)
|
|
340
|
+
- Stores public keys in `Map<peerId, publicKey>`
|
|
341
|
+
- Exposes access to the local secret key for use in `crypto_box_easy`
|
|
342
|
+
- A `removePeer()` method for cleanup on disconnect
|
|
343
|
+
- A `destroy()` method to clear all state
|
|
344
|
+
|
|
345
|
+
#### `src/crypto/DoubleRatchet.js` — PFS (Perfect Forward Secrecy)
|
|
346
|
+
- A simplified implementation of the Double Ratchet (Signal-style)
|
|
347
|
+
- Each message uses a unique derived key, destroyed after use
|
|
348
|
+
- DH ratchet: `crypto_scalarmult` (X25519) to generate a new DH output each turn
|
|
349
|
+
- KDF_RK: `BLAKE2b-512(rootKey, dhOutput)` → new rootKey + chainKey
|
|
350
|
+
- KDF_CK: `BLAKE2b-256(chainKey, 0x01)` → messageKey; `BLAKE2b-256(chainKey, 0x02)` → nextChainKey
|
|
351
|
+
- Encryption: `crypto_secretbox_easy` (symmetric XSalsa20-Poly1305) with the derived messageKey
|
|
352
|
+
- Management of skipped keys (out-of-order messages) with a 60s TTL
|
|
353
|
+
- Immediate destruction of keys after use (`sodium_memzero`)
|
|
354
|
+
- Automatic fallback to static keys when the ratchet is unavailable
|
|
355
|
+
|
|
356
|
+
#### `src/crypto/TrustStore.js` — TOFU + SAS
|
|
357
|
+
- **TOFU (Trust On First Use)**: Persists peers' fingerprints in `.ciphermesh/trusted-peers.json`
|
|
358
|
+
- Detects a public key change (possible MITM) — similar to SSH's `known_hosts`
|
|
359
|
+
- **SAS (Short Authentication String)**: A 6-digit code for out-of-band verification
|
|
360
|
+
- `BLAKE2b-256(sortedPubKeys || "CipherMesh-SAS-v1")` → first 3 bytes → 6 decimal digits
|
|
361
|
+
- Both sides compute the same value independently
|
|
362
|
+
- TrustResult: `NEW_PEER` | `TRUSTED` | `MISMATCH` | `VERIFIED_MISMATCH`
|
|
363
|
+
- Authenticated E2E rotation (`autoUpdatePeer`) preserves the verification status
|
|
364
|
+
- Rotation via the server (unauthenticated) does NOT update the trust store
|
|
365
|
+
|
|
366
|
+
#### `src/crypto/StateManager.js` — Encrypted Persistence
|
|
367
|
+
- Persists session state in `.ciphermesh/state/session-state.enc.json`
|
|
368
|
+
- `deriveKEK(passphrase, salt?)` — Derives a Key Encryption Key with `crypto_pwhash` (Argon2id)
|
|
369
|
+
- `saveState(data, kek, salt)` — Encrypts state with `crypto_secretbox_easy`, saves the envelope `{salt, nonce, ciphertext}`
|
|
370
|
+
- `loadState(passphrase)` — Re-derives the KEK from the saved salt, decrypts, returns the object or `null`
|
|
371
|
+
- `hasState()` / `clearState()` — Checks existence / removes saved state
|
|
372
|
+
- Used to preserve ratchets, keys, and peers across reconnections
|
|
373
|
+
|
|
374
|
+
#### `src/crypto/NonceManager.js` — Nonces
|
|
375
|
+
- Generates 24-byte nonces with `sodium.randombytes_buf()`
|
|
376
|
+
- Maintains a **monotonically increasing counter** per peer
|
|
377
|
+
- Rejects repeated nonces or ones smaller than the last received (anti-replay)
|
|
378
|
+
- Nonce structure:
|
|
379
|
+
|
|
380
|
+
```
|
|
381
|
+
[ 8 bytes: timestamp ms | 4 bytes: counter | 12 bytes: random ]
|
|
382
|
+
anti-replay sequencia unicidade
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
### 4.4 P2P (Alternative Mode)
|
|
386
|
+
|
|
387
|
+
#### `src/p2p/Discovery.js` — mDNS Discovery
|
|
388
|
+
- Publishes the `_ciphermesh._tcp` service via mDNS using `bonjour-service`
|
|
389
|
+
- TXT records: `{ nickname, publicKey, version }`
|
|
390
|
+
- Automatically searches for peers of the same type on the LAN
|
|
391
|
+
- Emits events: `peer-discovered`, `peer-lost`
|
|
392
|
+
- Ignores self (same nickname)
|
|
393
|
+
|
|
394
|
+
#### `src/p2p/PeerServer.js` — Local Server
|
|
395
|
+
- `WebSocketServer` listening on a random port (`port: 0`)
|
|
396
|
+
- Accepts inbound connections from peers on the LAN
|
|
397
|
+
- Emits `connection(ws)` for the controller to process
|
|
398
|
+
|
|
399
|
+
#### `src/p2p/PeerConnectionManager.js` — Connection Manager
|
|
400
|
+
- Manages outbound and inbound WebSocket connections
|
|
401
|
+
- **Deduplication**: The lexicographically smaller nickname initiates the connection
|
|
402
|
+
- If `alice < bob`: Alice connects, Bob waits
|
|
403
|
+
- Result: exactly 1 WebSocket between each pair
|
|
404
|
+
- P2P handshake: `{ type: "p2p_handshake", nickname, publicKey, version, timestamp }`
|
|
405
|
+
- Reconnect with exponential backoff (2s→30s) for outbound connections
|
|
406
|
+
- `send(nickname, data)` / `broadcast(data)` — sends to peer(s)
|
|
407
|
+
|
|
408
|
+
#### `src/p2p/P2PChatController.js` — P2P Orchestrator
|
|
409
|
+
- An adaptation of `ChatController` for peer-to-peer mode
|
|
410
|
+
- Uses the **nickname as the peer ID** (stable, vs. the ephemeral sessionId in server mode)
|
|
411
|
+
- Same crypto: DoubleRatchet, TOFU, SAS, key rotation, secure wipe
|
|
412
|
+
- Same commands: `/verify`, `/trust`, `/trustlist`, `/file`, etc.
|
|
413
|
+
- Main difference: messages go directly peer-to-peer, without a relay
|
|
414
|
+
|
|
415
|
+
| Aspect | Server Mode | P2P Mode |
|
|
416
|
+
|---------|---------------|----------|
|
|
417
|
+
| Connection | Single WS to the server | Direct WS between each pair of peers |
|
|
418
|
+
| Discovery | `JOIN_ACK` from the server | mDNS on the LAN |
|
|
419
|
+
| Peer ID | sessionId (ephemeral UUID) | nickname (stable) |
|
|
420
|
+
| Routing | Via the server (blind relay) | Direct peer-to-peer |
|
|
421
|
+
| Offline queue | Yes (the server stores it) | No (peers must be online) |
|
|
422
|
+
|
|
423
|
+
#### `src/p2p/index.js` — P2P Entry Point
|
|
424
|
+
- Prompt: nickname, passphrase (optional, to restore state)
|
|
425
|
+
- Initializes `PeerServer` (random port) + `Discovery` (mDNS)
|
|
426
|
+
- `PeerConnectionManager` + `P2PChatController` + `UI`
|
|
427
|
+
- Shutdown: saves encrypted state if a passphrase is set
|
|
428
|
+
|
|
429
|
+
### 4.5 Protocol (Server Mode)
|
|
430
|
+
|
|
431
|
+
#### `src/protocol/messages.js` — Message Types
|
|
432
|
+
|
|
433
|
+
Defines the protocol's message types. All messages have:
|
|
434
|
+
```js
|
|
435
|
+
{
|
|
436
|
+
type: string, // tipo da mensagem
|
|
437
|
+
version: 1, // versao do protocolo
|
|
438
|
+
timestamp: number // Date.now() do remetente
|
|
439
|
+
}
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
Types:
|
|
443
|
+
| Type | Direction | Description |
|
|
444
|
+
|------|---------|-----------|
|
|
445
|
+
| `join` | Client -> Server | The client wants to join (nickname + publicKey) |
|
|
446
|
+
| `join_ack` | Server -> Client | The server confirms the join (sessionId + peer list) |
|
|
447
|
+
| `peer_joined` | Server -> Clients | A new peer joined (nickname + publicKey) |
|
|
448
|
+
| `peer_left` | Server -> Clients | A peer left |
|
|
449
|
+
| `key_exchange` | Client -> Server -> Client | Public key exchange between peers |
|
|
450
|
+
| `encrypted_message` | Client -> Server -> Client | Encrypted message |
|
|
451
|
+
| `error` | Server -> Client | Error (duplicate nickname, etc.) |
|
|
452
|
+
| `ping` / `pong` | Bidirectional | Heartbeat |
|
|
453
|
+
|
|
454
|
+
#### `src/protocol/validators.js` — Validation
|
|
455
|
+
- Validates the JSON structure of each message type
|
|
456
|
+
- Checks required fields and types
|
|
457
|
+
- Checks maximum sizes (nickname: 20 chars, payload: 64KB)
|
|
458
|
+
- Sanitizes inputs (trim, removal of control characters)
|
|
459
|
+
- Rejects messages with an incompatible `version`
|
|
460
|
+
|
|
461
|
+
### 4.6 Shared
|
|
462
|
+
|
|
463
|
+
#### `src/shared/constants.js` — Constants
|
|
464
|
+
|
|
465
|
+
```js
|
|
466
|
+
export const SERVER_PORT = 3600;
|
|
467
|
+
export const MAX_NICKNAME_LENGTH = 20;
|
|
468
|
+
export const MAX_PAYLOAD_SIZE = 65536; // 64KB
|
|
469
|
+
export const HEARTBEAT_INTERVAL_MS = 30000; // 30s
|
|
470
|
+
export const RECONNECT_BASE_MS = 1000; // 1s
|
|
471
|
+
export const RECONNECT_MAX_MS = 30000; // 30s
|
|
472
|
+
export const RATE_LIMIT_PER_SECOND = 30;
|
|
473
|
+
export const SESSION_TIMEOUT_MS = 300000; // 5min inativo
|
|
474
|
+
export const PROTOCOL_VERSION = 1;
|
|
475
|
+
export const NONCE_SIZE = 24; // libsodium nonce
|
|
476
|
+
export const PUBLIC_KEY_SIZE = 32; // Curve25519
|
|
477
|
+
export const SECRET_KEY_SIZE = 32;
|
|
478
|
+
export const MAC_SIZE = 16; // Poly1305
|
|
479
|
+
|
|
480
|
+
// Offline queue
|
|
481
|
+
export const OFFLINE_QUEUE_MAX_PER_PEER = 100;
|
|
482
|
+
export const OFFLINE_QUEUE_MAX_AGE_MS = 3600000; // 1h
|
|
483
|
+
export const OFFLINE_QUEUE_MAX_TOTAL = 1000;
|
|
484
|
+
|
|
485
|
+
// File transfer
|
|
486
|
+
export const MAX_FILE_SIZE = 50 * 1024 * 1024; // 50MB
|
|
487
|
+
export const FILE_CHUNK_SIZE = 49152; // 48KB
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
#### `src/shared/logger.js` — Logger
|
|
491
|
+
- Levels: `debug`, `info`, `warn`, `error`
|
|
492
|
+
- Format: `[HH:MM:SS] [LEVEL] [module] message`
|
|
493
|
+
- Level configurable via the `LOG_LEVEL` environment variable
|
|
494
|
+
- Never logs message content or private keys
|
|
495
|
+
- Logs only metadata: connections, disconnections, errors
|
|
496
|
+
|
|
497
|
+
---
|
|
498
|
+
|
|
499
|
+
## 5. Data Model and Payloads
|
|
500
|
+
|
|
501
|
+
### 5.1 Join Message (client -> server)
|
|
502
|
+
|
|
503
|
+
```json
|
|
504
|
+
{
|
|
505
|
+
"type": "join",
|
|
506
|
+
"version": 1,
|
|
507
|
+
"timestamp": 1739800000000,
|
|
508
|
+
"nickname": "Alice",
|
|
509
|
+
"publicKey": "base64(32 bytes da chave publica Curve25519)"
|
|
510
|
+
}
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
### 5.2 Join ACK (server -> client)
|
|
514
|
+
|
|
515
|
+
```json
|
|
516
|
+
{
|
|
517
|
+
"type": "join_ack",
|
|
518
|
+
"version": 1,
|
|
519
|
+
"timestamp": 1739800000050,
|
|
520
|
+
"sessionId": "550e8400-e29b-41d4-a716-446655440000",
|
|
521
|
+
"peers": [
|
|
522
|
+
{
|
|
523
|
+
"sessionId": "660e8400-e29b-41d4-a716-446655440001",
|
|
524
|
+
"nickname": "Bob",
|
|
525
|
+
"publicKey": "base64(chave publica do Bob)"
|
|
526
|
+
}
|
|
527
|
+
]
|
|
528
|
+
}
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
### 5.3 Encrypted Message (client -> server -> client)
|
|
532
|
+
|
|
533
|
+
```json
|
|
534
|
+
{
|
|
535
|
+
"type": "encrypted_message",
|
|
536
|
+
"version": 1,
|
|
537
|
+
"timestamp": 1739800001000,
|
|
538
|
+
"from": "550e8400-e29b-41d4-a716-446655440000",
|
|
539
|
+
"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
|
+
}
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
**Note**: The `payload` field is completely opaque to the server. It only reads `from` and `to` for routing.
|
|
548
|
+
|
|
549
|
+
### 5.4 Decrypted content (never travels in cleartext)
|
|
550
|
+
|
|
551
|
+
After decrypting `payload.ciphertext`, the result is:
|
|
552
|
+
|
|
553
|
+
```json
|
|
554
|
+
{
|
|
555
|
+
"text": "Ola Bob, tudo bem?",
|
|
556
|
+
"sentAt": 1739800001000,
|
|
557
|
+
"messageId": "a1b2c3d4"
|
|
558
|
+
}
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
- `sentAt` inside the encrypted payload lets the receiver validate against the external `timestamp`
|
|
562
|
+
- `messageId` is a short random ID for reference (not a UUID, just 4 bytes of hex)
|
|
563
|
+
|
|
564
|
+
#### Encrypted commands (actions)
|
|
565
|
+
|
|
566
|
+
Besides text messages, the encrypted payload may contain commands (the `action` field):
|
|
567
|
+
|
|
568
|
+
```json
|
|
569
|
+
{ "action": "clear", "sentAt": 1739800001000 }
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
```json
|
|
573
|
+
{ "action": "typing", "sentAt": 1739800001000 }
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
- `clear` — clears the chat for all peers
|
|
577
|
+
- `typing` — indicates that the sender is typing (2s debounce, expires after 3s at the receiver)
|
|
578
|
+
- `file_offer` — offers to send a file (transferId, fileName, fileSize, totalChunks, sha256)
|
|
579
|
+
- `file_chunk` — sends a file chunk (transferId, chunkIndex, base64 data)
|
|
580
|
+
- `file_complete` — signals the end of the transfer (transferId)
|
|
581
|
+
|
|
582
|
+
### 5.5 Peer Notification (server -> clients)
|
|
583
|
+
|
|
584
|
+
```json
|
|
585
|
+
{
|
|
586
|
+
"type": "peer_joined",
|
|
587
|
+
"version": 1,
|
|
588
|
+
"timestamp": 1739800002000,
|
|
589
|
+
"peer": {
|
|
590
|
+
"sessionId": "770e8400-e29b-41d4-a716-446655440002",
|
|
591
|
+
"nickname": "Charlie",
|
|
592
|
+
"publicKey": "base64(chave publica do Charlie)"
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
### 5.6 Error (server -> client)
|
|
598
|
+
|
|
599
|
+
```json
|
|
600
|
+
{
|
|
601
|
+
"type": "error",
|
|
602
|
+
"version": 1,
|
|
603
|
+
"timestamp": 1739800003000,
|
|
604
|
+
"code": "NICKNAME_TAKEN",
|
|
605
|
+
"message": "O nickname 'Alice' ja esta em uso"
|
|
606
|
+
}
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
Error codes:
|
|
610
|
+
| Code | Description |
|
|
611
|
+
|--------|-----------|
|
|
612
|
+
| `NICKNAME_TAKEN` | Nickname already in use |
|
|
613
|
+
| `INVALID_MESSAGE` | Invalid message structure |
|
|
614
|
+
| `PEER_NOT_FOUND` | The recipient is not online |
|
|
615
|
+
| `RATE_LIMITED` | Too many messages per second |
|
|
616
|
+
| `PAYLOAD_TOO_LARGE` | Payload exceeds 64KB |
|
|
617
|
+
|
|
618
|
+
---
|
|
619
|
+
|
|
620
|
+
## 6. Detailed Cryptographic Flow
|
|
621
|
+
|
|
622
|
+
### 6.1 Algorithms Used
|
|
623
|
+
|
|
624
|
+
| Operation | Algorithm | Size | Description |
|
|
625
|
+
|----------|-----------|---------|-----------|
|
|
626
|
+
| Key pair | **Curve25519** | 32 bytes each | Elliptic curve for key exchange |
|
|
627
|
+
| Key exchange | **X25519** (ECDH) | 32-byte result | Diffie-Hellman over Curve25519 |
|
|
628
|
+
| Cipher | **XSalsa20** | stream cipher | A variant of Salsa20 with a 24-byte nonce |
|
|
629
|
+
| MAC | **Poly1305** | 16-byte tag | Message Authentication Code |
|
|
630
|
+
| Nonce | random + counter | 24 bytes | Number used only once |
|
|
631
|
+
|
|
632
|
+
### 6.2 Why Curve25519 + XSalsa20-Poly1305?
|
|
633
|
+
|
|
634
|
+
This combination (known as **NaCl crypto_box**) was chosen because:
|
|
635
|
+
|
|
636
|
+
1. **Modern curve**: Curve25519 was designed by Daniel J. Bernstein to be resistant to timing attacks and to have secure implementations
|
|
637
|
+
2. **Authenticated encryption**: XSalsa20-Poly1305 combines confidentiality + integrity in a single atomic operation (AEAD)
|
|
638
|
+
3. **24-byte nonce**: Large enough to be generated randomly with no practical risk of collision (2^192 combinations)
|
|
639
|
+
4. **No padding oracle**: A stream cipher needs no padding, eliminating an entire class of attacks
|
|
640
|
+
5. **Misuse-resistant**: Hard to use incorrectly (compared to AES-CBC, manual AES-CTR, etc.)
|
|
641
|
+
|
|
642
|
+
### 6.3 Key Generation
|
|
643
|
+
|
|
644
|
+
```
|
|
645
|
+
1. Cliente inicia
|
|
646
|
+
2. sodium.crypto_box_keypair() gera:
|
|
647
|
+
- publicKey: 32 bytes (pode ser compartilhada)
|
|
648
|
+
- secretKey: 32 bytes (NUNCA sai da memoria do processo)
|
|
649
|
+
3. Ambas armazenadas em sodium.sodium_malloc() (secure memory)
|
|
650
|
+
4. Fingerprint = SHA256(publicKey) formatada como XXXX:XXXX:XXXX:XXXX
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
### 6.4 Authenticated Encryption with crypto_box_easy
|
|
654
|
+
|
|
655
|
+
`crypto_box_easy` does everything internally in a single atomic operation:
|
|
656
|
+
|
|
657
|
+
```
|
|
658
|
+
crypto_box_easy(ciphertext, plaintext, nonce, recipientPublicKey, senderSecretKey)
|
|
659
|
+
|
|
660
|
+
Internamente:
|
|
661
|
+
1. X25519 DH: sharedSecret = ECDH(recipientPub, senderSec)
|
|
662
|
+
2. Key derivation: encKey = HSalsa20(sharedSecret, zeros)
|
|
663
|
+
3. Cifra: XSalsa20(plaintext, nonce, encKey) -> ciphertext
|
|
664
|
+
4. MAC: Poly1305(ciphertext) -> tag de 16 bytes
|
|
665
|
+
5. Output: tag || ciphertext (autenticado)
|
|
666
|
+
```
|
|
667
|
+
|
|
668
|
+
The shared key is derived implicitly on each call. The DH guarantees
|
|
669
|
+
that both sides (Alice and Bob) arrive at the same secret without exchanging it over the network.
|
|
670
|
+
|
|
671
|
+
### 6.5 Message Encryption
|
|
672
|
+
|
|
673
|
+
```
|
|
674
|
+
Input:
|
|
675
|
+
- plaintext: Buffer (mensagem em UTF-8)
|
|
676
|
+
- nonce: 24 bytes (gerado pelo NonceManager)
|
|
677
|
+
- recipientPublicKey: 32 bytes (chave publica do destinatario)
|
|
678
|
+
- senderSecretKey: 32 bytes (chave secreta do remetente)
|
|
679
|
+
|
|
680
|
+
Processo:
|
|
681
|
+
ciphertext = crypto_box_easy(plaintext, nonce, recipientPublicKey, senderSecretKey)
|
|
682
|
+
|
|
683
|
+
Output:
|
|
684
|
+
- ciphertext: Buffer (plaintext.length + 16 bytes de MAC)
|
|
685
|
+
- nonce: 24 bytes (enviado junto, nao e segredo)
|
|
686
|
+
|
|
687
|
+
Total enviado: ciphertext (N+16 bytes) + nonce (24 bytes)
|
|
688
|
+
```
|
|
689
|
+
|
|
690
|
+
### 6.6 Message Decryption
|
|
691
|
+
|
|
692
|
+
```
|
|
693
|
+
Input:
|
|
694
|
+
- ciphertext: Buffer (recebido da rede)
|
|
695
|
+
- nonce: 24 bytes (recebido da rede)
|
|
696
|
+
- senderPublicKey: 32 bytes (chave publica do remetente)
|
|
697
|
+
- recipientSecretKey: 32 bytes (chave secreta do destinatario)
|
|
698
|
+
|
|
699
|
+
Processo:
|
|
700
|
+
1. NonceManager valida que nonce nao foi usado antes (anti-replay)
|
|
701
|
+
2. plaintext = crypto_box_open_easy(ciphertext, nonce, senderPublicKey, recipientSecretKey)
|
|
702
|
+
3. Se MAC invalido -> rejeita (mensagem foi adulterada)
|
|
703
|
+
4. Se MAC valido -> parse do JSON interno
|
|
704
|
+
|
|
705
|
+
Output:
|
|
706
|
+
- plaintext: Buffer (mensagem original)
|
|
707
|
+
```
|
|
708
|
+
|
|
709
|
+
### 6.7 Nonce Structure (24 bytes)
|
|
710
|
+
|
|
711
|
+
```
|
|
712
|
+
┌──────────────────┬──────────────┬──────────────────────┐
|
|
713
|
+
│ Timestamp (8B) │ Counter (4B) │ Random (12B) │
|
|
714
|
+
│ ms desde epoch │ sequencial │ sodium.randombytes │
|
|
715
|
+
└──────────────────┴──────────────┴──────────────────────┘
|
|
716
|
+
|
|
717
|
+
- Timestamp: impede replay entre sessoes diferentes
|
|
718
|
+
- Counter: garante ordenacao e unicidade dentro da sessao
|
|
719
|
+
- Random: garante unicidade mesmo com clocks sincronizados
|
|
720
|
+
```
|
|
721
|
+
|
|
722
|
+
### 6.8 Fingerprint Verification
|
|
723
|
+
|
|
724
|
+
The fingerprint lets users verify each other's identity **out of band** (for example, in person or by phone):
|
|
725
|
+
|
|
726
|
+
```
|
|
727
|
+
1. Alice ve seu fingerprint: A1B2:C3D4:E5F6:7890
|
|
728
|
+
2. Bob ve o fingerprint de Alice: A1B2:C3D4:E5F6:7890
|
|
729
|
+
3. Bob confirma pessoalmente com Alice que os valores batem
|
|
730
|
+
4. Se nao baterem -> MITM detectado
|
|
731
|
+
```
|
|
732
|
+
|
|
733
|
+
The fingerprint is computed like this:
|
|
734
|
+
```
|
|
735
|
+
fingerprint = SHA-256(publicKey)
|
|
736
|
+
= primeiros 8 bytes, formatados em hex com separador ':'
|
|
737
|
+
= "A1B2:C3D4:E5F6:7890"
|
|
738
|
+
```
|
|
739
|
+
|
|
740
|
+
---
|
|
741
|
+
|
|
742
|
+
## 7. Handshake Protocol
|
|
743
|
+
|
|
744
|
+
### 7.1 Full Diagram
|
|
745
|
+
|
|
746
|
+
```
|
|
747
|
+
Cliente A Servidor Cliente B
|
|
748
|
+
│ │ │
|
|
749
|
+
│ 1. JOIN(nick, pubKeyA) │ │
|
|
750
|
+
│ ──────────────────────────>│ │
|
|
751
|
+
│ │ │
|
|
752
|
+
│ 2. JOIN_ACK(sessionId, │ │
|
|
753
|
+
│ peers=[B: pubKeyB]) │ │
|
|
754
|
+
│ <──────────────────────────│ │
|
|
755
|
+
│ │ │
|
|
756
|
+
│ │ 3. PEER_JOINED(A, pubKeyA)│
|
|
757
|
+
│ │───────────────────────────>│
|
|
758
|
+
│ │ │
|
|
759
|
+
│ 4. Deriva sharedKey(A,B) │ 5. Deriva sharedKey(B,A)
|
|
760
|
+
│ usando pubKeyB + secKeyA │ usando pubKeyA + secKeyB
|
|
761
|
+
│ │ │
|
|
762
|
+
│ 6. ENCRYPTED_MSG ─────────│────────────────────────> │
|
|
763
|
+
│ │ 7. Decifra com sharedKey│
|
|
764
|
+
│ │ │
|
|
765
|
+
```
|
|
766
|
+
|
|
767
|
+
### 7.2 Detailed Steps
|
|
768
|
+
|
|
769
|
+
**Step 1 — JOIN**: Client A generates a key pair and sends `{ type: "join", nickname: "Alice", publicKey: base64(pubKeyA) }` to the server.
|
|
770
|
+
|
|
771
|
+
**Step 2 — JOIN_ACK**: The server validates the nickname (unique?), registers the session, and returns the list of already-connected peers with their public keys.
|
|
772
|
+
|
|
773
|
+
**Step 3 — PEER_JOINED**: The server notifies all existing clients that Alice joined, including her public key.
|
|
774
|
+
|
|
775
|
+
**Steps 4 and 5 — Derivation**: Each side computes `crypto_box_beforenm()` with the other's public key and its own private key. Result: the same shared key.
|
|
776
|
+
|
|
777
|
+
**Step 6 — Encrypted message**: Alice encrypts and sends. The server relays without reading.
|
|
778
|
+
|
|
779
|
+
**Step 7 — Decryption**: Bob decrypts with the derived shared key.
|
|
780
|
+
|
|
781
|
+
### 7.3 Handshake Security
|
|
782
|
+
|
|
783
|
+
- The public key travels in cleartext (this is safe — it is public by definition)
|
|
784
|
+
- The private key **never** leaves the client's process
|
|
785
|
+
- The server sees public keys but cannot derive the shared key (it would need a private key)
|
|
786
|
+
- An attacker who captures all traffic sees only public keys + encrypted payloads = useless without a private key
|
|
787
|
+
|
|
788
|
+
**PFS implemented**: The system uses a simplified Double Ratchet for live conversations. Each message uses a unique derived key, destroyed after use. Compromising one key reveals at most ONE message. Offline messages use static keys as a fallback.
|
|
789
|
+
|
|
790
|
+
---
|
|
791
|
+
|
|
792
|
+
## 8. Step-by-Step Communication Flow
|
|
793
|
+
|
|
794
|
+
### 8.1 Full Scenario: Alice sends "Ola" to Bob
|
|
795
|
+
|
|
796
|
+
```
|
|
797
|
+
TEMPO ACAO
|
|
798
|
+
───── ──────────────────────────────────────────────────────
|
|
799
|
+
t0 Alice digita "Ola" no input e pressiona Enter
|
|
800
|
+
|
|
801
|
+
t1 ChatController recebe o texto da UI
|
|
802
|
+
ChatController verifica se tem sharedKey com Bob
|
|
803
|
+
Se nao tem -> erro "Handshake nao completado com Bob"
|
|
804
|
+
|
|
805
|
+
t2 MessageCrypto.encrypt():
|
|
806
|
+
- NonceManager gera nonce de 24 bytes
|
|
807
|
+
- Serializa payload interno: { text: "Ola", sentAt: t2, messageId: "a1b2" }
|
|
808
|
+
- crypto_box_easy_afternm(payload, nonce, sharedKeyAB)
|
|
809
|
+
- Retorna { ciphertext: Buffer, nonce: Buffer }
|
|
810
|
+
|
|
811
|
+
t3 Connection envia ao servidor:
|
|
812
|
+
{
|
|
813
|
+
type: "encrypted_message",
|
|
814
|
+
from: "alice-session-id",
|
|
815
|
+
to: "bob-session-id",
|
|
816
|
+
payload: { ciphertext: "base64(...)", nonce: "base64(...)" }
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
t4 Servidor (MessageRouter):
|
|
820
|
+
- Valida estrutura (tem type, from, to, payload)
|
|
821
|
+
- NAO abre payload
|
|
822
|
+
- Encontra WebSocket do Bob pelo sessionId
|
|
823
|
+
- Encaminha o JSON inteiro para Bob
|
|
824
|
+
|
|
825
|
+
t5 Bob (Connection) recebe o JSON
|
|
826
|
+
ChatController identifica: encrypted_message de Alice
|
|
827
|
+
|
|
828
|
+
t6 MessageCrypto.decrypt():
|
|
829
|
+
- Extrai ciphertext e nonce do payload
|
|
830
|
+
- NonceManager valida nonce (nao repetido, counter valido)
|
|
831
|
+
- crypto_box_open_easy_afternm(ciphertext, nonce, sharedKeyAB)
|
|
832
|
+
- Se MAC falhar -> rejeita (mensagem corrompida/adulterada)
|
|
833
|
+
- Se MAC ok -> parse do JSON interno
|
|
834
|
+
|
|
835
|
+
t7 ChatController recebe { text: "Ola", sentAt: t2, messageId: "a1b2" }
|
|
836
|
+
Valida que sentAt e razoavel (nao muito no passado/futuro)
|
|
837
|
+
|
|
838
|
+
t8 UI.displayMessage("Alice", "Ola", timestamp)
|
|
839
|
+
Bob ve: [10:30] Alice: Ola
|
|
840
|
+
```
|
|
841
|
+
|
|
842
|
+
### 8.2 Scenario: Group Chat (broadcast)
|
|
843
|
+
|
|
844
|
+
To send to everyone, Alice encrypts **individually** for each peer:
|
|
845
|
+
|
|
846
|
+
```
|
|
847
|
+
Alice -> Bob: encrypt(msg, nonceAB, sharedKeyAB)
|
|
848
|
+
Alice -> Charlie: encrypt(msg, nonceAC, sharedKeyAC)
|
|
849
|
+
```
|
|
850
|
+
|
|
851
|
+
Each message has a different nonce and ciphertext because each shared key is different. The server receives N messages and routes each one to the correct recipient.
|
|
852
|
+
|
|
853
|
+
**Impact**: O(N) encryptions per group message. Acceptable for a LAN with few users.
|
|
854
|
+
|
|
855
|
+
**Sender keys (real groups) — `src/crypto/SenderKey.js`**: an O(1) alternative. Each
|
|
856
|
+
sender has a *sender key chain* (symmetric BLAKE2b ratchet) per room; the
|
|
857
|
+
message is encrypted **once** (`crypto_secretbox` + length padding) and the
|
|
858
|
+
**same** ciphertext is valid for all members. The chain is distributed once to
|
|
859
|
+
each member over the pairwise channel (never in cleartext). Forward secrecy: the chain
|
|
860
|
+
advances with each message and is rotated (`rotate()`) on member changes (with
|
|
861
|
+
redistribution). It supports out-of-order delivery (skipped keys, limited to
|
|
862
|
+
`maxSkip`) and rejects replay (an already-consumed counter) and tampering (Poly1305 MAC).
|
|
863
|
+
|
|
864
|
+
**Live integration (P2P)**: normal room messages use sender keys — encrypted
|
|
865
|
+
once (`p2p_group`) and sent identically to all peers in the room. The sender key
|
|
866
|
+
is distributed (`sk_dist`) over the pairwise channel when joining the room / when a peer
|
|
867
|
+
joins my room; since it goes over the same TCP connection, it arrives ordered before
|
|
868
|
+
any group message. When a peer leaves the room, my sender key is
|
|
869
|
+
rotated and redistributed (forward secrecy). Deniable, ephemeral, DMs,
|
|
870
|
+
store-and-forward, and cover-constant remain on the pairwise path because they have their own
|
|
871
|
+
semantics. (On the relay, the same model would give O(1) multicast; it requires
|
|
872
|
+
a server change and is not wired up there yet.)
|
|
873
|
+
|
|
874
|
+
---
|
|
875
|
+
|
|
876
|
+
## 9. Initialization Strategy
|
|
877
|
+
|
|
878
|
+
### 9.1 Server
|
|
879
|
+
|
|
880
|
+
```
|
|
881
|
+
1. Carregar constantes (constants.js)
|
|
882
|
+
2. Criar instancia WebSocketServer na porta configurada
|
|
883
|
+
3. Criar SessionManager (mapa vazio de sessoes)
|
|
884
|
+
4. Criar MessageRouter (referencia ao SessionManager)
|
|
885
|
+
5. Registrar handlers:
|
|
886
|
+
- on('connection') -> SessionManager.handleConnection()
|
|
887
|
+
- on('close') -> SessionManager.handleDisconnection()
|
|
888
|
+
- on('message') -> MessageRouter.route()
|
|
889
|
+
6. Iniciar heartbeat interval (ping todos os clientes a cada 30s)
|
|
890
|
+
7. Registrar SIGINT/SIGTERM para graceful shutdown:
|
|
891
|
+
- Notificar todos os clientes
|
|
892
|
+
- Fechar conexoes
|
|
893
|
+
- Limpar recursos
|
|
894
|
+
8. Imprimir no console:
|
|
895
|
+
- IP local (todas as interfaces de rede)
|
|
896
|
+
- Porta
|
|
897
|
+
- "Servidor pronto. Clientes podem conectar em ws://<IP>:3600"
|
|
898
|
+
```
|
|
899
|
+
|
|
900
|
+
### 9.2 Client
|
|
901
|
+
|
|
902
|
+
```
|
|
903
|
+
1. Exibir banner "SecureLAN Chat v1.0"
|
|
904
|
+
2. Pedir nickname (validar: 1-20 chars, alfanumerico + underscore)
|
|
905
|
+
3. Pedir endereco do servidor (default: localhost:3600)
|
|
906
|
+
4. Gerar par de chaves (KeyManager)
|
|
907
|
+
5. Exibir fingerprint da chave publica
|
|
908
|
+
6. Conectar ao servidor via WebSocket
|
|
909
|
+
7. Enviar mensagem JOIN (nickname + publicKey)
|
|
910
|
+
8. Aguardar JOIN_ACK
|
|
911
|
+
9. Se erro (nickname duplicado) -> pedir outro nickname
|
|
912
|
+
10. Receber lista de peers e derivar sharedKey com cada um
|
|
913
|
+
11. Inicializar UI blessed
|
|
914
|
+
12. Exibir lista de usuarios online
|
|
915
|
+
13. Entrar no loop de input
|
|
916
|
+
14. Registrar handler de SIGINT para:
|
|
917
|
+
- sodium_memzero() em todas as chaves
|
|
918
|
+
- Fechar conexao WebSocket
|
|
919
|
+
- Destruir UI blessed
|
|
920
|
+
```
|
|
921
|
+
|
|
922
|
+
---
|
|
923
|
+
|
|
924
|
+
## 10. Security — Threat Analysis
|
|
925
|
+
|
|
926
|
+
### 10.1 Threat Model
|
|
927
|
+
|
|
928
|
+
| Threat | Mitigation | Status |
|
|
929
|
+
|--------|-----------|--------|
|
|
930
|
+
| **Malicious server reads messages** | Impossible — the server has no private key | Mitigated |
|
|
931
|
+
| **Server alters messages** | The Poly1305 MAC detects tampering | Mitigated |
|
|
932
|
+
| **Message replay** | Nonce with timestamp + monotonically increasing counter | Mitigated |
|
|
933
|
+
| **Man-in-the-Middle (MITM)** | TOFU + SAS (6-digit code) + fingerprint | Mitigated |
|
|
934
|
+
| **Private key leak** | Secure memory (sodium_malloc + mlock) | Mitigated |
|
|
935
|
+
| **Plaintext in memory** | Secure wipe (sodium_memzero) after use | Mitigated |
|
|
936
|
+
| **Private key in swap** | sodium_malloc() with mlock | Mitigated |
|
|
937
|
+
| **Brute force on the key** | Curve25519 = 128 bits of security (~3x10^38 operations) | Infeasible |
|
|
938
|
+
| **Nonce reuse** | Hybrid nonce (timestamp + counter + random) | Mitigated |
|
|
939
|
+
| **Denial of Service** | Rate limiting + maxPayload | Partial |
|
|
940
|
+
| **Forward secrecy** | Double Ratchet — a unique key per message | Mitigated |
|
|
941
|
+
| **Metadata analysis** | Message padding + fixed sizes | Partial |
|
|
942
|
+
|
|
943
|
+
### 10.2 What the server CAN deduce (metadata)
|
|
944
|
+
|
|
945
|
+
Even without reading content, the server knows:
|
|
946
|
+
- **Who** is online
|
|
947
|
+
- **Who** talks to whom
|
|
948
|
+
- **When** messages are sent — *mitigated by cover traffic (optional)*
|
|
949
|
+
- **Approximate size** of messages — *mitigated: only the padding bucket leaks*
|
|
950
|
+
- **Frequency** of communication — *mitigated by cover traffic (optional)*
|
|
951
|
+
|
|
952
|
+
Implemented mitigations:
|
|
953
|
+
- **Length padding** (`MessageCrypto.padMessage`, buckets
|
|
954
|
+
`[128..32768]`): applied on all three encryption paths (static, ratchet, and
|
|
955
|
+
deniable) before encrypting. The server sees only which bucket, not the real size.
|
|
956
|
+
- **File chunk padding** (`FileTransfer`): the last (partial) chunk is
|
|
957
|
+
padded up to the full size with random bytes, so all chunks have the
|
|
958
|
+
same size on the wire; the receiver truncates it back using `fileSize`. This hides
|
|
959
|
+
the exact file size (only the chunk-rounded size leaks).
|
|
960
|
+
- **Cover traffic** (`/cover`, `src/shared/coverTraffic.js`): encrypted
|
|
961
|
+
decoy messages (`action: 'cover'`) with random filler, indistinguishable from
|
|
962
|
+
real messages; the recipient discards them silently. Two modes:
|
|
963
|
+
- `on` (jitter): decoys at random intervals (20-60s) — a baseline of noise.
|
|
964
|
+
- `constant`: a fixed-rate channel (~3s) — each slot carries a queued real
|
|
965
|
+
message or, if there is none, a decoy. The wire keeps an identical cadence
|
|
966
|
+
whether you are chatting or idle (cost: up to ~3s of latency per message).
|
|
967
|
+
This masks *when/how often* you chat, not *with whom*.
|
|
968
|
+
- **Migrating to P2P** eliminates the central server (but exposes IPs on the LAN).
|
|
969
|
+
|
|
970
|
+
- **Sealed sender** (`src/crypto/SealedSender.js`): removes the `from` from the
|
|
971
|
+
network envelope. The sender's identity goes inside a libsodium *sealed box*
|
|
972
|
+
(`crypto_box_seal` — anonymous encryption with an ephemeral key to the recipient's
|
|
973
|
+
public key), which only the recipient opens. The relay routes only by `to` and does not
|
|
974
|
+
see who sent it. The primitive is implemented and tested; wiring it into the envelope
|
|
975
|
+
(client seals / server routes without `from` / recipient opens and decrypts the
|
|
976
|
+
inner content) is the next step.
|
|
977
|
+
|
|
978
|
+
Still leaking: who is online and who *receives* (the `to`, inherent to star
|
|
979
|
+
routing). With sealed sender, the *sender* side of the social graph stays hidden.
|
|
980
|
+
|
|
981
|
+
### 10.3 Resistance to Known Attacks
|
|
982
|
+
|
|
983
|
+
| Attack | Resistant? | Explanation |
|
|
984
|
+
|--------|-------------|-----------|
|
|
985
|
+
| Padding oracle | Yes | XSalsa20 is a stream cipher, no padding |
|
|
986
|
+
| Timing attack | Yes | libsodium uses constant-time comparisons |
|
|
987
|
+
| Chosen ciphertext | Yes | Poly1305 authenticates before decrypting |
|
|
988
|
+
| Key confusion | Yes | crypto_box uses typed keys (pub/sec) |
|
|
989
|
+
| Nonce misuse | Partial | The hybrid nonce reduces risk, but XSalsa20 is not nonce-misuse-resistant (unlike AES-GCM-SIV) |
|
|
990
|
+
|
|
991
|
+
---
|
|
992
|
+
|
|
993
|
+
## 11. Future Improvements
|
|
994
|
+
|
|
995
|
+
### 11.1 Perfect Forward Secrecy (PFS) — IMPLEMENTED
|
|
996
|
+
|
|
997
|
+
**Implementation**: A simplified Double Ratchet using libsodium primitives.
|
|
998
|
+
|
|
999
|
+
**Hybrid model**:
|
|
1000
|
+
- **Ratchet** for live conversations (both peers online) — each message uses a unique key
|
|
1001
|
+
- **Static keys** (`crypto_box`) as a fallback for the offline queue and initial msgs
|
|
1002
|
+
|
|
1003
|
+
**Primitives used**:
|
|
1004
|
+
- `crypto_scalarmult` — raw X25519 DH for ratchet steps
|
|
1005
|
+
- `crypto_generichash` (BLAKE2b) — KDF to derive root keys and chain keys
|
|
1006
|
+
- `crypto_secretbox_easy` — Symmetric encryption with a per-message derived key
|
|
1007
|
+
- `sodium_malloc` / `sodium_memzero` — Secure memory, immediate destruction of keys
|
|
1008
|
+
|
|
1009
|
+
**Files**:
|
|
1010
|
+
- `src/crypto/DoubleRatchet.js` — The ratchet's main class (KDF_RK, KDF_CK, encrypt, decrypt, skipped keys)
|
|
1011
|
+
- Integrated into `Handshake.js` (per-peer ratchet management) and `ChatController.js` (wiring)
|
|
1012
|
+
|
|
1013
|
+
**Wire format**: Ratcheted msgs include `ephemeralPublicKey`, `counter`, `previousCounter` in the payload. Static msgs have no `ephemeralPublicKey`. The server is unaffected (opaque relay).
|
|
1014
|
+
|
|
1015
|
+
**Anti-replay**: The ratchet uses its own counter (no NonceManager needed). Static msgs continue to use the NonceManager.
|
|
1016
|
+
|
|
1017
|
+
### 11.2 TOFU + SAS (Identity Verification) — IMPLEMENTED
|
|
1018
|
+
|
|
1019
|
+
**TOFU (Trust On First Use)**: When connecting with a peer for the first time, the public key fingerprint is saved locally in `.ciphermesh/trusted-peers.json`. On subsequent connections, if the key changes, the user is alerted (similar to SSH's `known_hosts`).
|
|
1020
|
+
|
|
1021
|
+
**SAS (Short Authentication String)**: A 6-digit code that both sides compute independently. It enables out-of-band verification (by voice, in person) to confirm there is no MITM.
|
|
1022
|
+
|
|
1023
|
+
**Commands**:
|
|
1024
|
+
- `/verify <nick>` — Shows the 6-digit SAS code
|
|
1025
|
+
- `/verify-confirm <nick>` — Marks a peer as verified after confirming the SAS
|
|
1026
|
+
- `/trust <nick>` — Accepts a peer's new key (resets verification)
|
|
1027
|
+
- `/trustlist` — Trust status of all online peers
|
|
1028
|
+
|
|
1029
|
+
**Trust model for key rotation**:
|
|
1030
|
+
- Authenticated E2E rotation (via the encrypted channel, action `key_rotation`) → `autoUpdatePeer()` preserves the `verified` status
|
|
1031
|
+
- Rotation via the server (`PEER_KEY_UPDATED`) → does NOT update the trust store (unauthenticated, potential MITM)
|
|
1032
|
+
|
|
1033
|
+
### 11.3 Secure Memory Wipe — IMPLEMENTED
|
|
1034
|
+
|
|
1035
|
+
**Problem**: After decrypting, the plaintext remained in a normal `Buffer.alloc()`, subject to swap and GC.
|
|
1036
|
+
|
|
1037
|
+
**Solution**:
|
|
1038
|
+
- `unpadSecure(padded)`: Copies the plaintext to `sodium_malloc()`, wipes the original padded buffer
|
|
1039
|
+
- `encrypt()` in MessageCrypto and DoubleRatchet: `sodium_memzero(padded)` after encryption
|
|
1040
|
+
- `decrypt()` in DoubleRatchet: `sodium_memzero(padded)` in case of an invalid MAC
|
|
1041
|
+
- `ChatController.#onEncryptedMessage`: `finally { sodium_memzero(plaintext) }` after processing
|
|
1042
|
+
|
|
1043
|
+
**Limitation**: JS strings (`toString('utf-8')`, `JSON.parse()`) CANNOT be wiped — the V8 GC controls their lifetime. Only data in a `Buffer` is wiped.
|
|
1044
|
+
|
|
1045
|
+
### 11.4 Reconnect with State — IMPLEMENTED
|
|
1046
|
+
|
|
1047
|
+
**Problem**: On disconnecting and reconnecting, the ratchets and keys were lost, forcing renegotiation.
|
|
1048
|
+
|
|
1049
|
+
**Solution**: `StateManager` persists encrypted state with the user's passphrase.
|
|
1050
|
+
|
|
1051
|
+
**Flow**:
|
|
1052
|
+
```
|
|
1053
|
+
Startup:
|
|
1054
|
+
1. Se existe estado salvo → prompt passphrase → loadState() → restaura KeyManager, Handshake, peers
|
|
1055
|
+
2. Se nao existe → prompt passphrase opcional (para proteger sessao futura)
|
|
1056
|
+
|
|
1057
|
+
Shutdown (Ctrl+C, /quit):
|
|
1058
|
+
Se passphrase definida → serializeState() → saveState() cifrado
|
|
1059
|
+
```
|
|
1060
|
+
|
|
1061
|
+
**State encryption**:
|
|
1062
|
+
- KDF: `crypto_pwhash` (Argon2id) with ops=3, mem=256MB → 32-byte KEK
|
|
1063
|
+
- Cipher: `crypto_secretbox_easy` (symmetric XSalsa20-Poly1305) with the KEK
|
|
1064
|
+
- Envelope: `{ salt, nonce, ciphertext }` in JSON
|
|
1065
|
+
|
|
1066
|
+
**Serialization**:
|
|
1067
|
+
- `DoubleRatchet.serialize()` / `deserialize()` — all private fields in base64, secrets in `sodium_malloc`
|
|
1068
|
+
- `KeyManager.serialize()` / `deserialize()` — publicKey + secretKey
|
|
1069
|
+
- `Handshake.serializeState()` / `restoreState()` — ratchets + peerKeys + mySessionId
|
|
1070
|
+
- `Handshake.migrateRatchet(oldId, newId)` — re-maps the ratchet when the sessionId changes on reconnect
|
|
1071
|
+
|
|
1072
|
+
### 11.5 P2P with mDNS — IMPLEMENTED
|
|
1073
|
+
|
|
1074
|
+
**Alternative mode** (`npm run p2p`) that eliminates the central server using discovery via mDNS on the LAN.
|
|
1075
|
+
|
|
1076
|
+
**Architecture**:
|
|
1077
|
+
```
|
|
1078
|
+
LAN (mDNS)
|
|
1079
|
+
┌────── _ciphermesh._tcp ──────┐
|
|
1080
|
+
│ │
|
|
1081
|
+
┌─────┴─────┐ ┌─────┴─────┐
|
|
1082
|
+
│ Peer A │◄──── WS ──────►│ Peer B │
|
|
1083
|
+
│ PeerServer│ direto │ PeerServer│
|
|
1084
|
+
│ :random │ │ :random │
|
|
1085
|
+
└───────────┘ └───────────┘
|
|
1086
|
+
```
|
|
1087
|
+
|
|
1088
|
+
**Components**:
|
|
1089
|
+
- `Discovery.js` — publishes/searches the `_ciphermesh._tcp` service via `bonjour-service` (pure JS, Windows-compatible)
|
|
1090
|
+
- `PeerServer.js` — WebSocket server on a random port for inbound connections
|
|
1091
|
+
- `PeerConnectionManager.js` — manages all connections + deduplication
|
|
1092
|
+
- `P2PChatController.js` — same crypto (DoubleRatchet, TOFU, SAS, key rotation)
|
|
1093
|
+
|
|
1094
|
+
**Connection deduplication**: When Alice and Bob discover each other simultaneously via mDNS, the peer with the lexicographically smaller nickname initiates. Result: exactly 1 WebSocket between each pair.
|
|
1095
|
+
|
|
1096
|
+
**P2P protocol**:
|
|
1097
|
+
- `p2p_handshake`: `{ type, nickname, publicKey, version, timestamp }` — exchanged when the WebSocket opens
|
|
1098
|
+
- `p2p_message`: `{ type, payload: { ciphertext, nonce, ephemeralPublicKey?, counter?, previousCounter? } }` — the same encrypted format
|
|
1099
|
+
|
|
1100
|
+
**Future evolution** — P2P with a DHT (for larger networks):
|
|
1101
|
+
```
|
|
1102
|
+
1. Distributed Hash Table para discovery
|
|
1103
|
+
2. Cada no mantem tabela de roteamento parcial
|
|
1104
|
+
3. Mensagens podem ser roteadas por multiplos hops
|
|
1105
|
+
4. Redundancia e tolerancia a falhas
|
|
1106
|
+
```
|
|
1107
|
+
|
|
1108
|
+
### 11.6 Professional Open-Source Project
|
|
1109
|
+
|
|
1110
|
+
**Repository structure**:
|
|
1111
|
+
```
|
|
1112
|
+
securelan-chat/
|
|
1113
|
+
├── .github/
|
|
1114
|
+
│ ├── workflows/
|
|
1115
|
+
│ │ ├── ci.yml # CI: lint + test em cada PR
|
|
1116
|
+
│ │ ├── release.yml # Release automatica com tags
|
|
1117
|
+
│ │ └── security-audit.yml # npm audit semanal
|
|
1118
|
+
│ ├── ISSUE_TEMPLATE/
|
|
1119
|
+
│ │ ├── bug_report.md
|
|
1120
|
+
│ │ └── feature_request.md
|
|
1121
|
+
│ ├── PULL_REQUEST_TEMPLATE.md
|
|
1122
|
+
│ └── CODEOWNERS
|
|
1123
|
+
├── docs/
|
|
1124
|
+
│ ├── ARCHITECTURE.md
|
|
1125
|
+
│ ├── SECURITY.md # Politica de seguranca
|
|
1126
|
+
│ ├── CONTRIBUTING.md # Guia de contribuicao
|
|
1127
|
+
│ └── PROTOCOL.md # Especificacao do protocolo
|
|
1128
|
+
├── LICENSE # MIT ou Apache-2.0
|
|
1129
|
+
├── CHANGELOG.md # Historico de mudancas (semver)
|
|
1130
|
+
├── CODE_OF_CONDUCT.md
|
|
1131
|
+
└── SECURITY.md # Como reportar vulnerabilidades
|
|
1132
|
+
```
|
|
1133
|
+
|
|
1134
|
+
**Best practices**:
|
|
1135
|
+
- Semantic versioning (semver)
|
|
1136
|
+
- Conventional commits
|
|
1137
|
+
- CI/CD with GitHub Actions
|
|
1138
|
+
- Dependabot to update dependencies
|
|
1139
|
+
- CodeQL for static security analysis
|
|
1140
|
+
- Releases signed with GPG
|
|
1141
|
+
- Documentation on GitHub Pages
|
|
1142
|
+
- Badges in the README (CI, coverage, license, version)
|
|
1143
|
+
- Issue templates and PR templates
|
|
1144
|
+
- Security policy with a responsible-disclosure process
|
|
1145
|
+
|
|
1146
|
+
### 11.7 Other Improvements
|
|
1147
|
+
|
|
1148
|
+
| Improvement | Priority | Complexity |
|
|
1149
|
+
|----------|------------|-------------|
|
|
1150
|
+
| Group messages with a group key | High | Medium |
|
|
1151
|
+
| ~~Encrypted file transfer~~ | ~~Medium~~ | ~~Medium~~ | ✅ Implemented |
|
|
1152
|
+
| ~~"Typing..." indicator~~ | ~~Low~~ | ~~Low~~ | ✅ Implemented |
|
|
1153
|
+
| ~~Sound notifications~~ | ~~Low~~ | ~~Low~~ | ✅ Implemented |
|
|
1154
|
+
| ~~Offline messages (server-side queue)~~ | ~~Medium~~ | ~~High~~ | ✅ Implemented |
|
|
1155
|
+
| Multiple devices per user | Low | High |
|
|
1156
|
+
| ~~Automatic key rotation~~ | ~~High~~ | ~~Medium~~ | ✅ Implemented |
|
|
1157
|
+
| ~~Message padding (anti-metadata)~~ | ~~Medium~~ | ~~Low~~ | ✅ Implemented |
|
|
1158
|
+
| ~~TLS on the WebSocket (wss://)~~ | ~~Medium~~ | ~~Low~~ | ✅ Implemented |
|
|
1159
|
+
| Server authentication (certificate) | Medium | Medium |
|
|
1160
|
+
| ~~Reconnect with encrypted state~~ | ~~High~~ | ~~High~~ | ✅ Implemented |
|
|
1161
|
+
| ~~P2P with mDNS (alternative mode)~~ | ~~Medium~~ | ~~High~~ | ✅ Implemented |
|
|
1162
|
+
|
|
1163
|
+
---
|
|
1164
|
+
|
|
1165
|
+
## 12. Glossary
|
|
1166
|
+
|
|
1167
|
+
| Term | Definition |
|
|
1168
|
+
|-------|-----------|
|
|
1169
|
+
| **E2EE** | End-to-End Encryption. Encryption where only the endpoints (sender and recipient) can read the content. Intermediaries (servers) have no access. |
|
|
1170
|
+
| **Curve25519** | An elliptic curve designed by Daniel J. Bernstein. Offers 128 bits of security with 32-byte keys. The basis of X25519 (key exchange). |
|
|
1171
|
+
| **X25519** | A key-exchange protocol (Diffie-Hellman) based on Curve25519. Two parties with different keys arrive at a shared secret. |
|
|
1172
|
+
| **XSalsa20** | A stream cipher. A variant of Salsa20 with an extended 24-byte nonce (vs. 8 in the original Salsa20). Designed by DJB. |
|
|
1173
|
+
| **Poly1305** | A Message Authentication Code (MAC). Generates a 16-byte tag proving the message was not altered. Combined with XSalsa20, it forms `crypto_box`. |
|
|
1174
|
+
| **AEAD** | Authenticated Encryption with Associated Data. A cipher that simultaneously guarantees confidentiality (nobody reads) and integrity (nobody alters). |
|
|
1175
|
+
| **Nonce** | Number Used Once. A unique value used in each encryption operation. If reused with the same key, security is compromised. |
|
|
1176
|
+
| **PFS** | Perfect Forward Secrecy. A property where the compromise of long-term keys does not affect past sessions. |
|
|
1177
|
+
| **DH** | Diffie-Hellman. A protocol that lets two parties establish a shared secret over an insecure channel. |
|
|
1178
|
+
| **MAC** | Message Authentication Code. A function that produces a verifiable tag guaranteeing a message's integrity and authenticity. |
|
|
1179
|
+
| **Fingerprint** | A short hash of a public key, used for human verification of identity. |
|
|
1180
|
+
| **Relay** | A server that forwards data without interpreting the content. |
|
|
1181
|
+
| **Handshake** | The process of establishing a secure connection between two parties, including key exchange and identity verification. |
|
|
1182
|
+
| **Side-channel attack** | An attack that exploits information leaked by the implementation (execution time, power consumption, cache) instead of attacking the algorithm mathematically. |
|
|
1183
|
+
| **sodium_malloc** | A libsodium function that allocates protected memory: does not go to swap, is zeroed when freed, and is protected against reads by other processes. |
|
|
1184
|
+
| **Double Ratchet** | An algorithm used by Signal that combines a DH ratchet with a KDF chain to guarantee per-message PFS. |
|
|
1185
|
+
| **TOFU** | Trust On First Use. A trust model where a peer's public key is accepted on the first connection and saved locally. Later changes trigger alerts (similar to SSH known_hosts). |
|
|
1186
|
+
| **SAS** | Short Authentication String. A short code (6 digits) derived from both sides' public keys, used for out-of-band identity verification. |
|
|
1187
|
+
| **BLAKE2b** | A fast and secure cryptographic hash function. Used as the KDF in the Double Ratchet and to compute the SAS. Supports keyed hashing (MAC). |
|
|
1188
|
+
| **mDNS** | Multicast DNS. A protocol for name resolution on local networks without a central DNS server. Used for service discovery (like Apple's Bonjour). |
|