fb-messenger-e2ee 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/DOCS.md +310 -0
  2. package/LICENSE +660 -0
  3. package/README.md +153 -0
  4. package/bun.lock +1014 -0
  5. package/examples/basic-usage.ts +52 -0
  6. package/jest.config.ts +18 -0
  7. package/package.json +56 -0
  8. package/src/config/env.ts +15 -0
  9. package/src/controllers/client.controller.ts +805 -0
  10. package/src/controllers/dgw-handler.ts +171 -0
  11. package/src/controllers/e2ee-handler.ts +832 -0
  12. package/src/controllers/event-mapper.ts +327 -0
  13. package/src/core/client.ts +151 -0
  14. package/src/e2ee/application/e2ee-client.ts +376 -0
  15. package/src/e2ee/application/fanout-planner.ts +79 -0
  16. package/src/e2ee/application/outbound-message-cache.ts +58 -0
  17. package/src/e2ee/application/prekey-maintenance.ts +55 -0
  18. package/src/e2ee/application/retry-manager.ts +156 -0
  19. package/src/e2ee/facebook/facebook-protocol-utils.ts +230 -0
  20. package/src/e2ee/facebook/icdc-payload.ts +31 -0
  21. package/src/e2ee/index.ts +76 -0
  22. package/src/e2ee/internal.ts +27 -0
  23. package/src/e2ee/media/media-crypto.ts +147 -0
  24. package/src/e2ee/media/media-upload.ts +90 -0
  25. package/src/e2ee/message/builders/client-payload.ts +54 -0
  26. package/src/e2ee/message/builders/consumer-application.ts +362 -0
  27. package/src/e2ee/message/builders/message-application.ts +64 -0
  28. package/src/e2ee/message/builders/message-transport.ts +84 -0
  29. package/src/e2ee/message/codecs/protobuf-codecs.ts +101 -0
  30. package/src/e2ee/message/constants.ts +4 -0
  31. package/src/e2ee/message/message-builder.ts +15 -0
  32. package/src/e2ee/message/proto/ArmadilloApplication.proto +281 -0
  33. package/src/e2ee/message/proto/ArmadilloICDC.proto +14 -0
  34. package/src/e2ee/message/proto/ConsumerApplication.proto +232 -0
  35. package/src/e2ee/message/proto/MessageApplication.proto +82 -0
  36. package/src/e2ee/message/proto/MessageTransport.proto +77 -0
  37. package/src/e2ee/message/proto/WACommon.proto +66 -0
  38. package/src/e2ee/message/proto/WAMediaTransport.proto +176 -0
  39. package/src/e2ee/message/proto/proto-writer.ts +76 -0
  40. package/src/e2ee/signal/prekey-manager.ts +140 -0
  41. package/src/e2ee/signal/signal-manager.ts +365 -0
  42. package/src/e2ee/store/device-json.ts +47 -0
  43. package/src/e2ee/store/device-repository.ts +12 -0
  44. package/src/e2ee/store/device-store.ts +538 -0
  45. package/src/e2ee/transport/binary/decoder.ts +180 -0
  46. package/src/e2ee/transport/binary/encoder.ts +143 -0
  47. package/src/e2ee/transport/binary/stanzas.ts +97 -0
  48. package/src/e2ee/transport/binary/tokens.ts +64 -0
  49. package/src/e2ee/transport/binary/wa-binary.ts +8 -0
  50. package/src/e2ee/transport/dgw/dgw-socket.ts +345 -0
  51. package/src/e2ee/transport/noise/noise-handshake.ts +417 -0
  52. package/src/e2ee/transport/noise/noise-socket.ts +230 -0
  53. package/src/index.ts +34 -0
  54. package/src/models/client.ts +26 -0
  55. package/src/models/config.ts +14 -0
  56. package/src/models/domain.ts +299 -0
  57. package/src/models/e2ee.ts +295 -0
  58. package/src/models/media.ts +41 -0
  59. package/src/models/messaging.ts +88 -0
  60. package/src/models/thread.ts +69 -0
  61. package/src/repositories/session.repository.ts +20 -0
  62. package/src/services/auth.service.ts +55 -0
  63. package/src/services/e2ee.service.ts +174 -0
  64. package/src/services/facebook-gateway.service.ts +245 -0
  65. package/src/services/icdc.service.ts +177 -0
  66. package/src/services/media.service.ts +304 -0
  67. package/src/services/messaging.service.ts +28 -0
  68. package/src/services/thread.service.ts +199 -0
  69. package/src/types/advanced-types.ts +61 -0
  70. package/src/types/fca-unofficial.d.ts +212 -0
  71. package/src/types/protocol-types.ts +43 -0
  72. package/src/utils/fca-utils.ts +30 -0
  73. package/src/utils/logger.ts +24 -0
  74. package/src/utils/mime.ts +51 -0
  75. package/tests/.env.example +87 -0
  76. package/tests/data/1x1.png +0 -0
  77. package/tests/data/example.txt +1 -0
  78. package/tests/data/file_example.mp3 +0 -0
  79. package/tests/data/file_example.mp4 +0 -0
  80. package/tests/integration.test.ts +498 -0
  81. package/tests/script/echo-e2ee.ts +174 -0
  82. package/tests/script/send-e2ee-media.ts +227 -0
  83. package/tests/script/send-e2ee-reaction.ts +108 -0
  84. package/tests/script/send-e2ee-unsend.ts +115 -0
  85. package/tests/script/send-typing.ts +107 -0
  86. package/tests/script/test-group-send.ts +105 -0
  87. package/tests/setup.ts +3 -0
  88. package/tests/types.test.ts +57 -0
  89. package/tests/unit/controllers/dgw-handler.test.ts +60 -0
  90. package/tests/unit/controllers/e2ee-handler.test.ts +293 -0
  91. package/tests/unit/controllers/event-mapper.test.ts +252 -0
  92. package/tests/unit/e2ee/application-helpers.test.ts +418 -0
  93. package/tests/unit/e2ee/device-store.test.ts +126 -0
  94. package/tests/unit/e2ee/facebook-protocol-utils.test.ts +134 -0
  95. package/tests/unit/e2ee/media-crypto.test.ts +55 -0
  96. package/tests/unit/e2ee/media-upload.test.ts +60 -0
  97. package/tests/unit/e2ee/message-builder.test.ts +42 -0
  98. package/tests/unit/e2ee/message-builders.test.ts +230 -0
  99. package/tests/unit/e2ee/noise-handshake.test.ts +260 -0
  100. package/tests/unit/e2ee/prekey-manager.test.ts +55 -0
  101. package/tests/unit/e2ee/wa-binary.test.ts +127 -0
  102. package/tests/unit/services/e2ee.service.test.ts +101 -0
  103. package/tests/unit/services/facebook-gateway.service.test.ts +138 -0
  104. package/tests/unit/services/media.service.test.ts +169 -0
  105. package/tests/unit/utils/fca-utils.test.ts +48 -0
  106. package/tsconfig.json +19 -0
@@ -0,0 +1,365 @@
1
+ /**
2
+ * E2EE Signal Manager - Layer 2 (Signal Protocol)
3
+ *
4
+ * Handles all Signal Protocol encrypt/decrypt operations for:
5
+ * - DM (1-to-1): X3DH session establishment + Double Ratchet
6
+ * - Group: Sender Key distribution + group cipher
7
+ */
8
+
9
+ import {
10
+ ProtocolAddress,
11
+ SenderKeyDistributionMessage,
12
+ CiphertextMessageType,
13
+ SignalMessage,
14
+ PreKeySignalMessage,
15
+ signalEncrypt,
16
+ signalDecrypt,
17
+ signalDecryptPreKey,
18
+ processPreKeyBundle,
19
+ groupEncrypt,
20
+ groupDecrypt,
21
+ processSenderKeyDistributionMessage,
22
+ SenderKeyMessage,
23
+ } from "@signalapp/libsignal-client";
24
+ import { randomUUID } from "node:crypto";
25
+
26
+ import type { DeviceStore } from "../store/device-store.ts";
27
+ import type { RawPreKeyBundle } from "./prekey-manager.ts";
28
+ import { buildPreKeyBundle } from "./prekey-manager.ts";
29
+ import {
30
+ wrapAsSignalSKMSG,
31
+ parseFBProtobufSKMSG,
32
+ stableDistributionId,
33
+ uuidStringify,
34
+ } from "../facebook/facebook-protocol-utils.ts";
35
+ import { logger } from "../../utils/logger.ts";
36
+
37
+ /**
38
+ * Cast for strict libsignal params.
39
+ * Converts Uint8Array to Buffer if needed.
40
+ */
41
+ const u8 = (b: Uint8Array | undefined | null): Buffer => {
42
+ if (!b) return Buffer.alloc(0);
43
+ return Buffer.isBuffer(b) ? b : Buffer.from(b.buffer, b.byteOffset, b.byteLength);
44
+ };
45
+
46
+ // Address helpers
47
+
48
+ /**
49
+ * Build a ProtocolAddress from a Messenger/WhatsApp JID string.
50
+ * Format: "user.agent:device@server" or "user.agent@server".
51
+ * The agent suffix is part of the Signal user ID; the device suffix is the Signal device ID.
52
+ */
53
+ export function jidToAddress(jid: string): ProtocolAddress {
54
+ const [userPartRaw = jid, server = ""] = jid.split("@");
55
+
56
+ // Messenger FBJID stores the device in the FBJID device field. Our decoder
57
+ // represents that as either user.device@msgr or user:device@msgr, while
58
+ // participant hashes may use ADString form user.0:device@msgr. In all cases
59
+ // the Signal username is the bare FBID and the Signal device is the device id.
60
+ if (server === "msgr") {
61
+ const colonIdx = userPartRaw.indexOf(":");
62
+ const dotIdx = userPartRaw.indexOf(".");
63
+ const userEnd = dotIdx !== -1 ? dotIdx : (colonIdx !== -1 ? colonIdx : userPartRaw.length);
64
+ const user = userPartRaw.slice(0, userEnd) || userPartRaw;
65
+ const rawDevice = colonIdx !== -1
66
+ ? userPartRaw.slice(colonIdx + 1)
67
+ : (dotIdx !== -1 ? userPartRaw.slice(dotIdx + 1) : "0");
68
+ const device = Number(rawDevice) || 0;
69
+ return ProtocolAddress.new(user, device);
70
+ }
71
+
72
+ const [userAndAgent = jid, rawDevicePart = ""] = userPartRaw.split(":");
73
+ const [user = jid, rawAgentPart = ""] = userAndAgent.split(".");
74
+ const signalUser = rawAgentPart ? `${user}_${rawAgentPart}` : user;
75
+ const device = rawDevicePart ? Number(rawDevicePart) : 0;
76
+ return ProtocolAddress.new(signalUser, device);
77
+ }
78
+
79
+ export function addressToJidKey(addr: ProtocolAddress): string {
80
+ return `${addr.name()}:${addr.deviceId()}`;
81
+ }
82
+
83
+ function legacyJidToAddress(jid: string): ProtocolAddress {
84
+ const [userPart] = jid.split("@");
85
+ const [userAndAgent = jid, rawDevicePart = ""] = (userPart ?? jid).split(":");
86
+ const [user = jid, rawAgentPart = ""] = userAndAgent.split(".");
87
+ const signalUser = rawAgentPart ? `${user}_${rawAgentPart}` : user;
88
+ const device = rawDevicePart ? Number(rawDevicePart) : 0;
89
+ return ProtocolAddress.new(signalUser, device);
90
+ }
91
+
92
+ function addressCandidatesForJid(jid: string): ProtocolAddress[] {
93
+ const candidates = [jidToAddress(jid), legacyJidToAddress(jid)];
94
+ const seen = new Set<string>();
95
+ return candidates.filter((addr) => {
96
+ const key = addr.toString();
97
+ if (seen.has(key)) return false;
98
+ seen.add(key);
99
+ return true;
100
+ });
101
+ }
102
+
103
+ function listSenderKeyDistributionIds(store: DeviceStore, senderAddr: ProtocolAddress): string[] {
104
+ const list = (store as any).listSenderKeyDistributionIds;
105
+ return typeof list === "function" ? list.call(store, senderAddr) : [];
106
+ }
107
+
108
+ // DM - X3DH session establish + Double Ratchet encrypt/decrypt
109
+
110
+ /** Establish an outgoing session with a new contact using their prekey bundle (X3DH). */
111
+ export async function establishSession(
112
+ store: DeviceStore,
113
+ recipient: ProtocolAddress,
114
+ rawBundle: RawPreKeyBundle,
115
+ ): Promise<void> {
116
+ const bundle = buildPreKeyBundle(rawBundle);
117
+ await processPreKeyBundle(bundle, recipient, store as any, store as any);
118
+ }
119
+
120
+ /** Encrypt plaintext for a DM recipient. */
121
+ export async function encryptDM(
122
+ store: DeviceStore,
123
+ recipient: ProtocolAddress,
124
+ selfAddress: ProtocolAddress,
125
+ plaintext: Uint8Array,
126
+ ): Promise<{ type: "msg" | "pkmsg"; ciphertext: Uint8Array }> {
127
+ const cipherMsg = await signalEncrypt(u8(plaintext), recipient, store as any, store as any);
128
+ const type = cipherMsg.type() === CiphertextMessageType.Whisper ? "msg" : "pkmsg";
129
+ return { type, ciphertext: cipherMsg.serialize() };
130
+ }
131
+
132
+ /** Decrypt a normal Signal message (not first-message). */
133
+ export async function decryptDM(
134
+ store: DeviceStore,
135
+ sender: ProtocolAddress,
136
+ ciphertext: Uint8Array,
137
+ ): Promise<Buffer> {
138
+ const msg = SignalMessage.deserialize(u8(ciphertext));
139
+ return Buffer.from(await signalDecrypt(msg, sender, store as any, store as any));
140
+ }
141
+
142
+ /** Decrypt a PreKeySignalMessage (first message from sender). */
143
+ export async function decryptDMPreKey(
144
+ store: DeviceStore,
145
+ sender: ProtocolAddress,
146
+ selfAddress: ProtocolAddress,
147
+ ciphertext: Uint8Array,
148
+ ): Promise<Buffer> {
149
+ const msg = PreKeySignalMessage.deserialize(u8(ciphertext));
150
+ return Buffer.from(
151
+ await signalDecryptPreKey(msg, sender, store as any, store as any, store as any, store as any, store as any)
152
+ );
153
+ }
154
+
155
+ // Group - Sender Key
156
+
157
+ /** Create or retrieve a SenderKeyDistributionMessage for the given group/sender. */
158
+ export async function createSenderKeyDistributionMessage(
159
+ store: DeviceStore,
160
+ groupJid: string,
161
+ senderJid: string,
162
+ ): Promise<{ skdm: SenderKeyDistributionMessage; distributionId: string }> {
163
+ const distributionId = randomUUID();
164
+ const senderAddr = jidToAddress(senderJid);
165
+ const skdm = await SenderKeyDistributionMessage.create(senderAddr, distributionId, store as any);
166
+ return { skdm, distributionId };
167
+ }
168
+
169
+ /**
170
+ * Process a received SenderKeyDistributionMessage from a group member.
171
+ * Handles both standard Signal and Facebook's signature-less variants.
172
+ */
173
+ export async function processSKDM(
174
+ store: DeviceStore,
175
+ senderJid: string,
176
+ skdmBytes: Uint8Array,
177
+ groupJid?: string,
178
+ ): Promise<void> {
179
+ const senderAddr = jidToAddress(senderJid);
180
+ const buf = u8(skdmBytes as Buffer);
181
+
182
+ const processAndAlias = async (skdm: SenderKeyDistributionMessage): Promise<void> => {
183
+ const distributionId = String(skdm.distributionId());
184
+ await processSenderKeyDistributionMessage(senderAddr, skdm, store as any);
185
+
186
+ // Some Facebook group SKMSG packets arrive without a usable distribution ID.
187
+ // decryptGroup() rewraps those packets with stableDistributionId(group, sender).
188
+ // Alias the freshly-processed SKDM record to that deterministic ID so the
189
+ // immediately-following group decrypt can find the sender key state.
190
+ if (groupJid) {
191
+ const stableId = stableDistributionId(groupJid, senderJid);
192
+ if (stableId !== distributionId) {
193
+ const record = await store.getSenderKey(senderAddr, distributionId);
194
+ if (record) {
195
+ await store.saveSenderKey(senderAddr, stableId, record);
196
+ logger.debug("signal-manager", `Aliased sender key ${distributionId} -> ${stableId} for ${senderJid} in ${groupJid}`);
197
+ }
198
+ }
199
+ }
200
+ };
201
+
202
+ // Try the standard libsignal format first. Facebook captures we have so far
203
+ // use the same 0x33-prefixed serialized message, so we should not invent our
204
+ // own distribution ID or signing key when the library can parse it directly.
205
+ try {
206
+ await processAndAlias(SenderKeyDistributionMessage.deserialize(buf));
207
+ return;
208
+ } catch (primaryErr) {
209
+ if (buf[0] === 0x33 && buf.length > 1) {
210
+ await processAndAlias(SenderKeyDistributionMessage.deserialize(buf.slice(1)));
211
+ return;
212
+ }
213
+ throw primaryErr;
214
+ }
215
+ }
216
+
217
+ /** Encrypt plaintext for a group using sender key. */
218
+ export async function encryptGroup(
219
+ store: DeviceStore,
220
+ groupJid: string,
221
+ senderJid: string,
222
+ plaintext: Uint8Array,
223
+ distributionId?: string,
224
+ ): Promise<Uint8Array> {
225
+ const activeDistributionId = distributionId ?? randomUUID();
226
+ const senderAddr = jidToAddress(senderJid);
227
+ const cipherMsg = await groupEncrypt(senderAddr, activeDistributionId, store as any, u8(plaintext));
228
+ const buf = u8(cipherMsg.serialize());
229
+ // If this is a Facebook-style SKMSG (0x33 prefix, signature-less), re-wrap into
230
+ // a signed Signal SKMSG compatible with libsignal group decrypt expectations.
231
+ if (buf && buf.length > 0 && buf[0] === 0x33) {
232
+ try {
233
+ // Try protobuf-style FB SKMSG first
234
+ const parsed = parseFBProtobufSKMSG(buf.slice(1));
235
+ if (parsed) {
236
+ const wrapped = wrapAsSignalSKMSG({ distributionId: activeDistributionId, id: parsed.id, iteration: parsed.iteration, ciphertext: parsed.ciphertext, senderJid });
237
+ return wrapped;
238
+ }
239
+ // Fallback: legacy binary FB SKMSG (distributionId(16) | id(4) | ct...)
240
+ if (buf.length >= 21 && buf[1] !== 0x08) {
241
+ const distId = uuidStringify(buf.slice(1, 17));
242
+ const id = buf.readUInt32BE(17);
243
+ const iteration = 0;
244
+ const ct = buf.slice(21);
245
+ const wrapped = wrapAsSignalSKMSG({ distributionId: distId, id, iteration, ciphertext: ct, senderJid });
246
+ return wrapped;
247
+ }
248
+ } catch (e) {
249
+ // fallback to raw buf
250
+ }
251
+ }
252
+ return buf;
253
+ }
254
+
255
+ /** Decrypt a group SenderKeyMessage. */
256
+ export async function decryptGroup(
257
+ store: DeviceStore,
258
+ senderJid: string,
259
+ ciphertext: Uint8Array,
260
+ groupJid?: string,
261
+ ): Promise<Buffer> {
262
+ const senderAddrs = addressCandidatesForJid(senderJid);
263
+ const senderAddr = senderAddrs[0]!;
264
+ let buf = u8(ciphertext);
265
+ let activeDistributionId: string | undefined;
266
+ let rewrapInfo: { id: number; iteration: number; ciphertext: Buffer } | null = null;
267
+
268
+ // If it's a Facebook-style message (version 0x33, usually lacking 64-byte signature)
269
+ if (buf[0] === 0x33) {
270
+ try {
271
+ const msg = SenderKeyMessage.deserialize(buf);
272
+ activeDistributionId = String(msg.distributionId());
273
+ // Already a valid Signal message
274
+ } catch (e) {
275
+ // Likely a Facebook signature-less message, needs re-encoding
276
+ logger.debug("signal-manager", `Re-encoding Facebook-style SKMSG from ${senderJid}`);
277
+
278
+ let id: number, iteration: number, ct: Buffer, distId: string;
279
+
280
+ if (buf.length >= 21 && buf[1] !== 0x08) {
281
+ // Legacy Binary
282
+ distId = uuidStringify(buf.slice(1, 17));
283
+ id = buf.readUInt32BE(17);
284
+ iteration = 0;
285
+ ct = buf.slice(21);
286
+ } else {
287
+ // Protobuf Style
288
+ const parsed = parseFBProtobufSKMSG(buf.slice(1));
289
+ if (!parsed) throw new Error("Failed to parse Facebook Protobuf SKMSG");
290
+ id = parsed.id;
291
+ iteration = parsed.iteration;
292
+ ct = parsed.ciphertext;
293
+ distId = stableDistributionId(groupJid || "unknown", senderJid);
294
+ }
295
+
296
+ activeDistributionId = distId;
297
+ rewrapInfo = { id, iteration, ciphertext: ct };
298
+ buf = wrapAsSignalSKMSG({ distributionId: distId, id, iteration, ciphertext: ct, senderJid });
299
+ }
300
+ }
301
+
302
+ try {
303
+ const result = await groupDecrypt(senderAddr, store as any, buf);
304
+ return Buffer.from(result);
305
+ } catch (primaryErr: any) {
306
+ const tried = new Set<string>([`${senderAddr.toString()}::${activeDistributionId ?? "original"}`]);
307
+
308
+ // Migration fallback: older builds used a different Messenger JID ->
309
+ // ProtocolAddress mapping (for example user.device@msgr became
310
+ // user_device.0). Try the same serialized SKMSG against those legacy
311
+ // sender-key namespaces before giving up.
312
+ for (const candidateAddr of senderAddrs.slice(1)) {
313
+ const attemptKey = `${candidateAddr.toString()}::${activeDistributionId ?? "original"}`;
314
+ if (tried.has(attemptKey)) continue;
315
+ tried.add(attemptKey);
316
+ try {
317
+ const result = await groupDecrypt(candidateAddr, store as any, buf);
318
+ logger.debug("signal-manager", `groupDecrypt succeeded with legacy sender address ${candidateAddr.toString()}`);
319
+ return Buffer.from(result);
320
+ } catch (candidateErr: any) {
321
+ logger.debug("signal-manager", `groupDecrypt fallback failed for ${attemptKey}: ${candidateErr.message}`);
322
+ }
323
+ }
324
+
325
+ // Facebook protobuf SKMSGs do not carry a distribution ID. The primary
326
+ // path uses a deterministic placeholder and processSKDM aliases fresh SKDMs
327
+ // to it, but existing stores may already have the right sender-key record
328
+ // under the real distribution ID. Rewrap the same ciphertext with each known
329
+ // distribution ID for this sender and let libsignal try the matching state.
330
+ if (rewrapInfo) {
331
+ for (const candidateAddr of senderAddrs) {
332
+ for (const distributionId of listSenderKeyDistributionIds(store, candidateAddr)) {
333
+ const attemptKey = `${candidateAddr.toString()}::${distributionId}`;
334
+ if (tried.has(attemptKey)) continue;
335
+ tried.add(attemptKey);
336
+
337
+ const candidateBuf = wrapAsSignalSKMSG({
338
+ distributionId,
339
+ id: rewrapInfo.id,
340
+ iteration: rewrapInfo.iteration,
341
+ ciphertext: rewrapInfo.ciphertext,
342
+ senderJid,
343
+ });
344
+
345
+ try {
346
+ const result = await groupDecrypt(candidateAddr, store as any, candidateBuf);
347
+ logger.debug("signal-manager", `groupDecrypt succeeded with sender key ${attemptKey}`);
348
+ return Buffer.from(result);
349
+ } catch (candidateErr: any) {
350
+ logger.debug("signal-manager", `groupDecrypt fallback failed for ${attemptKey}: ${candidateErr.message}`);
351
+ }
352
+ }
353
+ }
354
+ }
355
+
356
+ logger.error("signal-manager", `groupDecrypt failed: ${primaryErr.message} (op: ${primaryErr.operation})`);
357
+ throw primaryErr;
358
+ }
359
+ }
360
+
361
+ /** Check if a session exists for a given address. */
362
+ export async function hasSession(store: DeviceStore, address: ProtocolAddress): Promise<boolean> {
363
+ const record = await store.getSession(address);
364
+ return record != null;
365
+ }
@@ -0,0 +1,47 @@
1
+ import type { DeviceJSON } from "../../models/e2ee.ts";
2
+
3
+ export const DEVICE_STORE_SCHEMA_VERSION = 1;
4
+
5
+ export function parseDeviceJSON(json: string): DeviceJSON {
6
+ return migrateDeviceJSON(JSON.parse(json) as DeviceJSON);
7
+ }
8
+
9
+ export function migrateDeviceJSON(data: DeviceJSON): DeviceJSON {
10
+ return {
11
+ ...data,
12
+ schema_version: data.schema_version ?? DEVICE_STORE_SCHEMA_VERSION,
13
+ next_pre_key_id: data.next_pre_key_id ?? 1,
14
+ identities: data.identities ?? {},
15
+ sessions: data.sessions ?? {},
16
+ pre_keys: data.pre_keys ?? {},
17
+ sender_keys: data.sender_keys ?? {},
18
+ signed_pre_keys: data.signed_pre_keys ?? {},
19
+ };
20
+ }
21
+
22
+ export function decodeBase64(value: string): Buffer {
23
+ return Buffer.from(value, "base64");
24
+ }
25
+
26
+ export function encodeBase64(value: Buffer | Uint8Array): string {
27
+ return Buffer.from(value).toString("base64");
28
+ }
29
+
30
+ export function mapFromBase64Record<K extends string | number>(
31
+ record: Record<string, string> | undefined,
32
+ keyFromString: (key: string) => K,
33
+ ): Map<K, Uint8Array> {
34
+ const out = new Map<K, Uint8Array>();
35
+ for (const [key, value] of Object.entries(record ?? {})) {
36
+ out.set(keyFromString(key), Buffer.from(value, "base64"));
37
+ }
38
+ return out;
39
+ }
40
+
41
+ export function base64RecordFromMap<K extends string | number>(map: Map<K, Uint8Array>): Record<string, string> {
42
+ const out: Record<string, string> = {};
43
+ for (const [key, value] of map) {
44
+ out[String(key)] = encodeBase64(value);
45
+ }
46
+ return out;
47
+ }
@@ -0,0 +1,12 @@
1
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
+ import type { DeviceJSON } from "../../models/e2ee.ts";
3
+ import { parseDeviceJSON } from "./device-json.ts";
4
+
5
+ export function readDeviceJSONFile(path: string): DeviceJSON | null {
6
+ if (!existsSync(path)) return null;
7
+ return parseDeviceJSON(readFileSync(path, "utf8"));
8
+ }
9
+
10
+ export function writeDeviceJSONFile(path: string, data: string): void {
11
+ writeFileSync(path, data, { mode: 0o600 });
12
+ }