fb-messenger-e2ee 0.1.0 → 0.1.2

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 (110) hide show
  1. package/README.md +34 -6
  2. package/dist/index.cjs +8811 -0
  3. package/dist/index.cjs.map +1 -0
  4. package/dist/index.d.cts +885 -0
  5. package/dist/index.d.ts +885 -0
  6. package/dist/index.js +8843 -0
  7. package/dist/index.js.map +1 -0
  8. package/package.json +19 -4
  9. package/bun.lock +0 -1014
  10. package/examples/basic-usage.ts +0 -52
  11. package/jest.config.ts +0 -18
  12. package/src/config/env.ts +0 -15
  13. package/src/controllers/client.controller.ts +0 -805
  14. package/src/controllers/dgw-handler.ts +0 -171
  15. package/src/controllers/e2ee-handler.ts +0 -832
  16. package/src/controllers/event-mapper.ts +0 -327
  17. package/src/core/client.ts +0 -151
  18. package/src/e2ee/application/e2ee-client.ts +0 -376
  19. package/src/e2ee/application/fanout-planner.ts +0 -79
  20. package/src/e2ee/application/outbound-message-cache.ts +0 -58
  21. package/src/e2ee/application/prekey-maintenance.ts +0 -55
  22. package/src/e2ee/application/retry-manager.ts +0 -156
  23. package/src/e2ee/facebook/facebook-protocol-utils.ts +0 -230
  24. package/src/e2ee/facebook/icdc-payload.ts +0 -31
  25. package/src/e2ee/index.ts +0 -76
  26. package/src/e2ee/internal.ts +0 -27
  27. package/src/e2ee/media/media-crypto.ts +0 -147
  28. package/src/e2ee/media/media-upload.ts +0 -90
  29. package/src/e2ee/message/builders/client-payload.ts +0 -54
  30. package/src/e2ee/message/builders/consumer-application.ts +0 -362
  31. package/src/e2ee/message/builders/message-application.ts +0 -64
  32. package/src/e2ee/message/builders/message-transport.ts +0 -84
  33. package/src/e2ee/message/codecs/protobuf-codecs.ts +0 -101
  34. package/src/e2ee/message/constants.ts +0 -4
  35. package/src/e2ee/message/message-builder.ts +0 -15
  36. package/src/e2ee/signal/prekey-manager.ts +0 -140
  37. package/src/e2ee/signal/signal-manager.ts +0 -365
  38. package/src/e2ee/store/device-json.ts +0 -47
  39. package/src/e2ee/store/device-repository.ts +0 -12
  40. package/src/e2ee/store/device-store.ts +0 -538
  41. package/src/e2ee/transport/binary/decoder.ts +0 -180
  42. package/src/e2ee/transport/binary/encoder.ts +0 -143
  43. package/src/e2ee/transport/binary/stanzas.ts +0 -97
  44. package/src/e2ee/transport/binary/tokens.ts +0 -64
  45. package/src/e2ee/transport/binary/wa-binary.ts +0 -8
  46. package/src/e2ee/transport/dgw/dgw-socket.ts +0 -345
  47. package/src/e2ee/transport/noise/noise-handshake.ts +0 -417
  48. package/src/e2ee/transport/noise/noise-socket.ts +0 -230
  49. package/src/index.ts +0 -34
  50. package/src/models/client.ts +0 -26
  51. package/src/models/config.ts +0 -14
  52. package/src/models/domain.ts +0 -299
  53. package/src/models/e2ee.ts +0 -295
  54. package/src/models/media.ts +0 -41
  55. package/src/models/messaging.ts +0 -88
  56. package/src/models/thread.ts +0 -69
  57. package/src/repositories/session.repository.ts +0 -20
  58. package/src/services/auth.service.ts +0 -55
  59. package/src/services/e2ee.service.ts +0 -174
  60. package/src/services/facebook-gateway.service.ts +0 -245
  61. package/src/services/icdc.service.ts +0 -177
  62. package/src/services/media.service.ts +0 -304
  63. package/src/services/messaging.service.ts +0 -28
  64. package/src/services/thread.service.ts +0 -199
  65. package/src/types/advanced-types.ts +0 -61
  66. package/src/types/fca-unofficial.d.ts +0 -212
  67. package/src/types/protocol-types.ts +0 -43
  68. package/src/utils/fca-utils.ts +0 -30
  69. package/src/utils/logger.ts +0 -24
  70. package/src/utils/mime.ts +0 -51
  71. package/tests/.env.example +0 -87
  72. package/tests/data/1x1.png +0 -0
  73. package/tests/data/example.txt +0 -1
  74. package/tests/data/file_example.mp3 +0 -0
  75. package/tests/data/file_example.mp4 +0 -0
  76. package/tests/integration.test.ts +0 -498
  77. package/tests/script/echo-e2ee.ts +0 -174
  78. package/tests/script/send-e2ee-media.ts +0 -227
  79. package/tests/script/send-e2ee-reaction.ts +0 -108
  80. package/tests/script/send-e2ee-unsend.ts +0 -115
  81. package/tests/script/send-typing.ts +0 -107
  82. package/tests/script/test-group-send.ts +0 -105
  83. package/tests/setup.ts +0 -3
  84. package/tests/types.test.ts +0 -57
  85. package/tests/unit/controllers/dgw-handler.test.ts +0 -60
  86. package/tests/unit/controllers/e2ee-handler.test.ts +0 -293
  87. package/tests/unit/controllers/event-mapper.test.ts +0 -252
  88. package/tests/unit/e2ee/application-helpers.test.ts +0 -418
  89. package/tests/unit/e2ee/device-store.test.ts +0 -126
  90. package/tests/unit/e2ee/facebook-protocol-utils.test.ts +0 -134
  91. package/tests/unit/e2ee/media-crypto.test.ts +0 -55
  92. package/tests/unit/e2ee/media-upload.test.ts +0 -60
  93. package/tests/unit/e2ee/message-builder.test.ts +0 -42
  94. package/tests/unit/e2ee/message-builders.test.ts +0 -230
  95. package/tests/unit/e2ee/noise-handshake.test.ts +0 -260
  96. package/tests/unit/e2ee/prekey-manager.test.ts +0 -55
  97. package/tests/unit/e2ee/wa-binary.test.ts +0 -127
  98. package/tests/unit/services/e2ee.service.test.ts +0 -101
  99. package/tests/unit/services/facebook-gateway.service.test.ts +0 -138
  100. package/tests/unit/services/media.service.test.ts +0 -169
  101. package/tests/unit/utils/fca-utils.test.ts +0 -48
  102. package/tsconfig.json +0 -19
  103. /package/{src/e2ee/message/proto → proto}/ArmadilloApplication.proto +0 -0
  104. /package/{src/e2ee/message/proto → proto}/ArmadilloICDC.proto +0 -0
  105. /package/{src/e2ee/message/proto → proto}/ConsumerApplication.proto +0 -0
  106. /package/{src/e2ee/message/proto → proto}/MessageApplication.proto +0 -0
  107. /package/{src/e2ee/message/proto → proto}/MessageTransport.proto +0 -0
  108. /package/{src/e2ee/message/proto → proto}/WACommon.proto +0 -0
  109. /package/{src/e2ee/message/proto → proto}/WAMediaTransport.proto +0 -0
  110. /package/{src/e2ee/message/proto → proto}/proto-writer.ts +0 -0
@@ -1,417 +0,0 @@
1
- /**
2
- * E2EE Noise Handshake - Layer 1 (Transport)
3
- *
4
- * Implements Noise_XX_25519_AESGCM_SHA256 - the transport layer handshake
5
- * used by WhatsApp/Messenger before any Signal Protocol messages.
6
- *
7
- * Flow:
8
- * 1. Client -> Server: ClientHello { ephemeral: EphemeralPub }
9
- * 2. Server -> Client: ServerHello { ephemeral, static(enc), payload(enc=cert) }
10
- * 3. Client -> Server: ClientFinish { static(enc=NoiseKey.Pub), payload(enc=ClientPayload) }
11
- * -> Both sides derive sendKey + recvKey for AES-256-GCM frame encryption
12
- */
13
-
14
- import {
15
- createCipheriv,
16
- createDecipheriv,
17
- createHash,
18
- hkdfSync,
19
- randomBytes,
20
- generateKeyPairSync,
21
- createPublicKey,
22
- createPrivateKey,
23
- diffieHellman,
24
- } from "node:crypto";
25
- import { inflateSync } from "node:zlib";
26
- import type { HandshakeResult, NoiseSocket, RawWebSocket } from "../../../models/e2ee.ts";
27
- import { logger } from "../../../utils/logger.ts";
28
- export type { HandshakeResult, NoiseSocket, RawWebSocket };
29
-
30
- // X25519 Helpers using node:crypto
31
- const SPKI_HEADER = Buffer.from("302a300506032b656e032100", "hex");
32
- const PKCS8_HEADER = Buffer.from("302e020100300506032b656e04220420", "hex");
33
-
34
- export function generateX25519(): { priv: Buffer; pub: Buffer } {
35
- const { privateKey, publicKey } = generateKeyPairSync("x25519");
36
- const privRaw = privateKey.export({ type: "pkcs8", format: "der" });
37
- const pubRaw = publicKey.export({ type: "spki", format: "der" });
38
- return {
39
- priv: Buffer.from(privRaw.subarray(privRaw.length - 32)),
40
- pub: Buffer.from(pubRaw.subarray(pubRaw.length - 32)),
41
- };
42
- }
43
-
44
- export function getX25519PublicKey(privKeyRaw: Buffer): Buffer {
45
- const priv = createPrivateKey({ key: Buffer.concat([PKCS8_HEADER, privKeyRaw]), format: "der", type: "pkcs8" });
46
- const pubDer = createPublicKey(priv).export({ type: "spki", format: "der" });
47
- return Buffer.from(pubDer.subarray(pubDer.length - 32));
48
- }
49
-
50
- export function x25519DH(privKeyRaw: Buffer, pubKeyRaw: Buffer): Buffer {
51
- const pub = createPublicKey({ key: Buffer.concat([SPKI_HEADER, pubKeyRaw]), format: "der", type: "spki" });
52
- const priv = createPrivateKey({ key: Buffer.concat([PKCS8_HEADER, privKeyRaw]), format: "der", type: "pkcs8" });
53
- return diffieHellman({ privateKey: priv, publicKey: pub });
54
- }
55
-
56
- // Hardcoded protocol constants
57
-
58
- // Root certificate public key - Ed25519, 32 bytes
59
- export const WA_CERT_PUB_KEY = Buffer.from(
60
- "142375574d0a587166aae71ebe516437c4a28b73e3695c6ce1f7f9545da8ee6b",
61
- "hex",
62
- );
63
-
64
- // Noise handshake protocol name (WA variant)
65
- const NOISE_START_PATTERN = Buffer.from("Noise_XX_25519_AESGCM_SHA256\0\0\0\0");
66
-
67
- // WA header sent at the start of every connection (version + magic bytes)
68
- export const WA_HEADER = Buffer.from([87, 65, 6, 3]); // "WA\x06\x03"
69
-
70
- // Noise state machine helpers
71
-
72
- class NoiseHandshakeState {
73
- private h: Buffer; // hash (chaining key / handshake hash)
74
- private ck: Buffer; // chaining key
75
- private k: Buffer | null = null; // current symmetric key
76
- private n = 0; // nonce counter
77
-
78
- constructor(startPattern: Buffer, header: Buffer) {
79
- // If protocol_name is exactly 32 bytes, h = protocol_name
80
- if (startPattern.length === 32) {
81
- this.h = Buffer.from(startPattern);
82
- } else {
83
- this.h = this.sha256(startPattern);
84
- }
85
- this.ck = this.h;
86
- // h = SHA256(h ‖ prologue)
87
- this.mixHash(header);
88
- }
89
-
90
- private sha256(data: Buffer): Buffer {
91
- return Buffer.from(createHash("sha256").update(data).digest());
92
- }
93
-
94
- mixHash(data: Buffer): void {
95
- this.h = this.sha256(Buffer.concat([this.h, data]));
96
- }
97
-
98
- mixKey(input: Buffer): void {
99
- // HKDF-SHA256(ck, input) -> ck', k
100
- const expanded = Buffer.from(hkdfSync("sha256", input, this.ck, "", 64));
101
- this.ck = expanded.subarray(0, 32);
102
- this.k = expanded.subarray(32, 64);
103
- this.n = 0;
104
- // logger.debug("noise-handshake", `mixKey -> ck=${this.ck.toString("hex").slice(0, 8)} k=${this.k.toString("hex").slice(0, 8)}`);
105
- }
106
-
107
- mixSharedSecretIntoKey(privKey: Buffer, pubKey: Buffer): void {
108
- const sharedSecret = x25519DH(privKey, pubKey);
109
- this.mixKey(sharedSecret);
110
- }
111
-
112
- /** AES-256-GCM encrypt; ad = current h; updates h */
113
- encrypt(plaintext: Buffer): Buffer {
114
- if (!this.k) throw new Error("No key set in Noise state");
115
- const nonce = this.buildNonce(this.n++);
116
- const cipher = createCipheriv("aes-256-gcm", this.k, nonce);
117
- cipher.setAAD(this.h);
118
- const enc = Buffer.concat([cipher.update(plaintext), cipher.final(), cipher.getAuthTag()]);
119
- this.mixHash(enc);
120
- return enc;
121
- }
122
-
123
- /** AES-256-GCM decrypt; ad = current h; updates h */
124
- decrypt(ciphertext: Buffer): Buffer {
125
- if (!this.k) throw new Error("No key set in Noise state");
126
- const nonce = this.buildNonce(this.n++);
127
- const tag = ciphertext.subarray(-16);
128
- const body = ciphertext.subarray(0, -16);
129
- try {
130
- const decipher = createDecipheriv("aes-256-gcm", this.k, nonce);
131
- decipher.setAAD(this.h);
132
- decipher.setAuthTag(tag);
133
- const plain = Buffer.concat([decipher.update(body), decipher.final()]);
134
- this.mixHash(ciphertext);
135
- return plain;
136
- } catch (err) {
137
- logger.error("CipherState", `Decrypt error at n=${this.n - 1}:`, err);
138
- logger.error("CipherState", ` - Key: ${this.k.toString('hex')}`);
139
- logger.error("CipherState", ` - Nonce: ${nonce.toString('hex')}`);
140
- logger.error("CipherState", ` - AAD: ${this.h.toString('hex')}`);
141
- throw err;
142
- }
143
- }
144
-
145
- /** Derive final send/recv keys from the completed handshake */
146
- finish(): { sendKey: Buffer; recvKey: Buffer } {
147
- const expanded = Buffer.from(hkdfSync("sha256", Buffer.alloc(0), this.ck, Buffer.alloc(0), 64));
148
- return {
149
- sendKey: expanded.subarray(0, 32),
150
- recvKey: expanded.subarray(32, 64),
151
- };
152
- }
153
-
154
- get handshakeHash(): Buffer { return this.h; }
155
-
156
- private buildNonce(counter: number): Buffer {
157
- const nonce = Buffer.alloc(12);
158
- nonce.writeUInt32BE(counter, 8);
159
- return nonce;
160
- }
161
- }
162
-
163
- function verifyCertChain(certChainRaw: Buffer, serverStaticPub: Buffer): void {
164
- // Placeholder - full cert chain verification requires waCert proto parser.
165
- // Trust established via Noise shared-secret derivation instead.
166
- void certChainRaw;
167
- void serverStaticPub;
168
- // TODO: Implement full cert chain verification in production
169
- }
170
-
171
- // Noise handshake + frame socket
172
-
173
-
174
- /** Encrypted frame socket backed by established Noise keys */
175
- class EncryptedFrameSocket implements NoiseSocket {
176
- private ws: RawWebSocket;
177
- private sendKey: Buffer;
178
- private recvKey: Buffer;
179
- private sendCounter: number = 0;
180
- private recvCounter: number = 0;
181
-
182
- constructor(ws: RawWebSocket, sendKey: Buffer, recvKey: Buffer) {
183
- this.ws = ws;
184
- this.sendKey = sendKey;
185
- this.recvKey = recvKey;
186
- }
187
-
188
- private buildNonce(counter: number): Buffer {
189
- const n = Buffer.alloc(12);
190
- n.writeBigUInt64BE(BigInt(counter), 4);
191
- return n;
192
- }
193
-
194
- private encryptFrame(plaintext: Buffer): Buffer {
195
- const nonce = this.buildNonce(this.sendCounter++);
196
- const cipher = createCipheriv("aes-256-gcm", this.sendKey, nonce);
197
- const enc = Buffer.concat([cipher.update(plaintext), cipher.final(), cipher.getAuthTag()]);
198
-
199
- // Prefix with 3-byte big-endian length
200
- const header = Buffer.alloc(3);
201
- header.writeUIntBE(enc.length, 0, 3);
202
-
203
- const fullFrame = Buffer.concat([header, enc]);
204
- logger.debug("noise-handshake", `Sending frame (${fullFrame.length} bytes): ${fullFrame.toString("hex").slice(0, 32)}...`);
205
- return fullFrame;
206
- }
207
-
208
- private decryptFrame(data: Buffer): Buffer {
209
- if (data.length <= 16) {
210
- // Keep-alive or too short to be an encrypted frame
211
- return Buffer.alloc(0);
212
- }
213
- const nonce = this.buildNonce(this.recvCounter++);
214
- const tag = data.subarray(-16);
215
- const body = data.subarray(0, -16);
216
- const decipher = createDecipheriv("aes-256-gcm", this.recvKey, nonce);
217
- decipher.setAuthTag(tag);
218
-
219
- return Buffer.concat([decipher.update(body), decipher.final()]);
220
- }
221
-
222
- async sendFrame(data: Buffer): Promise<void> {
223
- this.ws.send(this.encryptFrame(data));
224
- }
225
-
226
- async readFrame(): Promise<Buffer> {
227
- const header = await this.ws.readRaw(3);
228
- if (!header) {
229
- throw new Error("Socket closed while reading frame header");
230
- }
231
- const len = header.readUIntBE(0, 3);
232
- logger.debug("FacebookE2EESocket", `RAW frame header: ${header.toString('hex')} (len=${len})`);
233
-
234
- const payload = await this.ws.readRaw(len);
235
- try {
236
- const decrypted = this.decryptFrame(payload);
237
- logger.debug("FacebookE2EESocket", `Decrypt successful, result length: ${decrypted.length}`);
238
- return decrypted;
239
- } catch (err) {
240
- logger.error("FacebookE2EESocket", "Decrypt FAILED:", err);
241
- throw err;
242
- }
243
- }
244
-
245
- close(): void { this.ws.close(); }
246
- }
247
-
248
- // Public API: doHandshake
249
-
250
-
251
- /**
252
- * Perform the Noise XX handshake with the WhatsApp/Messenger server.
253
- *
254
- * @param ws Raw WebSocket (caller handles connection)
255
- * @param noiseKeyPriv 32-byte Noise private key from DeviceStore
256
- * @param clientPayload Serialized ClientPayload protobuf (device identity)
257
- */
258
- export async function doHandshake(
259
- ws: RawWebSocket,
260
- noiseKeyPriv: Buffer,
261
- clientPayload: Buffer,
262
- ): Promise<HandshakeResult> {
263
- const state = new NoiseHandshakeState(NOISE_START_PATTERN, WA_HEADER);
264
-
265
- // Generate ephemeral X25519 keypair using node:crypto
266
- const eph = generateX25519();
267
- const ephPriv = eph.priv;
268
- const ephPub = eph.pub;
269
- logger.debug("debug", `ClientEphPriv: ${ephPriv.toString('hex')}`);
270
- logger.debug("debug", `ClientEphPub: ${ephPub.toString('hex')}`);
271
-
272
- state.mixHash(ephPub);
273
-
274
- // Step 1: Send ClientHello
275
- // Proto: HandshakeMessage { clientHello: { ephemeral: ephPub } }
276
- const clientHello = encodeHandshakeMessage({ clientHello: { ephemeral: ephPub } });
277
- ws.send(prependHeader(clientHello));
278
-
279
- // Step 2: Receive ServerHello
280
- const serverHelloRaw = await readRawFrame(ws);
281
- const serverHello = decodeServerHello(serverHelloRaw);
282
-
283
- const serverEphPub = Buffer.from(serverHello.ephemeral);
284
- const serverStaticEnc = Buffer.from(serverHello.static);
285
- logger.debug("debug", `ServerStaticEnc: ${serverStaticEnc.toString('hex')}`);
286
- const certEnc = Buffer.from(serverHello.payload);
287
-
288
- logger.debug("noise-handshake", `Eph: ${serverEphPub.length}, StaticEnc: ${serverStaticEnc.length}, CertEnc: ${certEnc.length}`);
289
-
290
- logger.debug("debug", `ServerEphPub: ${serverEphPub.toString('hex')}`);
291
- state.mixHash(serverEphPub);
292
- state.mixSharedSecretIntoKey(ephPriv, serverEphPub);
293
- logger.debug("debug", `After ServerHello mix: k=${state['k']?.toString('hex')}, h=${state['h']?.toString('hex')}`);
294
-
295
- const serverStaticPub = state.decrypt(serverStaticEnc);
296
- logger.debug("noise-handshake", `Decrypted Server Static Pub: ${serverStaticPub.toString("hex")}`);
297
- state.mixSharedSecretIntoKey(ephPriv, serverStaticPub);
298
-
299
- const certDecrypted = state.decrypt(certEnc);
300
- verifyCertChain(certDecrypted, serverStaticPub);
301
-
302
- // Step 3: Send ClientFinish
303
- const noiseKeyPub = getX25519PublicKey(noiseKeyPriv);
304
- const encNoisePub = state.encrypt(noiseKeyPub);
305
- state.mixSharedSecretIntoKey(noiseKeyPriv, serverEphPub);
306
-
307
- const encPayload = state.encrypt(clientPayload);
308
- const clientFinish = encodeHandshakeMessage({
309
- clientFinish: { static: encNoisePub, payload: encPayload },
310
- });
311
- const finishFrame = prependLength(clientFinish);
312
- logger.debug("noise-handshake", `Sending clientFinish frame (${finishFrame.length} bytes): ${finishFrame.toString("hex").slice(0, 32)}...`);
313
- ws.send(finishFrame);
314
-
315
- // Derive final keys
316
- const { sendKey, recvKey } = state.finish();
317
-
318
- return {
319
- socket: new EncryptedFrameSocket(ws, sendKey, recvKey),
320
- };
321
- }
322
-
323
- // Minimal protobuf helpers for handshake messages
324
-
325
- function encodeHandshakeMessage(msg: {
326
- clientHello?: { ephemeral: Buffer };
327
- clientFinish?: { static: Buffer; payload: Buffer };
328
- }): Buffer {
329
- // HandshakeMessage { field 2 = ClientHello { field 1 = ephemeral } }
330
- // HandshakeMessage { field 4 = ClientFinish { field 1 = static, field 3 = payload } }
331
- // Note: ClientFinish payload is field 3, not 2! (Wait, let me double check waProto.proto)
332
- // Let me output the fix for HandshakeMessage first.
333
- const chunks: Buffer[] = [];
334
-
335
- if (msg.clientHello) {
336
- const hello = encLenDelim(1, msg.clientHello.ephemeral);
337
- chunks.push(encLenDelim(2, hello));
338
- }
339
- if (msg.clientFinish) {
340
- const finish = Buffer.concat([
341
- encLenDelim(1, msg.clientFinish.static),
342
- encLenDelim(2, msg.clientFinish.payload),
343
- ]);
344
- chunks.push(encLenDelim(4, finish));
345
- }
346
-
347
- return Buffer.concat(chunks);
348
- }
349
-
350
- function decodeServerHello(data: Buffer): {
351
- ephemeral: Buffer;
352
- static: Buffer;
353
- payload: Buffer;
354
- } {
355
- // HandshakeMessage { field 3 = ServerHello { field 1 = ephemeral, field 2 = static, field 3 = payload } }
356
- const serverHelloRaw = decodeLenDelim(data, 3);
357
- return {
358
- ephemeral: decodeLenDelim(serverHelloRaw, 1),
359
- static: decodeLenDelim(serverHelloRaw, 2),
360
- payload: decodeLenDelim(serverHelloRaw, 3),
361
- };
362
- }
363
-
364
- // Tiny protobuf encoder helpers
365
- function encVarint(n: number): Buffer {
366
- const bytes: number[] = [];
367
- let v = n >>> 0;
368
- while (v > 127) { bytes.push((v & 0x7f) | 0x80); v >>>= 7; }
369
- bytes.push(v);
370
- return Buffer.from(bytes);
371
- }
372
-
373
- function encLenDelim(field: number, data: Buffer): Buffer {
374
- return Buffer.concat([encVarint((field << 3) | 2), encVarint(data.length), data]);
375
- }
376
-
377
- function decodeLenDelim(data: Buffer, targetField: number): Buffer {
378
- let pos = 0;
379
- while (pos < data.length) {
380
- const tag = data[pos]!; pos++;
381
- const field = tag >> 3;
382
- const wire = tag & 0x07;
383
- if (wire === 2) {
384
- let len = 0, shift = 0;
385
- while (true) {
386
- const b = data[pos]!; pos++;
387
- len |= (b & 0x7f) << shift;
388
- if (!(b & 0x80)) break;
389
- shift += 7;
390
- }
391
- const value = data.subarray(pos, pos + len); pos += len;
392
- if (field === targetField) return Buffer.from(value);
393
- } else if (wire === 0) {
394
- while (pos < data.length && (data[pos]! & 0x80)) pos++;
395
- pos++;
396
- } else break;
397
- }
398
- throw new Error(`Field ${targetField} not found in handshake message`);
399
- }
400
-
401
- function prependHeader(data: Buffer): Buffer {
402
- return Buffer.concat([WA_HEADER, prependLength(data)]);
403
- }
404
-
405
- function prependLength(data: Buffer): Buffer {
406
- const header = Buffer.alloc(3);
407
- header.writeUIntBE(data.length, 0, 3);
408
- return Buffer.concat([header, data]);
409
- }
410
-
411
- async function readRawFrame(ws: RawWebSocket): Promise<Buffer> {
412
- const header = await ws.readRaw(3);
413
- const len = header.readUIntBE(0, 3);
414
- const payload = await ws.readRaw(len);
415
- logger.debug("noise-handshake", `Raw Frame received: header=${header.toString('hex')} (len=${len}), payload_head=${payload.toString('hex').slice(0, 32)}...`);
416
- return payload;
417
- }
@@ -1,230 +0,0 @@
1
- import { EventEmitter } from "node:events";
2
- import { appendFileSync } from "node:fs";
3
- import { doHandshake } from "./noise-handshake.ts";
4
- import type { NoiseSocket, RawWebSocket } from "../../../models/e2ee.ts";
5
- import { encodeKeepAlive, unmarshal } from "../binary/wa-binary.ts";
6
- import { logger } from "../../../utils/logger.ts";
7
-
8
- export class FacebookE2EESocket extends EventEmitter {
9
- private ws: WebSocket | null = null;
10
- private noiseSocket: NoiseSocket | null = null;
11
- private url: string;
12
- private heartbeatInterval: any = null;
13
- private isConnected: boolean = false;
14
-
15
- constructor(endpoint: string) {
16
- super();
17
- this.url = endpoint;
18
- }
19
-
20
- public async connect(noisePrivKey: Buffer, authPayload: Buffer): Promise<void> {
21
- return new Promise((resolve, reject) => {
22
- try {
23
- const wsUrl = new URL(this.url);
24
- // Add chat specific query params if needed
25
- wsUrl.searchParams.set("cid", "client-" + Date.now());
26
-
27
- const UserAgentStr = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36";
28
-
29
- this.ws = new (WebSocket as any)(wsUrl.toString(), undefined, {
30
- headers: {
31
- "Origin": "https://www.facebook.com",
32
- "User-Agent": UserAgentStr,
33
- }
34
- });
35
- this.ws!.binaryType = "arraybuffer";
36
-
37
- let handshakeResolved = false;
38
- let streamBuffer: Buffer[] = [];
39
- let streamLen = 0;
40
- let waitingResolver: ((data: Buffer) => void) | null = null;
41
- let waitingLen: number = 0;
42
-
43
- this.ws!.addEventListener("message", (ev) => {
44
- const frame = Buffer.from(ev.data as ArrayBuffer);
45
- streamBuffer.push(frame);
46
- streamLen += frame.length;
47
- if (waitingResolver && streamLen >= waitingLen) {
48
- const res = waitingResolver;
49
- const len = waitingLen;
50
- waitingResolver = null;
51
- waitingLen = 0;
52
- res(readFromBuffer(len));
53
- }
54
- });
55
-
56
- function readFromBuffer(len: number): Buffer {
57
- let res = Buffer.alloc(len);
58
- let offset = 0;
59
- while (offset < len && streamBuffer.length > 0) {
60
- const first = streamBuffer[0];
61
- const remaining = len - offset;
62
- if (first && first.length <= remaining) {
63
- first.copy(res, offset);
64
- offset += first.length;
65
- streamBuffer.shift();
66
- } else if (first) {
67
- first.copy(res, offset, 0, remaining);
68
- streamBuffer[0] = first.subarray(remaining);
69
- offset += remaining;
70
- } else {
71
- break;
72
- }
73
- }
74
- streamLen -= len;
75
- return res;
76
- }
77
-
78
- const rawWs: RawWebSocket = {
79
- send: (data: Buffer) => {
80
- if (this.ws?.readyState === WebSocket.OPEN) {
81
- this.ws.send(data);
82
- }
83
- },
84
- readRaw: (len?: number): Promise<Buffer> => {
85
- const targetLen = len || 0;
86
- if (targetLen === 0) {
87
- if (streamLen > 0) return Promise.resolve(readFromBuffer(streamLen));
88
- return new Promise((resolve) => {
89
- waitingLen = 1;
90
- waitingResolver = () => resolve(readFromBuffer(streamLen));
91
- });
92
- }
93
- if (streamLen >= targetLen) {
94
- return Promise.resolve(readFromBuffer(targetLen));
95
- }
96
- if (this.ws?.readyState !== WebSocket.OPEN) {
97
- return Promise.reject(new Error("WebSocket not open"));
98
- }
99
- return new Promise((resolve) => {
100
- waitingLen = targetLen;
101
- waitingResolver = resolve;
102
- });
103
- },
104
- close: () => this.ws?.close(),
105
- };
106
-
107
- this.ws!.addEventListener("close", () => {
108
- this.isConnected = false;
109
- this.stopHeartbeat();
110
- if (waitingResolver) {
111
- waitingResolver(null as any);
112
- waitingResolver = null;
113
- }
114
- this.emit("disconnected");
115
- });
116
-
117
- this.ws!.addEventListener("open", async () => {
118
- try {
119
- const { socket } = await doHandshake(rawWs, noisePrivKey, authPayload);
120
- this.noiseSocket = socket;
121
- handshakeResolved = true;
122
- this.isConnected = true;
123
- this.emit("connected");
124
- resolve();
125
- this.startReading();
126
- this.startHeartbeat();
127
- } catch (err) {
128
- reject(err);
129
- this.ws?.close();
130
- }
131
- });
132
-
133
- this.ws!.addEventListener("error", (ev) => {
134
- if (!handshakeResolved) reject(new Error("WebSocket error"));
135
- this.emit("error", ev);
136
- });
137
- } catch (err) {
138
- reject(err);
139
- }
140
- });
141
- }
142
-
143
- private startHeartbeat() {
144
- this.stopHeartbeat();
145
- this.heartbeatInterval = setInterval(async () => {
146
- try {
147
- if (this.ws && this.ws.readyState === WebSocket.OPEN) {
148
- logger.debug("NoiseSocket", "Sending Noise heartbeat (0,0,0)...");
149
- this.ws.send(Buffer.from([0, 0, 0]));
150
- }
151
-
152
- } catch (err) {
153
- logger.error("FacebookE2EESocket", "Heartbeat failed:", err);
154
- }
155
- }, 15000);
156
- }
157
-
158
- private stopHeartbeat() {
159
- if (this.heartbeatInterval) {
160
- clearInterval(this.heartbeatInterval);
161
- this.heartbeatInterval = null;
162
- }
163
- }
164
-
165
- private async startReading(): Promise<void> {
166
- if (!this.noiseSocket) return;
167
- try {
168
- while (this.isConnected && this.noiseSocket) {
169
- logger.debug("FacebookE2EESocket", "Waiting for frame...");
170
- const frame = await this.noiseSocket.readFrame();
171
- if (frame && frame.length > 0) {
172
- try {
173
- logger.debug("FacebookE2EESocket", `Unmarshaling frame (${frame.length} bytes)...`);
174
- const node = unmarshal(frame);
175
- logger.debug("FacebookE2EESocket", `Decrypted node: <${node.tag}>`, JSON.stringify(node.attrs, null, 2));
176
- if (node.content) {
177
- logger.debug("FacebookE2EESocket", `Node content type: ${Array.isArray(node.content) ? "Array[" + node.content.length + "]" : typeof node.content}`);
178
- }
179
- } catch (e) {
180
- logger.error("FacebookE2EESocket", `Received decrypted frame (${frame.length} bytes), but failed to unmarshal: ${e}`);
181
- logger.debug("FacebookE2EESocket", `Frame hex: ${frame.toString("hex").slice(0, 100)}`);
182
- }
183
- this.emit("frame", frame);
184
- } else if (frame) {
185
- // Heartbeat or empty frame from server
186
- logger.debug("FacebookE2EESocket", "Received empty frame (heartbeat from server)");
187
- }
188
- }
189
- } catch (err) {
190
- if (!this.isConnected) {
191
- logger.debug("FacebookE2EESocket", "Read loop stopped after intentional socket close");
192
- return;
193
- }
194
- logger.error("FacebookE2EESocket", "Read loop stopped due to error:", err);
195
- this.isConnected = false;
196
- this.emit("error", err);
197
- }
198
- }
199
-
200
- public async sendFrame(data: Buffer): Promise<void> {
201
- if (!this.noiseSocket) throw new Error("Socket not connected");
202
- try {
203
- const node = unmarshal(data);
204
- logger.debug("NoiseSocket", `Sending encrypted node: <${node.tag}>`, JSON.stringify(node.attrs, null, 2));
205
- try {
206
- const record = { ts: Date.now(), tag: node.tag, attrs: node.attrs, hex: data.toString("hex") };
207
- appendFileSync("./outbound_frames.log", JSON.stringify(record) + "\n");
208
- } catch (e) {
209
- logger.debug("NoiseSocket", "Failed to write outbound frame log:", e);
210
- }
211
- } catch (e) {
212
- logger.debug("NoiseSocket", `Sending raw encrypted frame (${data.length} bytes)`);
213
- try {
214
- const record = { ts: Date.now(), tag: null, attrs: null, hex: data.toString("hex") };
215
- appendFileSync("./outbound_frames.log", JSON.stringify(record) + "\n");
216
- } catch (e) {
217
- logger.debug("NoiseSocket", "Failed to write outbound frame log:", e);
218
- }
219
- }
220
- await this.noiseSocket.sendFrame(data);
221
- }
222
-
223
-
224
- public close(): void {
225
- this.isConnected = false;
226
- this.stopHeartbeat();
227
- this.noiseSocket?.close();
228
- this.ws?.close();
229
- }
230
- }
package/src/index.ts DELETED
@@ -1,34 +0,0 @@
1
- export { FBClient } from "./core/client.ts";
2
- export { E2EEService } from "./services/e2ee.service.ts";
3
-
4
- export type { ClientOptions, SessionData, MessengerEventMap } from "./models/client.ts";
5
- export type { AuthConfig, AppEnv } from "./models/config.ts";
6
- export type {
7
- Attachment,
8
- Mention,
9
- ReplyTo,
10
- Platform,
11
- MessengerEvent,
12
- E2EEMessage,
13
- E2EEMessageKind,
14
- } from "./models/domain.ts";
15
- export type {
16
- SendMessageInput,
17
- SendMediaInput,
18
- SendReactionInput,
19
- TypingInput,
20
- } from "./models/messaging.ts";
21
- export type {
22
- E2EESendTextOptions,
23
- E2EESendTextResult,
24
- E2EEEncryptMediaOptions,
25
- E2EEEncryptMediaResult,
26
- E2EEDecryptMediaOptions,
27
- E2EEDownloadOptions,
28
- E2EEDownloadResult,
29
- } from "./models/e2ee.ts";
30
- export type {
31
- MediaUploadConfig,
32
- MediaUploadResult,
33
- MmsTypeStr,
34
- } from "./models/media.ts";