fb-messenger-e2ee 0.1.1 → 0.1.3
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/DOCS.md +41 -3
- package/README.md +27 -2
- package/dist/index.cjs +9017 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +910 -0
- package/dist/index.d.ts +910 -0
- package/dist/index.js +9049 -0
- package/dist/index.js.map +1 -0
- package/package.json +19 -4
- package/bun.lock +0 -1014
- package/examples/basic-usage.ts +0 -52
- package/jest.config.ts +0 -18
- package/src/config/env.ts +0 -15
- package/src/controllers/client.controller.ts +0 -805
- package/src/controllers/dgw-handler.ts +0 -171
- package/src/controllers/e2ee-handler.ts +0 -832
- package/src/controllers/event-mapper.ts +0 -327
- package/src/core/client.ts +0 -151
- package/src/e2ee/application/e2ee-client.ts +0 -376
- package/src/e2ee/application/fanout-planner.ts +0 -79
- package/src/e2ee/application/outbound-message-cache.ts +0 -58
- package/src/e2ee/application/prekey-maintenance.ts +0 -55
- package/src/e2ee/application/retry-manager.ts +0 -156
- package/src/e2ee/facebook/facebook-protocol-utils.ts +0 -230
- package/src/e2ee/facebook/icdc-payload.ts +0 -31
- package/src/e2ee/index.ts +0 -76
- package/src/e2ee/internal.ts +0 -27
- package/src/e2ee/media/media-crypto.ts +0 -147
- package/src/e2ee/media/media-upload.ts +0 -90
- package/src/e2ee/message/builders/client-payload.ts +0 -54
- package/src/e2ee/message/builders/consumer-application.ts +0 -362
- package/src/e2ee/message/builders/message-application.ts +0 -64
- package/src/e2ee/message/builders/message-transport.ts +0 -84
- package/src/e2ee/message/codecs/protobuf-codecs.ts +0 -101
- package/src/e2ee/message/constants.ts +0 -4
- package/src/e2ee/message/message-builder.ts +0 -15
- package/src/e2ee/signal/prekey-manager.ts +0 -140
- package/src/e2ee/signal/signal-manager.ts +0 -365
- package/src/e2ee/store/device-json.ts +0 -47
- package/src/e2ee/store/device-repository.ts +0 -12
- package/src/e2ee/store/device-store.ts +0 -538
- package/src/e2ee/transport/binary/decoder.ts +0 -180
- package/src/e2ee/transport/binary/encoder.ts +0 -143
- package/src/e2ee/transport/binary/stanzas.ts +0 -97
- package/src/e2ee/transport/binary/tokens.ts +0 -64
- package/src/e2ee/transport/binary/wa-binary.ts +0 -8
- package/src/e2ee/transport/dgw/dgw-socket.ts +0 -345
- package/src/e2ee/transport/noise/noise-handshake.ts +0 -417
- package/src/e2ee/transport/noise/noise-socket.ts +0 -230
- package/src/index.ts +0 -34
- package/src/models/client.ts +0 -26
- package/src/models/config.ts +0 -14
- package/src/models/domain.ts +0 -299
- package/src/models/e2ee.ts +0 -295
- package/src/models/media.ts +0 -41
- package/src/models/messaging.ts +0 -88
- package/src/models/thread.ts +0 -69
- package/src/repositories/session.repository.ts +0 -20
- package/src/services/auth.service.ts +0 -55
- package/src/services/e2ee.service.ts +0 -174
- package/src/services/facebook-gateway.service.ts +0 -245
- package/src/services/icdc.service.ts +0 -177
- package/src/services/media.service.ts +0 -304
- package/src/services/messaging.service.ts +0 -28
- package/src/services/thread.service.ts +0 -199
- package/src/types/advanced-types.ts +0 -61
- package/src/types/fca-unofficial.d.ts +0 -212
- package/src/types/protocol-types.ts +0 -43
- package/src/utils/fca-utils.ts +0 -30
- package/src/utils/logger.ts +0 -24
- package/src/utils/mime.ts +0 -51
- package/tests/.env.example +0 -87
- package/tests/data/1x1.png +0 -0
- package/tests/data/example.txt +0 -1
- package/tests/data/file_example.mp3 +0 -0
- package/tests/data/file_example.mp4 +0 -0
- package/tests/integration.test.ts +0 -498
- package/tests/script/echo-e2ee.ts +0 -174
- package/tests/script/send-e2ee-media.ts +0 -227
- package/tests/script/send-e2ee-reaction.ts +0 -108
- package/tests/script/send-e2ee-unsend.ts +0 -115
- package/tests/script/send-typing.ts +0 -107
- package/tests/script/test-group-send.ts +0 -105
- package/tests/setup.ts +0 -3
- package/tests/types.test.ts +0 -57
- package/tests/unit/controllers/dgw-handler.test.ts +0 -60
- package/tests/unit/controllers/e2ee-handler.test.ts +0 -293
- package/tests/unit/controllers/event-mapper.test.ts +0 -252
- package/tests/unit/e2ee/application-helpers.test.ts +0 -418
- package/tests/unit/e2ee/device-store.test.ts +0 -126
- package/tests/unit/e2ee/facebook-protocol-utils.test.ts +0 -134
- package/tests/unit/e2ee/media-crypto.test.ts +0 -55
- package/tests/unit/e2ee/media-upload.test.ts +0 -60
- package/tests/unit/e2ee/message-builder.test.ts +0 -42
- package/tests/unit/e2ee/message-builders.test.ts +0 -230
- package/tests/unit/e2ee/noise-handshake.test.ts +0 -260
- package/tests/unit/e2ee/prekey-manager.test.ts +0 -55
- package/tests/unit/e2ee/wa-binary.test.ts +0 -127
- package/tests/unit/services/e2ee.service.test.ts +0 -101
- package/tests/unit/services/facebook-gateway.service.test.ts +0 -138
- package/tests/unit/services/media.service.test.ts +0 -169
- package/tests/unit/utils/fca-utils.test.ts +0 -48
- package/tsconfig.json +0 -19
- /package/{src/e2ee/message/proto → proto}/ArmadilloApplication.proto +0 -0
- /package/{src/e2ee/message/proto → proto}/ArmadilloICDC.proto +0 -0
- /package/{src/e2ee/message/proto → proto}/ConsumerApplication.proto +0 -0
- /package/{src/e2ee/message/proto → proto}/MessageApplication.proto +0 -0
- /package/{src/e2ee/message/proto → proto}/MessageTransport.proto +0 -0
- /package/{src/e2ee/message/proto → proto}/WACommon.proto +0 -0
- /package/{src/e2ee/message/proto → proto}/WAMediaTransport.proto +0 -0
- /package/{src/e2ee/message/proto → proto}/proto-writer.ts +0 -0
|
@@ -1,376 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* E2EE Client - Layer orchestrator
|
|
3
|
-
*
|
|
4
|
-
* Ties together all E2EE layers:
|
|
5
|
-
* - DeviceStore (key persistence)
|
|
6
|
-
* - Signal Manager (DM + Group encryption)
|
|
7
|
-
* - Message Builder (protobuf)
|
|
8
|
-
* - Media Crypto (AES-CBC + HMAC + HKDF)
|
|
9
|
-
* - Media Upload (HTTP)
|
|
10
|
-
* - Noise Handshake (transport) - used by connection layer
|
|
11
|
-
*
|
|
12
|
-
* This replaces the stub E2EEService for actual E2EE message handling.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import type { DeviceStore } from "../store/device-store.ts";
|
|
16
|
-
import type { MediaTypeKey } from "../media/media-crypto.ts";
|
|
17
|
-
import { encryptMedia, decryptMedia } from "../media/media-crypto.ts";
|
|
18
|
-
import {
|
|
19
|
-
encryptDM,
|
|
20
|
-
decryptDM,
|
|
21
|
-
decryptDMPreKey,
|
|
22
|
-
encryptGroup,
|
|
23
|
-
decryptGroup,
|
|
24
|
-
createSenderKeyDistributionMessage,
|
|
25
|
-
processSKDM,
|
|
26
|
-
establishSession,
|
|
27
|
-
jidToAddress,
|
|
28
|
-
hasSession,
|
|
29
|
-
} from "../signal/signal-manager.ts";
|
|
30
|
-
import type {
|
|
31
|
-
E2EEDecryptMediaOptions,
|
|
32
|
-
E2EEEncryptMediaResult,
|
|
33
|
-
E2EESendTextOptions,
|
|
34
|
-
EncryptionResult,
|
|
35
|
-
} from "../../models/e2ee.ts";
|
|
36
|
-
export type {
|
|
37
|
-
E2EEDecryptMediaOptions,
|
|
38
|
-
E2EEEncryptMediaOptions,
|
|
39
|
-
E2EEEncryptMediaResult,
|
|
40
|
-
E2EESendTextOptions,
|
|
41
|
-
E2EESendTextResult,
|
|
42
|
-
EncryptionResult,
|
|
43
|
-
MediaFields,
|
|
44
|
-
} from "../../models/e2ee.ts";
|
|
45
|
-
import {
|
|
46
|
-
encodeMessageApplication,
|
|
47
|
-
encodeMessageTransport,
|
|
48
|
-
MessageBuilder,
|
|
49
|
-
encodeTextMessage,
|
|
50
|
-
encodeImageMessage,
|
|
51
|
-
encodeVideoMessage,
|
|
52
|
-
encodeAudioMessage,
|
|
53
|
-
encodeDocumentMessage,
|
|
54
|
-
encodeStickerMessage,
|
|
55
|
-
encodeReactionMessage,
|
|
56
|
-
encodeEditMessage,
|
|
57
|
-
encodeRevokeMessage,
|
|
58
|
-
} from "../message/message-builder.ts";
|
|
59
|
-
import type { RawPreKeyBundle } from "../../models/e2ee.ts";
|
|
60
|
-
import type { MediaUploadConfig, MmsTypeStr } from "../../models/media.ts";
|
|
61
|
-
import { uploadMedia } from "../media/media-upload.ts";
|
|
62
|
-
import { MmsType } from "../media/media-crypto.ts";
|
|
63
|
-
|
|
64
|
-
// Types
|
|
65
|
-
|
|
66
|
-
export interface DMTextFanoutPayloads {
|
|
67
|
-
type: "dm";
|
|
68
|
-
messageApp: Buffer;
|
|
69
|
-
devicePayload: Buffer;
|
|
70
|
-
selfDevicePayload: Buffer;
|
|
71
|
-
frankingTag: Buffer;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// E2EEClient
|
|
75
|
-
|
|
76
|
-
export class E2EEClient {
|
|
77
|
-
private store: DeviceStore;
|
|
78
|
-
|
|
79
|
-
constructor(store: DeviceStore) {
|
|
80
|
-
this.store = store;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// Session management
|
|
84
|
-
|
|
85
|
-
/** Establish a session with a contact using their prekey bundle (X3DH). */
|
|
86
|
-
async establishSession(recipientJid: string, bundle: RawPreKeyBundle): Promise<void> {
|
|
87
|
-
const addr = jidToAddress(recipientJid);
|
|
88
|
-
await establishSession(this.store, addr, bundle);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
async processSenderKeyDistribution(
|
|
92
|
-
senderJid: string,
|
|
93
|
-
skdmBytes: Buffer,
|
|
94
|
-
groupJid?: string,
|
|
95
|
-
): Promise<void> {
|
|
96
|
-
await processSKDM(this.store, senderJid, skdmBytes, groupJid);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// Message encrypt (DM)
|
|
100
|
-
|
|
101
|
-
/** Build DM text transports for V3 participant fanout. */
|
|
102
|
-
async buildDMTextFanoutPayloads(opts: E2EESendTextOptions): Promise<DMTextFanoutPayloads> {
|
|
103
|
-
const builder = new MessageBuilder().setText(opts.text);
|
|
104
|
-
if (opts.replyToId && opts.replyToSenderJid) {
|
|
105
|
-
builder.setReply(opts.replyToId, opts.replyToSenderJid);
|
|
106
|
-
}
|
|
107
|
-
const consumerApp = builder.build();
|
|
108
|
-
const { messageApp, frankingTag } = encodeMessageApplication(consumerApp, builder.getReply());
|
|
109
|
-
|
|
110
|
-
return {
|
|
111
|
-
type: "dm",
|
|
112
|
-
messageApp,
|
|
113
|
-
devicePayload: encodeMessageTransport({ messageApp }),
|
|
114
|
-
selfDevicePayload: encodeMessageTransport({
|
|
115
|
-
messageApp,
|
|
116
|
-
dsm: { destinationJid: opts.toJid, phash: "" },
|
|
117
|
-
}),
|
|
118
|
-
frankingTag,
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Build and encrypt a DM text message for Signal transport.
|
|
124
|
-
* Kept for low-level callers; production send should fan out through participants.
|
|
125
|
-
*/
|
|
126
|
-
async encryptDMText(opts: E2EESendTextOptions): Promise<Extract<EncryptionResult, { type: "dm" }>> {
|
|
127
|
-
const fanout = await this.buildDMTextFanoutPayloads(opts);
|
|
128
|
-
const recipientAddr = jidToAddress(opts.toJid);
|
|
129
|
-
const selfAddr = jidToAddress(opts.selfJid);
|
|
130
|
-
const encrypted = await encryptDM(this.store, recipientAddr, selfAddr, fanout.devicePayload);
|
|
131
|
-
|
|
132
|
-
return {
|
|
133
|
-
type: "dm",
|
|
134
|
-
encrypted: { type: encrypted.type, ciphertext: Buffer.from(encrypted.ciphertext) },
|
|
135
|
-
frankingTag: fanout.frankingTag,
|
|
136
|
-
messageApp: fanout.messageApp,
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/** Build and encrypt a group text message. */
|
|
141
|
-
async encryptGroupText(
|
|
142
|
-
groupJid: string,
|
|
143
|
-
selfJid: string,
|
|
144
|
-
text: string,
|
|
145
|
-
messageId: string,
|
|
146
|
-
replyToId?: string,
|
|
147
|
-
replyToSenderJid?: string
|
|
148
|
-
): Promise<Extract<EncryptionResult, { type: "group" }>> {
|
|
149
|
-
const builder = new MessageBuilder().setText(text);
|
|
150
|
-
if (replyToId && replyToSenderJid) {
|
|
151
|
-
builder.setReply(replyToId, replyToSenderJid);
|
|
152
|
-
}
|
|
153
|
-
const consumerApp = builder.build();
|
|
154
|
-
const { messageApp, frankingTag } = encodeMessageApplication(consumerApp, builder.getReply());
|
|
155
|
-
|
|
156
|
-
const { skdm, distributionId } = await createSenderKeyDistributionMessage(this.store, groupJid, selfJid);
|
|
157
|
-
|
|
158
|
-
const groupTransport = encodeMessageTransport({
|
|
159
|
-
messageApp,
|
|
160
|
-
backupDirective: { messageId, actionType: "UPSERT" },
|
|
161
|
-
});
|
|
162
|
-
// Per-device SKDM transport should not include application payload
|
|
163
|
-
const deviceTransport = encodeMessageTransport({
|
|
164
|
-
skdm: { groupId: groupJid, skdmBytes: Buffer.from(skdm.serialize()) },
|
|
165
|
-
});
|
|
166
|
-
// DSM is included only for sender's other devices (same user, different device)
|
|
167
|
-
const selfDeviceTransport = encodeMessageTransport({
|
|
168
|
-
skdm: { groupId: groupJid, skdmBytes: Buffer.from(skdm.serialize()) },
|
|
169
|
-
dsm: { destinationJid: groupJid, phash: "" },
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
const groupCiphertext = await encryptGroup(this.store, groupJid, selfJid, groupTransport, distributionId);
|
|
173
|
-
|
|
174
|
-
return {
|
|
175
|
-
type: "group",
|
|
176
|
-
messageApp,
|
|
177
|
-
groupCiphertext: Buffer.from(groupCiphertext),
|
|
178
|
-
devicePayload: Buffer.from(deviceTransport),
|
|
179
|
-
selfDevicePayload: Buffer.from(selfDeviceTransport),
|
|
180
|
-
skdmPayload: Buffer.from(groupTransport),
|
|
181
|
-
skdm: {
|
|
182
|
-
groupId: groupJid,
|
|
183
|
-
skdmBytes: Buffer.from(skdm.serialize()),
|
|
184
|
-
distributionId,
|
|
185
|
-
},
|
|
186
|
-
frankingTag,
|
|
187
|
-
};
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/** Build and encrypt a pre-built MessageApplication for a group send. */
|
|
191
|
-
async encryptGroupMessageApplication(
|
|
192
|
-
groupJid: string,
|
|
193
|
-
selfJid: string,
|
|
194
|
-
messageApp: Buffer,
|
|
195
|
-
messageId: string,
|
|
196
|
-
): Promise<Omit<Extract<EncryptionResult, { type: "group" }>, "frankingTag">> {
|
|
197
|
-
const { skdm, distributionId } = await createSenderKeyDistributionMessage(this.store, groupJid, selfJid);
|
|
198
|
-
|
|
199
|
-
const groupTransport = encodeMessageTransport({
|
|
200
|
-
messageApp,
|
|
201
|
-
backupDirective: { messageId, actionType: "UPSERT" },
|
|
202
|
-
});
|
|
203
|
-
const deviceTransport = encodeMessageTransport({
|
|
204
|
-
skdm: { groupId: groupJid, skdmBytes: Buffer.from(skdm.serialize()) },
|
|
205
|
-
});
|
|
206
|
-
const selfDeviceTransport = encodeMessageTransport({
|
|
207
|
-
skdm: { groupId: groupJid, skdmBytes: Buffer.from(skdm.serialize()) },
|
|
208
|
-
dsm: { destinationJid: groupJid, phash: "" },
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
const groupCiphertext = await encryptGroup(this.store, groupJid, selfJid, groupTransport, distributionId);
|
|
212
|
-
|
|
213
|
-
return {
|
|
214
|
-
type: "group",
|
|
215
|
-
messageApp,
|
|
216
|
-
groupCiphertext: Buffer.from(groupCiphertext),
|
|
217
|
-
devicePayload: Buffer.from(deviceTransport),
|
|
218
|
-
selfDevicePayload: Buffer.from(selfDeviceTransport),
|
|
219
|
-
skdmPayload: Buffer.from(groupTransport),
|
|
220
|
-
skdm: {
|
|
221
|
-
groupId: groupJid,
|
|
222
|
-
skdmBytes: Buffer.from(skdm.serialize()),
|
|
223
|
-
distributionId,
|
|
224
|
-
},
|
|
225
|
-
};
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
/** Create a sender-key distribution payload for targeted group retry responses. */
|
|
229
|
-
async createSenderKeyDistributionPayload(groupJid: string, selfJid: string): Promise<{ groupId: string; skdmBytes: Buffer; distributionId: string }> {
|
|
230
|
-
const { skdm, distributionId } = await createSenderKeyDistributionMessage(this.store, groupJid, selfJid);
|
|
231
|
-
return { groupId: groupJid, skdmBytes: Buffer.from(skdm.serialize()), distributionId };
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
/** Encrypt a MessageApplication payload directly to one device (used for retry responses). */
|
|
235
|
-
async encryptMessageAppForDevice(
|
|
236
|
-
recipientJid: string,
|
|
237
|
-
selfJid: string,
|
|
238
|
-
messageApp: Buffer,
|
|
239
|
-
opts: {
|
|
240
|
-
skdm?: { groupId: string; skdmBytes: Buffer };
|
|
241
|
-
dsm?: { destinationJid: string; phash: string };
|
|
242
|
-
backupDirective?: { messageId: string; actionType: "UPSERT" | "REMOVE" };
|
|
243
|
-
} = {},
|
|
244
|
-
): Promise<{ type: "msg" | "pkmsg"; ciphertext: Buffer }> {
|
|
245
|
-
const transport = encodeMessageTransport({
|
|
246
|
-
messageApp,
|
|
247
|
-
skdm: opts.skdm,
|
|
248
|
-
dsm: opts.dsm,
|
|
249
|
-
backupDirective: opts.backupDirective,
|
|
250
|
-
});
|
|
251
|
-
return this.encryptDevicePayload(recipientJid, selfJid, transport);
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
/** Check if a session exists for a given device JID. */
|
|
255
|
-
async hasSession(jid: string): Promise<boolean> {
|
|
256
|
-
const addr = jidToAddress(jid);
|
|
257
|
-
return hasSession(this.store, addr);
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
/** Encrypt an SKDM for a specific device DM. */
|
|
261
|
-
async encryptSKDM(recipientJid: string, selfJid: string, skdm: { groupId: string; skdmBytes: Buffer }): Promise<{ type: "msg" | "pkmsg"; ciphertext: Buffer }> {
|
|
262
|
-
const transport = encodeMessageTransport({
|
|
263
|
-
skdm, // No application payload for SKDM-only DM
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
const recipientAddr = jidToAddress(recipientJid);
|
|
267
|
-
const selfAddr = jidToAddress(selfJid);
|
|
268
|
-
const encrypted = await encryptDM(this.store, recipientAddr, selfAddr, transport);
|
|
269
|
-
|
|
270
|
-
return {
|
|
271
|
-
type: encrypted.type,
|
|
272
|
-
ciphertext: Buffer.from(encrypted.ciphertext),
|
|
273
|
-
};
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
async encryptDevicePayload(recipientJid: string, selfJid: string, payload: Buffer): Promise<{ type: "msg" | "pkmsg"; ciphertext: Buffer }> {
|
|
277
|
-
const recipientAddr = jidToAddress(recipientJid);
|
|
278
|
-
const selfAddr = jidToAddress(selfJid);
|
|
279
|
-
const encrypted = await encryptDM(this.store, recipientAddr, selfAddr, payload);
|
|
280
|
-
|
|
281
|
-
return {
|
|
282
|
-
type: encrypted.type,
|
|
283
|
-
ciphertext: Buffer.from(encrypted.ciphertext),
|
|
284
|
-
};
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
// Message decrypt
|
|
288
|
-
|
|
289
|
-
/** Decrypt a DM Signal message (type = "msg"). Returns raw MessageTransport bytes. */
|
|
290
|
-
async decryptDMMessage(senderJid: string, ciphertext: Buffer): Promise<Buffer> {
|
|
291
|
-
const addr = jidToAddress(senderJid);
|
|
292
|
-
return decryptDM(this.store, addr, ciphertext);
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
/** Decrypt a DM PreKeySignalMessage (first message from sender). */
|
|
296
|
-
async decryptDMPreKeyMessage(senderJid: string, selfJid: string, ciphertext: Buffer): Promise<Buffer> {
|
|
297
|
-
const senderAddr = jidToAddress(senderJid);
|
|
298
|
-
const selfAddr = jidToAddress(selfJid);
|
|
299
|
-
return decryptDMPreKey(this.store, senderAddr, selfAddr, ciphertext);
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
async decryptGroupMessage(
|
|
303
|
-
senderJid: string,
|
|
304
|
-
ciphertext: Buffer,
|
|
305
|
-
groupJid?: string,
|
|
306
|
-
): Promise<Buffer> {
|
|
307
|
-
return decryptGroup(this.store, senderJid, ciphertext, groupJid);
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
// Media
|
|
311
|
-
|
|
312
|
-
/** Encrypt media bytes for upload. Returns crypto fields + uploadable buffer. */
|
|
313
|
-
encryptMedia(data: Buffer, type: MediaTypeKey) {
|
|
314
|
-
return encryptMedia(data, type);
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
/** Decrypt downloaded E2EE media. */
|
|
318
|
-
decryptMedia(opts: E2EEDecryptMediaOptions): Buffer {
|
|
319
|
-
return decryptMedia(opts);
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
/**
|
|
323
|
-
* Encrypt + upload media in one step.
|
|
324
|
-
* Returns all fields needed to build a ConsumerApplication media message.
|
|
325
|
-
* @param refreshConfig Optional callback to refresh upload config on 401.
|
|
326
|
-
*/
|
|
327
|
-
async encryptAndUploadMedia(
|
|
328
|
-
uploadConfig: MediaUploadConfig,
|
|
329
|
-
data: Buffer,
|
|
330
|
-
type: MediaTypeKey,
|
|
331
|
-
mimeType: string,
|
|
332
|
-
refreshConfig?: () => Promise<MediaUploadConfig>,
|
|
333
|
-
): Promise<E2EEEncryptMediaResult> {
|
|
334
|
-
const mmsTypeStr = MmsType[type] as MmsTypeStr;
|
|
335
|
-
const encrypted = encryptMedia(data, type);
|
|
336
|
-
const uploaded = await uploadMedia(uploadConfig, encrypted.dataToUpload, encrypted.fileEncSHA256, mmsTypeStr, {
|
|
337
|
-
refreshConfig,
|
|
338
|
-
});
|
|
339
|
-
|
|
340
|
-
const mediaKeyTimestamp = Math.floor(Date.now() / 1000);
|
|
341
|
-
|
|
342
|
-
return {
|
|
343
|
-
mediaKey: encrypted.mediaKey,
|
|
344
|
-
fileSHA256: encrypted.fileSHA256,
|
|
345
|
-
fileEncSHA256: encrypted.fileEncSHA256,
|
|
346
|
-
fileLength: encrypted.fileLength,
|
|
347
|
-
directPath: uploaded.directPath,
|
|
348
|
-
handle: uploaded.handle,
|
|
349
|
-
objectId: uploaded.objectId,
|
|
350
|
-
mediaFields: {
|
|
351
|
-
mimeType,
|
|
352
|
-
fileSHA256: encrypted.fileSHA256,
|
|
353
|
-
fileLength: encrypted.fileLength,
|
|
354
|
-
mediaKey: encrypted.mediaKey,
|
|
355
|
-
fileEncSHA256: encrypted.fileEncSHA256,
|
|
356
|
-
directPath: uploaded.directPath,
|
|
357
|
-
objectId: uploaded.objectId,
|
|
358
|
-
mediaKeyTimestamp,
|
|
359
|
-
},
|
|
360
|
-
};
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
// Message builder helpers (passthrough)
|
|
364
|
-
|
|
365
|
-
buildTextMessage = encodeTextMessage;
|
|
366
|
-
buildImageMessage = encodeImageMessage;
|
|
367
|
-
buildVideoMessage = encodeVideoMessage;
|
|
368
|
-
buildAudioMessage = encodeAudioMessage;
|
|
369
|
-
buildDocumentMessage = encodeDocumentMessage;
|
|
370
|
-
buildStickerMessage = encodeStickerMessage;
|
|
371
|
-
buildReactionMessage = encodeReactionMessage;
|
|
372
|
-
buildEditMessage = encodeEditMessage;
|
|
373
|
-
buildRevokeMessage = encodeRevokeMessage;
|
|
374
|
-
buildMessageApplication = encodeMessageApplication;
|
|
375
|
-
buildMessageTransport = encodeMessageTransport;
|
|
376
|
-
}
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
|
|
3
|
-
export interface ParsedMessengerJid {
|
|
4
|
-
user: string;
|
|
5
|
-
device: number;
|
|
6
|
-
server: string;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export function parseMessengerJid(jid: string): ParsedMessengerJid {
|
|
10
|
-
const [userPart = jid, server = ""] = jid.split("@");
|
|
11
|
-
const colonIdx = userPart.indexOf(":");
|
|
12
|
-
const dotIdx = userPart.indexOf(".");
|
|
13
|
-
const userEnd = dotIdx !== -1 ? dotIdx : (colonIdx !== -1 ? colonIdx : userPart.length);
|
|
14
|
-
const user = userPart.slice(0, userEnd) || userPart;
|
|
15
|
-
const rawDevice = colonIdx !== -1
|
|
16
|
-
? userPart.slice(colonIdx + 1)
|
|
17
|
-
: (dotIdx !== -1 ? userPart.slice(dotIdx + 1) : "0");
|
|
18
|
-
return { user, device: Number(rawDevice) || 0, server };
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function toBareMessengerJid(jid: string): string {
|
|
22
|
-
const parsed = parseMessengerJid(jid);
|
|
23
|
-
return parsed.server === "msgr" ? `${parsed.user}.0@msgr` : jid;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function normalizeDMThreadToJid(threadId: string): string {
|
|
27
|
-
const jid = threadId.includes("@")
|
|
28
|
-
? threadId
|
|
29
|
-
: (threadId.includes(".") || threadId.includes(":") ? `${threadId}@msgr` : `${threadId}.0@msgr`);
|
|
30
|
-
return toBareMessengerJid(jid);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export function sameMessengerUser(a: string, b: string): boolean {
|
|
34
|
-
const pa = parseMessengerJid(a);
|
|
35
|
-
const pb = parseMessengerJid(b);
|
|
36
|
-
return pa.server === "msgr" && pb.server === "msgr" && pa.user === pb.user;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function sameMessengerDevice(a: string, b: string): boolean {
|
|
40
|
-
const pa = parseMessengerJid(a);
|
|
41
|
-
const pb = parseMessengerJid(b);
|
|
42
|
-
return pa.server === "msgr" && pb.server === "msgr" && pa.user === pb.user && pa.device === pb.device;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export function uniqueJids(jids: string[]): string[] {
|
|
46
|
-
const seen = new Set<string>();
|
|
47
|
-
const out: string[] = [];
|
|
48
|
-
for (const jid of jids) {
|
|
49
|
-
if (!jid) continue;
|
|
50
|
-
const parsed = parseMessengerJid(jid);
|
|
51
|
-
const key = parsed.server === "msgr" ? `${parsed.user}:${parsed.device}@${parsed.server}` : jid;
|
|
52
|
-
if (seen.has(key)) continue;
|
|
53
|
-
seen.add(key);
|
|
54
|
-
out.push(jid);
|
|
55
|
-
}
|
|
56
|
-
return out;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export function toADString(jid: string): string {
|
|
60
|
-
const [userPart = "", server = ""] = jid.split("@");
|
|
61
|
-
if (server === "msgr") {
|
|
62
|
-
const parsed = parseMessengerJid(jid);
|
|
63
|
-
if (!parsed.user) return jid;
|
|
64
|
-
return `${parsed.user}.0:${parsed.device}@${server}`;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const [userAndAgent = "", devicePart = ""] = userPart.split(":");
|
|
68
|
-
const [user = "", rawAgentPart = ""] = userAndAgent.split(".");
|
|
69
|
-
const rawAgent = rawAgentPart ? Number(rawAgentPart) : 0;
|
|
70
|
-
const device = devicePart ? Number(devicePart) : 0;
|
|
71
|
-
if (!user) return jid;
|
|
72
|
-
return `${user}.${rawAgent}:${device}@${server}`;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export function buildParticipantListHash(participants: string[]): string {
|
|
76
|
-
const sorted = [...participants].map((jid) => toADString(jid)).sort();
|
|
77
|
-
const hash = createHash("sha256").update(sorted.join("")).digest();
|
|
78
|
-
return `2:${hash.subarray(0, 6).toString("base64").replace(/=+$/, "")}`;
|
|
79
|
-
}
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import { now } from "../../utils/fca-utils.ts";
|
|
2
|
-
|
|
3
|
-
export interface RecentE2EEOutgoing {
|
|
4
|
-
kind: "dm" | "group";
|
|
5
|
-
chatJid: string;
|
|
6
|
-
messageId: string;
|
|
7
|
-
messageType: string;
|
|
8
|
-
messageApp: Buffer;
|
|
9
|
-
frankingTag: Buffer;
|
|
10
|
-
createdAtMs: number;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export interface OutboundMessageCacheOptions {
|
|
14
|
-
ttlMs?: number;
|
|
15
|
-
maxEntries?: number;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Short-lived cache of encrypted outbound message material used to answer
|
|
20
|
-
* Messenger retry receipts without re-registering the device.
|
|
21
|
-
*/
|
|
22
|
-
export class OutboundMessageCache {
|
|
23
|
-
private readonly records = new Map<string, RecentE2EEOutgoing>();
|
|
24
|
-
private readonly ttlMs: number;
|
|
25
|
-
private readonly maxEntries: number;
|
|
26
|
-
|
|
27
|
-
constructor(opts: OutboundMessageCacheOptions = {}) {
|
|
28
|
-
this.ttlMs = opts.ttlMs ?? 15 * 60 * 1000;
|
|
29
|
-
this.maxEntries = opts.maxEntries ?? 200;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
remember(record: RecentE2EEOutgoing): void {
|
|
33
|
-
this.prune();
|
|
34
|
-
this.records.set(record.messageId, record);
|
|
35
|
-
|
|
36
|
-
while (this.records.size > this.maxEntries) {
|
|
37
|
-
const oldest = this.records.keys().next().value;
|
|
38
|
-
if (!oldest) break;
|
|
39
|
-
this.records.delete(oldest);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
get(messageId: string): RecentE2EEOutgoing | undefined {
|
|
44
|
-
this.prune();
|
|
45
|
-
return this.records.get(messageId);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
prune(nowMs: number = now()): void {
|
|
49
|
-
const cutoff = nowMs - this.ttlMs;
|
|
50
|
-
for (const [messageId, record] of this.records) {
|
|
51
|
-
if (record.createdAtMs < cutoff) this.records.delete(messageId);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
clear(): void {
|
|
56
|
-
this.records.clear();
|
|
57
|
-
}
|
|
58
|
-
}
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import { now } from "../../utils/fca-utils.ts";
|
|
2
|
-
import { logger } from "../../utils/logger.ts";
|
|
3
|
-
import { MIN_PREKEY_COUNT, WANTED_PREKEY_COUNT } from "../signal/prekey-manager.ts";
|
|
4
|
-
|
|
5
|
-
export interface PreKeyMaintenanceOptions {
|
|
6
|
-
getSocket: () => unknown | null;
|
|
7
|
-
getStore: () => unknown | null;
|
|
8
|
-
getServerPreKeyCount: () => Promise<number>;
|
|
9
|
-
uploadPreKeys: (count: number) => Promise<void>;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/** Periodically tops up one-time prekeys without rotating the registered device. */
|
|
13
|
-
export class PreKeyMaintenance {
|
|
14
|
-
private interval?: ReturnType<typeof setInterval>;
|
|
15
|
-
|
|
16
|
-
constructor(private readonly opts: PreKeyMaintenanceOptions) {}
|
|
17
|
-
|
|
18
|
-
start(): void {
|
|
19
|
-
this.stop();
|
|
20
|
-
const intervalMs = Number(process.env.FB_E2EE_PREKEY_SYNC_INTERVAL_MS ?? "1800000");
|
|
21
|
-
if (!Number.isFinite(intervalMs) || intervalMs <= 0) return;
|
|
22
|
-
|
|
23
|
-
this.interval = setInterval(() => {
|
|
24
|
-
void this.sync("periodic").catch((err) => {
|
|
25
|
-
logger.error("PreKeyMaintenance", "Periodic prekey sync failed:", err);
|
|
26
|
-
});
|
|
27
|
-
}, intervalMs);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
stop(): void {
|
|
31
|
-
if (this.interval) {
|
|
32
|
-
clearInterval(this.interval);
|
|
33
|
-
this.interval = undefined;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
async sync(reason: string): Promise<void> {
|
|
38
|
-
if (!this.opts.getSocket() || !this.opts.getStore()) return;
|
|
39
|
-
|
|
40
|
-
const minCount = Number(process.env.FB_E2EE_PREKEY_MIN_COUNT ?? String(MIN_PREKEY_COUNT));
|
|
41
|
-
const uploadCount = Number(process.env.FB_E2EE_PREKEY_UPLOAD_COUNT ?? String(WANTED_PREKEY_COUNT));
|
|
42
|
-
|
|
43
|
-
try {
|
|
44
|
-
const serverCount = await this.opts.getServerPreKeyCount();
|
|
45
|
-
logger.info("PreKeyMaintenance", `E2EE prekey sync (${reason}): server has ${serverCount} prekeys`);
|
|
46
|
-
|
|
47
|
-
if (serverCount < minCount) {
|
|
48
|
-
await this.opts.uploadPreKeys(uploadCount);
|
|
49
|
-
logger.info("PreKeyMaintenance", `Uploaded ${uploadCount} E2EE prekeys without changing registered device`);
|
|
50
|
-
}
|
|
51
|
-
} catch (err) {
|
|
52
|
-
logger.error("PreKeyMaintenance", `Prekey sync failed (${reason}) at ${now()}:`, err);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
}
|