fb-messenger-e2ee 0.1.1 → 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.
- package/README.md +27 -2
- package/dist/index.cjs +8811 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +885 -0
- package/dist/index.d.ts +885 -0
- package/dist/index.js +8843 -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,418 +0,0 @@
|
|
|
1
|
-
import { jest, describe, it, expect, beforeEach, afterEach } from "@jest/globals";
|
|
2
|
-
import {
|
|
3
|
-
buildParticipantListHash,
|
|
4
|
-
normalizeDMThreadToJid,
|
|
5
|
-
parseMessengerJid,
|
|
6
|
-
sameMessengerDevice,
|
|
7
|
-
sameMessengerUser,
|
|
8
|
-
toADString,
|
|
9
|
-
toBareMessengerJid,
|
|
10
|
-
uniqueJids,
|
|
11
|
-
} from "../../../src/e2ee/application/fanout-planner.ts";
|
|
12
|
-
import { OutboundMessageCache, type RecentE2EEOutgoing } from "../../../src/e2ee/application/outbound-message-cache.ts";
|
|
13
|
-
import { PreKeyMaintenance } from "../../../src/e2ee/application/prekey-maintenance.ts";
|
|
14
|
-
import { E2EERetryManager } from "../../../src/e2ee/application/retry-manager.ts";
|
|
15
|
-
import { unmarshal, type Node } from "../../../src/e2ee/transport/binary/wa-binary.ts";
|
|
16
|
-
|
|
17
|
-
const makeOutgoing = (overrides: Partial<RecentE2EEOutgoing> = {}): RecentE2EEOutgoing => ({
|
|
18
|
-
kind: "dm",
|
|
19
|
-
chatJid: "200.0@msgr",
|
|
20
|
-
messageId: "m1",
|
|
21
|
-
messageType: "text",
|
|
22
|
-
messageApp: Buffer.from("message-app"),
|
|
23
|
-
frankingTag: Buffer.from("franking-tag"),
|
|
24
|
-
createdAtMs: 1_000,
|
|
25
|
-
...overrides,
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
const findChild = (node: Node, tag: string): Node | undefined => {
|
|
29
|
-
const children = Array.isArray(node.content) ? node.content as Node[] : [];
|
|
30
|
-
return children.find((child) => child.tag === tag);
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
describe("E2EE application helpers", () => {
|
|
34
|
-
let logSpy: jest.SpiedFunction<typeof console.log>;
|
|
35
|
-
|
|
36
|
-
beforeEach(() => {
|
|
37
|
-
logSpy = jest.spyOn(console, "log").mockImplementation(() => undefined);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
afterEach(() => {
|
|
41
|
-
logSpy.mockRestore();
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
describe("fanout-planner", () => {
|
|
45
|
-
it("normalizes Messenger device JIDs to stable bare DM chat JIDs", () => {
|
|
46
|
-
expect(normalizeDMThreadToJid("100042415119261")).toBe("100042415119261.0@msgr");
|
|
47
|
-
expect(normalizeDMThreadToJid("100042415119261.160@msgr")).toBe("100042415119261.0@msgr");
|
|
48
|
-
expect(toBareMessengerJid("100042415119261:160@msgr")).toBe("100042415119261.0@msgr");
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it("parses dot, colon, and AD-style Messenger device identifiers", () => {
|
|
52
|
-
expect(parseMessengerJid("100.160@msgr")).toMatchObject({ user: "100", device: 160, server: "msgr" });
|
|
53
|
-
expect(parseMessengerJid("100:160@msgr")).toMatchObject({ user: "100", device: 160, server: "msgr" });
|
|
54
|
-
expect(parseMessengerJid("100.0:160@msgr")).toMatchObject({ user: "100", device: 160, server: "msgr" });
|
|
55
|
-
expect(toADString("100.160@msgr")).toBe("100.0:160@msgr");
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it("deduplicates devices by Messenger user and device while preserving first occurrence", () => {
|
|
59
|
-
expect(uniqueJids(["100.1@msgr", "100:1@msgr", "100.2@msgr", "200.1@msgr"])).toEqual([
|
|
60
|
-
"100.1@msgr",
|
|
61
|
-
"100.2@msgr",
|
|
62
|
-
"200.1@msgr",
|
|
63
|
-
]);
|
|
64
|
-
expect(sameMessengerUser("100.1@msgr", "100.2@msgr")).toBe(true);
|
|
65
|
-
expect(sameMessengerDevice("100.1@msgr", "100:1@msgr")).toBe(true);
|
|
66
|
-
expect(sameMessengerDevice("100.1@msgr", "100.2@msgr")).toBe(false);
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it("builds participant hashes independent of input order and JID separator style", () => {
|
|
70
|
-
const hashA = buildParticipantListHash(["200.2@msgr", "100.1@msgr"]);
|
|
71
|
-
const hashB = buildParticipantListHash(["100:1@msgr", "200:2@msgr"]);
|
|
72
|
-
|
|
73
|
-
expect(hashA).toBe(hashB);
|
|
74
|
-
expect(hashA).toMatch(/^2:[A-Za-z0-9+/]{8}$/);
|
|
75
|
-
});
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
describe("OutboundMessageCache", () => {
|
|
79
|
-
it("returns remembered records and prunes expired entries", () => {
|
|
80
|
-
const nowMs = Date.now();
|
|
81
|
-
const cache = new OutboundMessageCache({ ttlMs: 100, maxEntries: 10 });
|
|
82
|
-
cache.remember(makeOutgoing({ messageId: "fresh", createdAtMs: nowMs }));
|
|
83
|
-
cache.remember(makeOutgoing({ messageId: "old", createdAtMs: nowMs - 200 }));
|
|
84
|
-
|
|
85
|
-
cache.prune(nowMs + 1);
|
|
86
|
-
expect(cache.get("fresh")?.messageId).toBe("fresh");
|
|
87
|
-
cache.prune(nowMs + 1);
|
|
88
|
-
expect(cache.get("old")).toBeUndefined();
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it("keeps only the newest records when maxEntries is exceeded", () => {
|
|
92
|
-
const nowMs = Date.now();
|
|
93
|
-
const cache = new OutboundMessageCache({ ttlMs: 10_000, maxEntries: 2 });
|
|
94
|
-
cache.remember(makeOutgoing({ messageId: "m1", createdAtMs: nowMs }));
|
|
95
|
-
cache.remember(makeOutgoing({ messageId: "m2", createdAtMs: nowMs }));
|
|
96
|
-
cache.remember(makeOutgoing({ messageId: "m3", createdAtMs: nowMs }));
|
|
97
|
-
|
|
98
|
-
expect(cache.get("m1")).toBeUndefined();
|
|
99
|
-
expect(cache.get("m2")?.messageId).toBe("m2");
|
|
100
|
-
expect(cache.get("m3")?.messageId).toBe("m3");
|
|
101
|
-
|
|
102
|
-
cache.clear();
|
|
103
|
-
expect(cache.get("m2")).toBeUndefined();
|
|
104
|
-
});
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
describe("PreKeyMaintenance", () => {
|
|
108
|
-
const oldEnv = process.env;
|
|
109
|
-
|
|
110
|
-
beforeEach(() => {
|
|
111
|
-
process.env = { ...oldEnv, FB_E2EE_PREKEY_MIN_COUNT: "5", FB_E2EE_PREKEY_UPLOAD_COUNT: "9" };
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
afterEach(() => {
|
|
115
|
-
process.env = oldEnv;
|
|
116
|
-
jest.useRealTimers();
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
it("uploads fresh prekeys when server count is below threshold", async () => {
|
|
120
|
-
const uploadPreKeys = jest.fn<(count: number) => Promise<void>>().mockResolvedValue(undefined);
|
|
121
|
-
const maintenance = new PreKeyMaintenance({
|
|
122
|
-
getSocket: () => ({}),
|
|
123
|
-
getStore: () => ({}),
|
|
124
|
-
getServerPreKeyCount: jest.fn<() => Promise<number>>().mockResolvedValue(4),
|
|
125
|
-
uploadPreKeys,
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
await maintenance.sync("test");
|
|
129
|
-
|
|
130
|
-
expect(uploadPreKeys).toHaveBeenCalledWith(9);
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
it("does not upload when disconnected or when server count is healthy", async () => {
|
|
134
|
-
const uploadPreKeys = jest.fn<(count: number) => Promise<void>>().mockResolvedValue(undefined);
|
|
135
|
-
const disconnected = new PreKeyMaintenance({
|
|
136
|
-
getSocket: () => null,
|
|
137
|
-
getStore: () => ({}),
|
|
138
|
-
getServerPreKeyCount: jest.fn<() => Promise<number>>().mockResolvedValue(0),
|
|
139
|
-
uploadPreKeys,
|
|
140
|
-
});
|
|
141
|
-
await disconnected.sync("disconnected");
|
|
142
|
-
|
|
143
|
-
const healthy = new PreKeyMaintenance({
|
|
144
|
-
getSocket: () => ({}),
|
|
145
|
-
getStore: () => ({}),
|
|
146
|
-
getServerPreKeyCount: jest.fn<() => Promise<number>>().mockResolvedValue(5),
|
|
147
|
-
uploadPreKeys,
|
|
148
|
-
});
|
|
149
|
-
await healthy.sync("healthy");
|
|
150
|
-
|
|
151
|
-
expect(uploadPreKeys).not.toHaveBeenCalled();
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
it("starts periodic sync and stop cancels the interval", async () => {
|
|
156
|
-
jest.useFakeTimers();
|
|
157
|
-
process.env.FB_E2EE_PREKEY_SYNC_INTERVAL_MS = "100";
|
|
158
|
-
const getServerPreKeyCount = jest.fn<() => Promise<number>>().mockResolvedValue(5);
|
|
159
|
-
const maintenance = new PreKeyMaintenance({
|
|
160
|
-
getSocket: () => ({}),
|
|
161
|
-
getStore: () => ({}),
|
|
162
|
-
getServerPreKeyCount,
|
|
163
|
-
uploadPreKeys: jest.fn<(count: number) => Promise<void>>().mockResolvedValue(undefined),
|
|
164
|
-
});
|
|
165
|
-
try {
|
|
166
|
-
maintenance.start();
|
|
167
|
-
await jest.advanceTimersByTimeAsync(100);
|
|
168
|
-
expect(getServerPreKeyCount).toHaveBeenCalledTimes(1);
|
|
169
|
-
|
|
170
|
-
maintenance.stop();
|
|
171
|
-
await jest.advanceTimersByTimeAsync(100);
|
|
172
|
-
expect(getServerPreKeyCount).toHaveBeenCalledTimes(1);
|
|
173
|
-
} finally {
|
|
174
|
-
maintenance.stop();
|
|
175
|
-
}
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
it("does not start periodic sync when interval is disabled", async () => {
|
|
179
|
-
jest.useFakeTimers();
|
|
180
|
-
process.env.FB_E2EE_PREKEY_SYNC_INTERVAL_MS = "0";
|
|
181
|
-
const getServerPreKeyCount = jest.fn<() => Promise<number>>().mockResolvedValue(0);
|
|
182
|
-
const maintenance = new PreKeyMaintenance({
|
|
183
|
-
getSocket: () => ({}),
|
|
184
|
-
getStore: () => ({}),
|
|
185
|
-
getServerPreKeyCount,
|
|
186
|
-
uploadPreKeys: jest.fn<(count: number) => Promise<void>>().mockResolvedValue(undefined),
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
maintenance.start();
|
|
190
|
-
await jest.advanceTimersByTimeAsync(1_000);
|
|
191
|
-
|
|
192
|
-
expect(getServerPreKeyCount).not.toHaveBeenCalled();
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
it("swallows sync errors after logging them", async () => {
|
|
196
|
-
const errorSpy = jest.spyOn(console, "error").mockImplementation(() => undefined);
|
|
197
|
-
const maintenance = new PreKeyMaintenance({
|
|
198
|
-
getSocket: () => ({}),
|
|
199
|
-
getStore: () => ({}),
|
|
200
|
-
getServerPreKeyCount: jest.fn<() => Promise<number>>().mockRejectedValue(new Error("count failed")),
|
|
201
|
-
uploadPreKeys: jest.fn<(count: number) => Promise<void>>().mockResolvedValue(undefined),
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
try {
|
|
205
|
-
await expect(maintenance.sync("manual")).resolves.toBeUndefined();
|
|
206
|
-
expect(errorSpy).toHaveBeenCalledWith(
|
|
207
|
-
"PreKeyMaintenance",
|
|
208
|
-
expect.stringContaining("Prekey sync failed (manual)"),
|
|
209
|
-
expect.any(Error),
|
|
210
|
-
);
|
|
211
|
-
} finally {
|
|
212
|
-
errorSpy.mockRestore();
|
|
213
|
-
}
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
describe("E2EERetryManager", () => {
|
|
219
|
-
it("re-encrypts cached DM messages to the requesting device", async () => {
|
|
220
|
-
const cache = new OutboundMessageCache({ ttlMs: 10_000 });
|
|
221
|
-
cache.remember(makeOutgoing({ messageId: "dm-1", chatJid: "200.0@msgr", createdAtMs: Date.now() }));
|
|
222
|
-
const socket = { sendFrame: jest.fn<(count: number) => Promise<void>>().mockResolvedValue(undefined) };
|
|
223
|
-
const client = {
|
|
224
|
-
hasSession: jest.fn<() => Promise<boolean>>().mockResolvedValue(true),
|
|
225
|
-
encryptMessageAppForDevice: jest.fn<(recipientJid: string, selfJid: string, messageApp: Buffer, opts?: any) => Promise<{ type: "msg"; ciphertext: Buffer }>>()
|
|
226
|
-
.mockResolvedValue({ type: "msg", ciphertext: Buffer.from("cipher") }),
|
|
227
|
-
};
|
|
228
|
-
const manager = new E2EERetryManager({
|
|
229
|
-
cache,
|
|
230
|
-
getClient: () => client as any,
|
|
231
|
-
getSocket: () => socket as any,
|
|
232
|
-
getSelfJid: () => "100.5@msgr",
|
|
233
|
-
getPreKeyBundle: jest.fn() as any,
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
await manager.handleReceipt({
|
|
237
|
-
tag: "receipt",
|
|
238
|
-
attrs: { id: "dm-1", from: "200.9@msgr" },
|
|
239
|
-
content: [{ tag: "retry", attrs: { id: "dm-1", count: "2", t: "123" } }],
|
|
240
|
-
} as any);
|
|
241
|
-
|
|
242
|
-
expect(client.encryptMessageAppForDevice).toHaveBeenCalledWith(
|
|
243
|
-
"200.9@msgr",
|
|
244
|
-
"100.5@msgr",
|
|
245
|
-
Buffer.from("message-app"),
|
|
246
|
-
{ dsm: undefined },
|
|
247
|
-
);
|
|
248
|
-
expect(socket.sendFrame).toHaveBeenCalledTimes(1);
|
|
249
|
-
const sent = unmarshal((socket.sendFrame as any).mock.calls[0][0]);
|
|
250
|
-
expect(sent.attrs).toMatchObject({ to: "200.0@msgr", id: "dm-1", t: "123", device_fanout: "false" });
|
|
251
|
-
expect(findChild(sent, "enc")?.attrs).toMatchObject({ type: "msg", count: "2" });
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
it("resends group messages with a fresh SKDM and backup directive", async () => {
|
|
255
|
-
const cache = new OutboundMessageCache({ ttlMs: 10_000 });
|
|
256
|
-
cache.remember(makeOutgoing({ kind: "group", messageId: "g-1", chatJid: "180@g.us", createdAtMs: Date.now() }));
|
|
257
|
-
const socket = { sendFrame: jest.fn<(count: number) => Promise<void>>().mockResolvedValue(undefined) };
|
|
258
|
-
const skdm = { groupId: "180@g.us", skdmBytes: Buffer.from("skdm"), distributionId: "dist" };
|
|
259
|
-
const client = {
|
|
260
|
-
hasSession: jest.fn<() => Promise<boolean>>().mockResolvedValue(true),
|
|
261
|
-
createSenderKeyDistributionPayload: jest.fn<(groupJid: string, selfJid: string) => Promise<typeof skdm>>().mockResolvedValue(skdm),
|
|
262
|
-
encryptMessageAppForDevice: jest.fn<(recipientJid: string, selfJid: string, messageApp: Buffer, opts?: any) => Promise<{ type: "pkmsg"; ciphertext: Buffer }>>()
|
|
263
|
-
.mockResolvedValue({ type: "pkmsg", ciphertext: Buffer.from("cipher") }),
|
|
264
|
-
};
|
|
265
|
-
const manager = new E2EERetryManager({
|
|
266
|
-
cache,
|
|
267
|
-
getClient: () => client as any,
|
|
268
|
-
getSocket: () => socket as any,
|
|
269
|
-
getSelfJid: () => "100.5@msgr",
|
|
270
|
-
getPreKeyBundle: jest.fn() as any,
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
await manager.handleReceipt({
|
|
274
|
-
tag: "receipt",
|
|
275
|
-
attrs: { id: "g-1", participant: "200.9@msgr" },
|
|
276
|
-
content: [{ tag: "retry", attrs: { id: "g-1", count: "1" } }],
|
|
277
|
-
} as any);
|
|
278
|
-
|
|
279
|
-
expect(client.createSenderKeyDistributionPayload).toHaveBeenCalledWith("180@g.us", "100.5@msgr");
|
|
280
|
-
expect(client.encryptMessageAppForDevice).toHaveBeenCalledWith(
|
|
281
|
-
"200.9@msgr",
|
|
282
|
-
"100.5@msgr",
|
|
283
|
-
Buffer.from("message-app"),
|
|
284
|
-
{ skdm, backupDirective: { messageId: "g-1", actionType: "UPSERT" } },
|
|
285
|
-
);
|
|
286
|
-
const sent = unmarshal((socket.sendFrame as any).mock.calls[0][0]);
|
|
287
|
-
expect(sent.attrs).toMatchObject({ to: "180@g.us", participant: "200.9@msgr", id: "g-1" });
|
|
288
|
-
expect(findChild(sent, "enc")?.attrs).toMatchObject({ type: "pkmsg", count: "1" });
|
|
289
|
-
});
|
|
290
|
-
|
|
291
|
-
it("establishes a session from retry receipt keys before resending", async () => {
|
|
292
|
-
const cache = new OutboundMessageCache({ ttlMs: 10_000 });
|
|
293
|
-
cache.remember(makeOutgoing({ messageId: "dm-keys", chatJid: "200.0@msgr", createdAtMs: Date.now() }));
|
|
294
|
-
const socket = { sendFrame: jest.fn<(frame: Buffer) => Promise<void>>().mockResolvedValue(undefined) };
|
|
295
|
-
const client = {
|
|
296
|
-
establishSession: jest.fn<(jid: string, bundle: any) => Promise<void>>().mockResolvedValue(undefined),
|
|
297
|
-
hasSession: jest.fn<() => Promise<boolean>>().mockResolvedValue(false),
|
|
298
|
-
encryptMessageAppForDevice: jest.fn<(recipientJid: string, selfJid: string, messageApp: Buffer, opts?: any) => Promise<{ type: "msg"; ciphertext: Buffer }>>()
|
|
299
|
-
.mockResolvedValue({ type: "msg", ciphertext: Buffer.from("cipher") }),
|
|
300
|
-
};
|
|
301
|
-
const getPreKeyBundle = jest.fn<() => Promise<any>>().mockResolvedValue({});
|
|
302
|
-
const manager = new E2EERetryManager({
|
|
303
|
-
cache,
|
|
304
|
-
getClient: () => client as any,
|
|
305
|
-
getSocket: () => socket as any,
|
|
306
|
-
getSelfJid: () => "100.5@msgr",
|
|
307
|
-
getPreKeyBundle,
|
|
308
|
-
});
|
|
309
|
-
const registration = Buffer.alloc(4);
|
|
310
|
-
registration.writeUInt32BE(0x01020304);
|
|
311
|
-
|
|
312
|
-
await manager.handleReceipt({
|
|
313
|
-
tag: "receipt",
|
|
314
|
-
attrs: { id: "dm-keys", from: "200.9@msgr" },
|
|
315
|
-
content: [
|
|
316
|
-
{ tag: "retry", attrs: { id: "dm-keys", count: "1" } },
|
|
317
|
-
{ tag: "registration", attrs: {}, content: registration },
|
|
318
|
-
{
|
|
319
|
-
tag: "keys",
|
|
320
|
-
attrs: {},
|
|
321
|
-
content: [
|
|
322
|
-
{ tag: "identity", attrs: {}, content: Buffer.alloc(32, 1) },
|
|
323
|
-
{ tag: "key", attrs: {}, content: [
|
|
324
|
-
{ tag: "id", attrs: {}, content: Buffer.from([0, 0, 7]) },
|
|
325
|
-
{ tag: "value", attrs: {}, content: Buffer.alloc(32, 2) },
|
|
326
|
-
] },
|
|
327
|
-
{ tag: "skey", attrs: {}, content: [
|
|
328
|
-
{ tag: "id", attrs: {}, content: Buffer.from([0, 0, 8]) },
|
|
329
|
-
{ tag: "value", attrs: {}, content: Buffer.alloc(32, 3) },
|
|
330
|
-
{ tag: "signature", attrs: {}, content: Buffer.alloc(64, 4) },
|
|
331
|
-
] },
|
|
332
|
-
],
|
|
333
|
-
},
|
|
334
|
-
],
|
|
335
|
-
} as any);
|
|
336
|
-
|
|
337
|
-
expect(client.establishSession).toHaveBeenCalledWith("200.9@msgr", {
|
|
338
|
-
registrationId: 0x01020304,
|
|
339
|
-
deviceId: 9,
|
|
340
|
-
identityKey: Buffer.concat([Buffer.from([5]), Buffer.alloc(32, 1)]),
|
|
341
|
-
signedPreKey: {
|
|
342
|
-
keyId: 8,
|
|
343
|
-
publicKey: Buffer.concat([Buffer.from([5]), Buffer.alloc(32, 3)]),
|
|
344
|
-
signature: Buffer.alloc(64, 4),
|
|
345
|
-
},
|
|
346
|
-
preKey: {
|
|
347
|
-
keyId: 7,
|
|
348
|
-
publicKey: Buffer.concat([Buffer.from([5]), Buffer.alloc(32, 2)]),
|
|
349
|
-
},
|
|
350
|
-
});
|
|
351
|
-
expect(client.hasSession).not.toHaveBeenCalled();
|
|
352
|
-
expect(getPreKeyBundle).not.toHaveBeenCalled();
|
|
353
|
-
expect(socket.sendFrame).toHaveBeenCalledTimes(1);
|
|
354
|
-
});
|
|
355
|
-
|
|
356
|
-
it("fetches a prekey bundle when no retry keys and no session exist", async () => {
|
|
357
|
-
const cache = new OutboundMessageCache({ ttlMs: 10_000 });
|
|
358
|
-
cache.remember(makeOutgoing({ messageId: "dm-fetch", chatJid: "200.0@msgr", createdAtMs: Date.now() }));
|
|
359
|
-
const socket = { sendFrame: jest.fn<(frame: Buffer) => Promise<void>>().mockResolvedValue(undefined) };
|
|
360
|
-
const bundle = { registrationId: 1, deviceId: 9, identityKey: Buffer.alloc(33), signedPreKey: { keyId: 1, publicKey: Buffer.alloc(33), signature: Buffer.alloc(64) } };
|
|
361
|
-
const client = {
|
|
362
|
-
establishSession: jest.fn<(jid: string, bundle: any) => Promise<void>>().mockResolvedValue(undefined),
|
|
363
|
-
hasSession: jest.fn<() => Promise<boolean>>().mockResolvedValue(false),
|
|
364
|
-
encryptMessageAppForDevice: jest.fn<(recipientJid: string, selfJid: string, messageApp: Buffer, opts?: any) => Promise<{ type: "msg"; ciphertext: Buffer }>>()
|
|
365
|
-
.mockResolvedValue({ type: "msg", ciphertext: Buffer.from("cipher") }),
|
|
366
|
-
};
|
|
367
|
-
const getPreKeyBundle = jest.fn<(jid: string) => Promise<any>>().mockResolvedValue(bundle);
|
|
368
|
-
const manager = new E2EERetryManager({
|
|
369
|
-
cache,
|
|
370
|
-
getClient: () => client as any,
|
|
371
|
-
getSocket: () => socket as any,
|
|
372
|
-
getSelfJid: () => "100.5@msgr",
|
|
373
|
-
getPreKeyBundle,
|
|
374
|
-
});
|
|
375
|
-
|
|
376
|
-
await manager.handleReceipt({
|
|
377
|
-
tag: "receipt",
|
|
378
|
-
attrs: { id: "dm-fetch", from: "200.9@msgr" },
|
|
379
|
-
content: [{ tag: "retry", attrs: { id: "dm-fetch", count: "1" } }],
|
|
380
|
-
} as any);
|
|
381
|
-
|
|
382
|
-
expect(getPreKeyBundle).toHaveBeenCalledWith("200.9@msgr");
|
|
383
|
-
expect(client.establishSession).toHaveBeenCalledWith("200.9@msgr", bundle);
|
|
384
|
-
});
|
|
385
|
-
|
|
386
|
-
it("ignores retry receipts at or above the retry cap", async () => {
|
|
387
|
-
const warnSpy = jest.spyOn(console, "warn").mockImplementation(() => undefined);
|
|
388
|
-
const cache = new OutboundMessageCache({ ttlMs: 10_000 });
|
|
389
|
-
cache.remember(makeOutgoing({ messageId: "dm-cap", createdAtMs: Date.now() }));
|
|
390
|
-
const socket = { sendFrame: jest.fn<(frame: Buffer) => Promise<void>>().mockResolvedValue(undefined) };
|
|
391
|
-
const client = {
|
|
392
|
-
hasSession: jest.fn<() => Promise<boolean>>().mockResolvedValue(true),
|
|
393
|
-
encryptMessageAppForDevice: jest.fn(),
|
|
394
|
-
};
|
|
395
|
-
const manager = new E2EERetryManager({
|
|
396
|
-
cache,
|
|
397
|
-
getClient: () => client as any,
|
|
398
|
-
getSocket: () => socket as any,
|
|
399
|
-
getSelfJid: () => "100.5@msgr",
|
|
400
|
-
getPreKeyBundle: jest.fn() as any,
|
|
401
|
-
});
|
|
402
|
-
|
|
403
|
-
try {
|
|
404
|
-
await manager.handleReceipt({
|
|
405
|
-
tag: "receipt",
|
|
406
|
-
attrs: { id: "dm-cap", from: "200.9@msgr" },
|
|
407
|
-
content: [{ tag: "retry", attrs: { id: "dm-cap", count: "10" } }],
|
|
408
|
-
} as any);
|
|
409
|
-
|
|
410
|
-
expect(client.encryptMessageAppForDevice).not.toHaveBeenCalled();
|
|
411
|
-
expect(socket.sendFrame).not.toHaveBeenCalled();
|
|
412
|
-
} finally {
|
|
413
|
-
warnSpy.mockRestore();
|
|
414
|
-
}
|
|
415
|
-
});
|
|
416
|
-
|
|
417
|
-
});
|
|
418
|
-
});
|
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
import { DeviceStore } from "../../../src/e2ee/store/device-store.ts";
|
|
2
|
-
import { Direction, PrivateKey, ProtocolAddress } from "@signalapp/libsignal-client";
|
|
3
|
-
import * as fs from "node:fs";
|
|
4
|
-
import * as path from "node:path";
|
|
5
|
-
import * as os from "node:os";
|
|
6
|
-
|
|
7
|
-
describe("DeviceStore", () => {
|
|
8
|
-
let store: DeviceStore;
|
|
9
|
-
let testPath: string;
|
|
10
|
-
|
|
11
|
-
beforeEach(async () => {
|
|
12
|
-
testPath = path.join(os.tmpdir(), `device-${Math.random().toString(36).slice(2)}.json`);
|
|
13
|
-
store = await DeviceStore.fromFile(testPath);
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
afterEach(() => {
|
|
17
|
-
if (fs.existsSync(testPath)) {
|
|
18
|
-
fs.unlinkSync(testPath);
|
|
19
|
-
}
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
describe("serialization", () => {
|
|
23
|
-
it("should generate a new device if file missing", () => {
|
|
24
|
-
expect(store.registrationId).toBeGreaterThan(0);
|
|
25
|
-
expect(store.noiseKeyPriv).toBeDefined();
|
|
26
|
-
expect(store.toJSON().schema_version).toBe(1);
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it("should export to JSON correctly", () => {
|
|
30
|
-
const json = store.toJSON();
|
|
31
|
-
expect(json.registration_id).toBe(store.registrationId);
|
|
32
|
-
expect(json.noise_key_priv).toBe(store.noiseKeyPriv.toString("base64"));
|
|
33
|
-
expect(json.identities).toEqual({});
|
|
34
|
-
expect(json.sessions).toEqual({});
|
|
35
|
-
expect(json.sender_keys).toEqual({});
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it("should save and reload from file without rotating identity", async () => {
|
|
39
|
-
const regId = store.registrationId;
|
|
40
|
-
const identity = store.getIdentityPrivateKey().toString("base64");
|
|
41
|
-
(store as any).saveToFile();
|
|
42
|
-
|
|
43
|
-
const newStore = await DeviceStore.fromFile(testPath);
|
|
44
|
-
expect(newStore.registrationId).toBe(regId);
|
|
45
|
-
expect(newStore.getIdentityPrivateKey().toString("base64")).toBe(identity);
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it("should load old JSON through migration defaults", async () => {
|
|
49
|
-
const oldJson: any = store.toJSON();
|
|
50
|
-
delete oldJson.schema_version;
|
|
51
|
-
delete oldJson.identities;
|
|
52
|
-
delete oldJson.sessions;
|
|
53
|
-
delete oldJson.pre_keys;
|
|
54
|
-
delete oldJson.sender_keys;
|
|
55
|
-
delete oldJson.signed_pre_keys;
|
|
56
|
-
delete oldJson.next_pre_key_id;
|
|
57
|
-
|
|
58
|
-
const loaded = await DeviceStore.fromData(JSON.stringify(oldJson));
|
|
59
|
-
expect(loaded.toJSON()).toMatchObject({
|
|
60
|
-
schema_version: 1,
|
|
61
|
-
next_pre_key_id: 1,
|
|
62
|
-
identities: {},
|
|
63
|
-
sessions: {},
|
|
64
|
-
pre_keys: {},
|
|
65
|
-
sender_keys: {},
|
|
66
|
-
signed_pre_keys: {},
|
|
67
|
-
});
|
|
68
|
-
});
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
describe("Messenger JID persistence", () => {
|
|
72
|
-
it("should parse dot and colon device JIDs and trigger onDataChanged only on changes", async () => {
|
|
73
|
-
const changes: string[] = [];
|
|
74
|
-
const memoryStore = await DeviceStore.fromData(store.getData(), json => changes.push(json));
|
|
75
|
-
|
|
76
|
-
memoryStore.setJIDs("100042415119261.160@msgr", "");
|
|
77
|
-
memoryStore.setJIDs("100042415119261.160@msgr", "");
|
|
78
|
-
expect(memoryStore.jidUser).toBe("100042415119261");
|
|
79
|
-
expect(memoryStore.jidDevice).toBe(160);
|
|
80
|
-
expect(changes).toHaveLength(1);
|
|
81
|
-
|
|
82
|
-
memoryStore.setJIDs("100042415119261:161@msgr", "");
|
|
83
|
-
expect(memoryStore.jidDevice).toBe(161);
|
|
84
|
-
expect(changes).toHaveLength(2);
|
|
85
|
-
expect(JSON.parse(changes.at(-1)!).jid_device).toBe(161);
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it("should ignore invalid non-Messenger JIDs", () => {
|
|
89
|
-
store.setJIDs("100@s.whatsapp.net", "");
|
|
90
|
-
expect(store.jidUser).toBeUndefined();
|
|
91
|
-
expect(store.jidDevice).toBeUndefined();
|
|
92
|
-
});
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
describe("identity and in-memory protocol maps", () => {
|
|
96
|
-
it("should save identities, detect changes, and enforce trust-on-first-use", async () => {
|
|
97
|
-
const address = ProtocolAddress.new("1001", 2);
|
|
98
|
-
const keyA = PrivateKey.generate().getPublicKey();
|
|
99
|
-
const keyB = PrivateKey.generate().getPublicKey();
|
|
100
|
-
|
|
101
|
-
await expect(store.isTrustedIdentity(address, keyA, Direction.Sending)).resolves.toBe(true);
|
|
102
|
-
await expect(store.saveIdentity(address, keyA)).resolves.toBe(true);
|
|
103
|
-
await expect(store.saveIdentity(address, keyA)).resolves.toBe(false);
|
|
104
|
-
await expect(store.isTrustedIdentity(address, keyA, Direction.Sending)).resolves.toBe(true);
|
|
105
|
-
await expect(store.isTrustedIdentity(address, keyB, Direction.Sending)).resolves.toBe(false);
|
|
106
|
-
store.autoTrust = true;
|
|
107
|
-
await expect(store.isTrustedIdentity(address, keyB, Direction.Sending)).resolves.toBe(true);
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
it("should report existing sessions without deserializing unrelated addresses", () => {
|
|
111
|
-
(store as any).sessions.set("1001.2", Buffer.from("fake-session-record"));
|
|
112
|
-
|
|
113
|
-
expect(store.hasSession("1001.2")).toBe(true);
|
|
114
|
-
expect(store.hasSession("1001.3")).toBe(false);
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it("should list sender-key distribution IDs for one sender only", () => {
|
|
118
|
-
const sender = ProtocolAddress.new("1001", 2);
|
|
119
|
-
(store as any).senderKeys.set("1001.2::dist-a", Buffer.from("a"));
|
|
120
|
-
(store as any).senderKeys.set("1001.2::dist-b", Buffer.from("b"));
|
|
121
|
-
(store as any).senderKeys.set("1001.3::dist-c", Buffer.from("c"));
|
|
122
|
-
|
|
123
|
-
expect(store.listSenderKeyDistributionIds(sender).sort()).toEqual(["dist-a", "dist-b"]);
|
|
124
|
-
});
|
|
125
|
-
});
|
|
126
|
-
});
|
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
decodeVarint,
|
|
3
|
-
encodeVarint,
|
|
4
|
-
parseFBProtobufSKDM,
|
|
5
|
-
parseFBProtobufSKMSG,
|
|
6
|
-
stableDistributionId,
|
|
7
|
-
uuidParse,
|
|
8
|
-
uuidStringify,
|
|
9
|
-
wrapAsSignalSKMSG,
|
|
10
|
-
} from "../../../src/e2ee/facebook/facebook-protocol-utils.ts";
|
|
11
|
-
|
|
12
|
-
const fieldVarint = (field: number, value: number) => Buffer.concat([
|
|
13
|
-
encodeVarint((field << 3) | 0),
|
|
14
|
-
encodeVarint(value),
|
|
15
|
-
]);
|
|
16
|
-
|
|
17
|
-
const fieldBytes = (field: number, value: Buffer) => Buffer.concat([
|
|
18
|
-
encodeVarint((field << 3) | 2),
|
|
19
|
-
encodeVarint(value.length),
|
|
20
|
-
value,
|
|
21
|
-
]);
|
|
22
|
-
|
|
23
|
-
describe("facebook-protocol-utils", () => {
|
|
24
|
-
describe("varint", () => {
|
|
25
|
-
it.each([0, 1, 127, 128, 150, 255, 16_384, 1_234_567, 0xffffffff])(
|
|
26
|
-
"round-trips %i",
|
|
27
|
-
(value) => {
|
|
28
|
-
const buf = encodeVarint(value);
|
|
29
|
-
const decoded = decodeVarint(Buffer.concat([Buffer.from([0xaa]), buf]), 1);
|
|
30
|
-
expect(decoded.value).toBe(value);
|
|
31
|
-
expect(decoded.length).toBe(buf.length);
|
|
32
|
-
},
|
|
33
|
-
);
|
|
34
|
-
|
|
35
|
-
it("should throw on unexpected EOF", () => {
|
|
36
|
-
const buf = Buffer.from([0x80, 0x80]);
|
|
37
|
-
expect(() => decodeVarint(buf, 0)).toThrow("Unexpected EOF in varint");
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it("should throw on overlong varints", () => {
|
|
41
|
-
expect(() => decodeVarint(Buffer.alloc(11, 0x80), 0)).toThrow("Varint too long");
|
|
42
|
-
});
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
describe("uuid", () => {
|
|
46
|
-
it("should stringify and parse UUIDs correctly", () => {
|
|
47
|
-
const original = "6ba7b810-9dad-11d1-80b4-00c04fd430c8";
|
|
48
|
-
const buf = uuidParse(original);
|
|
49
|
-
expect(buf.length).toBe(16);
|
|
50
|
-
const str = uuidStringify(buf);
|
|
51
|
-
expect(str).toBe(original);
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it("stableDistributionId should be deterministic and look like a v4 UUID", () => {
|
|
55
|
-
const group = "12345@g.us";
|
|
56
|
-
const sender = "67890@s.whatsapp.net";
|
|
57
|
-
const id1 = stableDistributionId(group, sender);
|
|
58
|
-
const id2 = stableDistributionId(group, sender);
|
|
59
|
-
const other = stableDistributionId(group, "11111@s.whatsapp.net");
|
|
60
|
-
|
|
61
|
-
expect(id1).toBe(id2);
|
|
62
|
-
expect(id1).not.toBe(other);
|
|
63
|
-
expect(id1).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/);
|
|
64
|
-
});
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
describe("Facebook protobuf SKMSG/SKDM", () => {
|
|
68
|
-
it("parses SKMSG and skips unknown varint/bytes fields", () => {
|
|
69
|
-
const ciphertext = Buffer.from("ciphertext");
|
|
70
|
-
const encoded = Buffer.concat([
|
|
71
|
-
fieldVarint(9, 1),
|
|
72
|
-
fieldBytes(10, Buffer.from("skip")),
|
|
73
|
-
fieldVarint(1, 42),
|
|
74
|
-
fieldVarint(2, 7),
|
|
75
|
-
fieldBytes(3, ciphertext),
|
|
76
|
-
]);
|
|
77
|
-
|
|
78
|
-
expect(parseFBProtobufSKMSG(encoded)).toEqual({
|
|
79
|
-
id: 42,
|
|
80
|
-
iteration: 7,
|
|
81
|
-
ciphertext,
|
|
82
|
-
});
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it("returns null for malformed SKMSG length prefixes", () => {
|
|
86
|
-
expect(parseFBProtobufSKMSG(Buffer.from([0x1a, 0x80]))).toBeNull();
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it("parses SKDM with optional signing public key", () => {
|
|
90
|
-
const chainKey = Buffer.alloc(32, 1);
|
|
91
|
-
const signingPublicKey = Buffer.alloc(33, 2);
|
|
92
|
-
const encoded = Buffer.concat([
|
|
93
|
-
fieldVarint(1, 123),
|
|
94
|
-
fieldVarint(2, 456),
|
|
95
|
-
fieldBytes(3, chainKey),
|
|
96
|
-
fieldBytes(4, signingPublicKey),
|
|
97
|
-
fieldBytes(12, Buffer.from("unknown")),
|
|
98
|
-
]);
|
|
99
|
-
|
|
100
|
-
expect(parseFBProtobufSKDM(encoded)).toEqual({
|
|
101
|
-
chainId: 123,
|
|
102
|
-
iteration: 456,
|
|
103
|
-
chainKey,
|
|
104
|
-
signingPublicKey,
|
|
105
|
-
});
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it("returns null for malformed SKDM length prefixes", () => {
|
|
109
|
-
expect(parseFBProtobufSKDM(Buffer.from([0x1a, 0x80]))).toBeNull();
|
|
110
|
-
});
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
describe("wrapAsSignalSKMSG", () => {
|
|
114
|
-
it("wraps FB sender-key message fields and appends deterministic signature", () => {
|
|
115
|
-
const distributionId = "6ba7b810-9dad-41d1-80b4-00c04fd430c8";
|
|
116
|
-
const ciphertext = Buffer.from("group-ciphertext");
|
|
117
|
-
const wrapped = wrapAsSignalSKMSG({
|
|
118
|
-
distributionId,
|
|
119
|
-
id: 5,
|
|
120
|
-
iteration: 9,
|
|
121
|
-
ciphertext,
|
|
122
|
-
senderJid: "100.1@msgr",
|
|
123
|
-
});
|
|
124
|
-
const signedPayloadLength = 1 + 18 + 2 + 2 + 2 + ciphertext.length;
|
|
125
|
-
|
|
126
|
-
expect(wrapped).toHaveLength(signedPayloadLength + 64);
|
|
127
|
-
expect(wrapped[0]).toBe(0x33);
|
|
128
|
-
expect(wrapped.subarray(1, 19)).toEqual(Buffer.concat([Buffer.from([0x0a, 0x10]), uuidParse(distributionId)]));
|
|
129
|
-
expect(wrapped.includes(ciphertext)).toBe(true);
|
|
130
|
-
expect(wrapped.subarray(0, signedPayloadLength).includes(ciphertext)).toBe(true);
|
|
131
|
-
expect(wrapped.subarray(signedPayloadLength)).toHaveLength(64);
|
|
132
|
-
});
|
|
133
|
-
});
|
|
134
|
-
});
|