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.
Files changed (58) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +251 -0
  3. package/README.pt-BR.md +253 -0
  4. package/bin/ciphermesh.js +34 -0
  5. package/docs/ARCHITECTURE.md +1188 -0
  6. package/docs/SETUP.md +305 -0
  7. package/docs/demo.svg +46 -0
  8. package/package.json +87 -0
  9. package/src/client/ChatController.js +2476 -0
  10. package/src/client/Connection.js +129 -0
  11. package/src/client/FileTransfer.js +488 -0
  12. package/src/client/ImagePreview.js +88 -0
  13. package/src/client/UI.js +1830 -0
  14. package/src/client/index.js +231 -0
  15. package/src/crypto/CertPinStore.js +79 -0
  16. package/src/crypto/DeniableEncrypt.js +53 -0
  17. package/src/crypto/DoubleRatchet.js +574 -0
  18. package/src/crypto/Handshake.js +219 -0
  19. package/src/crypto/HistoryStore.js +241 -0
  20. package/src/crypto/IdentityBackup.js +70 -0
  21. package/src/crypto/KeyManager.js +134 -0
  22. package/src/crypto/MessageCrypto.js +181 -0
  23. package/src/crypto/NonceManager.js +72 -0
  24. package/src/crypto/SealedSender.js +58 -0
  25. package/src/crypto/SenderKey.js +204 -0
  26. package/src/crypto/StateManager.js +138 -0
  27. package/src/crypto/TrustStore.js +216 -0
  28. package/src/p2p/Discovery.js +80 -0
  29. package/src/p2p/P2PChatController.js +1856 -0
  30. package/src/p2p/PeerConnectionManager.js +252 -0
  31. package/src/p2p/PeerServer.js +68 -0
  32. package/src/p2p/index.js +219 -0
  33. package/src/protocol/messages.js +138 -0
  34. package/src/protocol/validators.js +175 -0
  35. package/src/server/CertManager.js +173 -0
  36. package/src/server/MessageRouter.js +80 -0
  37. package/src/server/OfflineQueue.js +124 -0
  38. package/src/server/SessionManager.js +296 -0
  39. package/src/server/WebSocketServer.js +632 -0
  40. package/src/server/index.js +89 -0
  41. package/src/shared/AuditLog.js +91 -0
  42. package/src/shared/PluginManager.js +83 -0
  43. package/src/shared/banner.js +271 -0
  44. package/src/shared/commandSuggest.js +59 -0
  45. package/src/shared/config.js +90 -0
  46. package/src/shared/constants.js +126 -0
  47. package/src/shared/coverTraffic.js +34 -0
  48. package/src/shared/dnd.js +60 -0
  49. package/src/shared/emoji.js +17 -0
  50. package/src/shared/fuzzy.js +40 -0
  51. package/src/shared/invite.js +61 -0
  52. package/src/shared/keyArt.js +66 -0
  53. package/src/shared/logger.js +38 -0
  54. package/src/shared/panic.js +38 -0
  55. package/src/shared/prompt.js +31 -0
  56. package/src/shared/terminalGraphics.js +72 -0
  57. package/src/shared/themes.js +36 -0
  58. package/src/shared/voiceNote.js +128 -0
@@ -0,0 +1,216 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync, statSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import sodium from 'sodium-native';
4
+ import { KeyManager } from './KeyManager.js';
5
+
6
+ const TRUST_DIR = '.ciphermesh';
7
+ const TRUST_FILE = 'trusted-peers.json';
8
+
9
+ export const TrustResult = {
10
+ NEW_PEER: 'new_peer',
11
+ TRUSTED: 'trusted',
12
+ MISMATCH: 'mismatch',
13
+ VERIFIED_MISMATCH: 'verified_mismatch',
14
+ };
15
+
16
+ export class TrustStore {
17
+ #storePath;
18
+ #store; // Map<lowerNickname, record>
19
+
20
+ constructor(baseDir = process.cwd()) {
21
+ const dir = join(baseDir, TRUST_DIR);
22
+ if (!existsSync(dir)) {
23
+ mkdirSync(dir, { recursive: true });
24
+ }
25
+ this.#storePath = join(dir, TRUST_FILE);
26
+ this.#store = new Map();
27
+ this.#load();
28
+ }
29
+
30
+ #load() {
31
+ try {
32
+ if (existsSync(this.#storePath)) {
33
+ const raw = readFileSync(this.#storePath, 'utf-8');
34
+ const data = JSON.parse(raw);
35
+ for (const [nick, record] of Object.entries(data)) {
36
+ this.#store.set(nick, record);
37
+ }
38
+ }
39
+ } catch {
40
+ this.#store = new Map();
41
+ }
42
+ }
43
+
44
+ #save() {
45
+ const obj = Object.fromEntries(this.#store);
46
+ writeFileSync(this.#storePath, JSON.stringify(obj, null, 2), {
47
+ encoding: 'utf-8',
48
+ mode: 0o600,
49
+ });
50
+ }
51
+
52
+ // Securely delete the trust store from disk (panic / duress).
53
+ wipe() {
54
+ this.#store = new Map();
55
+ try {
56
+ if (existsSync(this.#storePath)) {
57
+ writeFileSync(this.#storePath, Buffer.alloc(Math.max(256, statSync(this.#storePath).size)));
58
+ unlinkSync(this.#storePath);
59
+ }
60
+ } catch {
61
+ /* best effort */
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Check if a peer's fingerprint matches the stored one.
67
+ */
68
+ checkPeer(nickname, publicKeyB64) {
69
+ const key = nickname.toLowerCase();
70
+ const record = this.#store.get(key);
71
+
72
+ if (!record) {
73
+ return TrustResult.NEW_PEER;
74
+ }
75
+
76
+ // Compare the FULL public key (256 bits), not the 64-bit fingerprint.
77
+ // Fall back to the fingerprint only for legacy records without publicKey.
78
+ const matches = record.publicKey
79
+ ? record.publicKey === publicKeyB64
80
+ : record.fingerprint === KeyManager.computeFingerprint(Buffer.from(publicKeyB64, 'base64'));
81
+
82
+ if (matches) {
83
+ record.lastSeen = Date.now();
84
+ this.#save();
85
+ return TrustResult.TRUSTED;
86
+ }
87
+
88
+ if (record.verified) {
89
+ return TrustResult.VERIFIED_MISMATCH;
90
+ }
91
+ return TrustResult.MISMATCH;
92
+ }
93
+
94
+ /**
95
+ * Record a first-time peer.
96
+ */
97
+ recordPeer(nickname, publicKeyB64) {
98
+ const key = nickname.toLowerCase();
99
+ const fingerprint = KeyManager.computeFingerprint(Buffer.from(publicKeyB64, 'base64'));
100
+ this.#store.set(key, {
101
+ fingerprint,
102
+ publicKey: publicKeyB64,
103
+ firstSeen: Date.now(),
104
+ lastSeen: Date.now(),
105
+ verified: false,
106
+ });
107
+ this.#save();
108
+ }
109
+
110
+ /**
111
+ * User explicitly accepts a new key (resets verified status).
112
+ */
113
+ updatePeer(nickname, publicKeyB64) {
114
+ const key = nickname.toLowerCase();
115
+ const fingerprint = KeyManager.computeFingerprint(Buffer.from(publicKeyB64, 'base64'));
116
+ const record = this.#store.get(key);
117
+ if (!record) {
118
+ this.recordPeer(nickname, publicKeyB64);
119
+ return;
120
+ }
121
+ record.fingerprint = fingerprint;
122
+ record.publicKey = publicKeyB64;
123
+ record.lastSeen = Date.now();
124
+ record.verified = false;
125
+ this.#save();
126
+ }
127
+
128
+ /**
129
+ * Auto-update from authenticated E2E key rotation (preserves verified status).
130
+ */
131
+ autoUpdatePeer(nickname, publicKeyB64) {
132
+ const key = nickname.toLowerCase();
133
+ const record = this.#store.get(key);
134
+ if (!record) {
135
+ this.recordPeer(nickname, publicKeyB64);
136
+ return;
137
+ }
138
+ const fingerprint = KeyManager.computeFingerprint(Buffer.from(publicKeyB64, 'base64'));
139
+ record.fingerprint = fingerprint;
140
+ record.publicKey = publicKeyB64;
141
+ record.lastSeen = Date.now();
142
+ this.#save();
143
+ }
144
+
145
+ /**
146
+ * Compute a Short Authentication String from both public keys.
147
+ * ~40 bits of entropy shown as 13 grouped decimal digits (was 6 digits / ~20
148
+ * bits, which is grindable offline). Both sides compute the same value; it is
149
+ * only ever compared by humans out-of-band, never typed back.
150
+ */
151
+ static computeSAS(myPublicKey, peerPublicKey) {
152
+ const myPub = Buffer.isBuffer(myPublicKey) ? myPublicKey : Buffer.from(myPublicKey, 'base64');
153
+ const peerPub = Buffer.isBuffer(peerPublicKey)
154
+ ? peerPublicKey
155
+ : Buffer.from(peerPublicKey, 'base64');
156
+
157
+ // Sort lexicographically for deterministic ordering
158
+ const [first, second] =
159
+ Buffer.compare(myPub, peerPub) <= 0 ? [myPub, peerPub] : [peerPub, myPub];
160
+
161
+ // BLAKE2b-256(pubA || pubB || domain separator)
162
+ const context = Buffer.from('CipherMesh-SAS-v1');
163
+ const input = Buffer.concat([first, second, context]);
164
+ const hash = Buffer.alloc(32);
165
+ sodium.crypto_generichash(hash, input);
166
+
167
+ // First 5 bytes (40 bits) → up to 13 decimal digits, grouped 4-4-5.
168
+ let num = 0n;
169
+ for (let i = 0; i < 5; i++) {
170
+ num = (num << 8n) | BigInt(hash[i]);
171
+ }
172
+ const digits = num.toString().padStart(13, '0');
173
+ return `${digits.slice(0, 4)} ${digits.slice(4, 8)} ${digits.slice(8)}`;
174
+ }
175
+
176
+ markVerified(nickname) {
177
+ const key = nickname.toLowerCase();
178
+ const record = this.#store.get(key);
179
+ if (record) {
180
+ record.verified = true;
181
+ this.#save();
182
+ return true;
183
+ }
184
+ return false;
185
+ }
186
+
187
+ isVerified(nickname) {
188
+ const key = nickname.toLowerCase();
189
+ const record = this.#store.get(key);
190
+ return record?.verified === true;
191
+ }
192
+
193
+ getPeerRecord(nickname) {
194
+ return this.#store.get(nickname.toLowerCase()) || null;
195
+ }
196
+
197
+ /** Export all trust records as a plain object (for identity backup). */
198
+ exportData() {
199
+ return Object.fromEntries(this.#store);
200
+ }
201
+
202
+ /** Merge trust records from a backup, preferring imported verified peers. */
203
+ importData(obj) {
204
+ if (!obj || typeof obj !== 'object') {
205
+ return;
206
+ }
207
+ for (const [nick, record] of Object.entries(obj)) {
208
+ const existing = this.#store.get(nick);
209
+ // Keep whichever record is verified; otherwise the imported one wins.
210
+ if (!existing || record.verified || !existing.verified) {
211
+ this.#store.set(nick, record);
212
+ }
213
+ }
214
+ this.#save();
215
+ }
216
+ }
@@ -0,0 +1,80 @@
1
+ import { Bonjour } from 'bonjour-service';
2
+ import { EventEmitter } from 'node:events';
3
+ import { PROTOCOL_VERSION } from '../shared/constants.js';
4
+
5
+ const SERVICE_TYPE = 'ciphermesh';
6
+
7
+ export class Discovery extends EventEmitter {
8
+ #bonjour;
9
+ #service;
10
+ #browser;
11
+ #myNickname;
12
+
13
+ constructor() {
14
+ super();
15
+ this.#bonjour = new Bonjour();
16
+ this.#service = null;
17
+ this.#browser = null;
18
+ this.#myNickname = null;
19
+ }
20
+
21
+ /**
22
+ * Publish our service and start browsing for peers.
23
+ */
24
+ start(nickname, port, publicKeyB64) {
25
+ this.#myNickname = nickname;
26
+
27
+ // Publish mDNS service
28
+ this.#service = this.#bonjour.publish({
29
+ name: `ciphermesh-${nickname}`,
30
+ type: SERVICE_TYPE,
31
+ port,
32
+ txt: {
33
+ nickname,
34
+ publicKey: publicKeyB64,
35
+ version: String(PROTOCOL_VERSION),
36
+ },
37
+ });
38
+
39
+ // Browse for other CipherMesh peers
40
+ this.#browser = this.#bonjour.find({ type: SERVICE_TYPE });
41
+
42
+ this.#browser.on('up', (service) => {
43
+ const peerNick = service.txt?.nickname;
44
+ if (!peerNick || peerNick === this.#myNickname) {
45
+ return;
46
+ }
47
+
48
+ // Prefer referer address (actual IP), fallback to host
49
+ const host = service.referer?.address || service.host;
50
+
51
+ this.emit('peer-discovered', {
52
+ nickname: peerNick,
53
+ host,
54
+ port: service.port,
55
+ publicKey: service.txt.publicKey,
56
+ });
57
+ });
58
+
59
+ this.#browser.on('down', (service) => {
60
+ const peerNick = service.txt?.nickname;
61
+ if (!peerNick || peerNick === this.#myNickname) {
62
+ return;
63
+ }
64
+
65
+ this.emit('peer-lost', { nickname: peerNick });
66
+ });
67
+ }
68
+
69
+ stop() {
70
+ if (this.#browser) {
71
+ this.#browser.stop();
72
+ this.#browser = null;
73
+ }
74
+ if (this.#service) {
75
+ this.#service.stop();
76
+ this.#service = null;
77
+ }
78
+ this.#bonjour.destroy();
79
+ }
80
+ }