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.
Files changed (110) hide show
  1. package/README.md +27 -2
  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,101 +0,0 @@
1
- import * as protobuf from "protobufjs";
2
- import { fileURLToPath } from "node:url";
3
- import { dirname, join } from "node:path";
4
-
5
- // Protobuf Decoders (using protobufjs for simplicity)
6
-
7
- const __filename = fileURLToPath(import.meta.url);
8
- const __dirname = dirname(__filename);
9
-
10
- const root = new ((protobuf as any).default?.Root || protobuf.Root)();
11
- root.resolvePath = (origin: string, target: string) => {
12
- return join(__dirname, "../proto", target);
13
- };
14
- root.loadSync([
15
- "WACommon.proto",
16
- "MessageTransport.proto",
17
- "WAMediaTransport.proto",
18
- "MessageApplication.proto",
19
- "ConsumerApplication.proto",
20
- "ArmadilloApplication.proto",
21
- "ArmadilloICDC.proto"
22
- ]);
23
-
24
- const MsgTransportType = root.lookupType("waMsgTransport.MessageTransport");
25
- const MsgApplicationType = root.lookupType("WAMsgApplication.MessageApplication");
26
- const ConsumerAppType = root.lookupType("waConsumerApplication.ConsumerApplication");
27
- const ArmadilloAppType = root.lookupType("waArmadilloApplication.Armadillo");
28
- const ICDCIdentityListType = root.lookupType("waArmadilloICDC.ICDCIdentityList");
29
- const SignedICDCIdentityListType = root.lookupType("waArmadilloICDC.SignedICDCIdentityList");
30
- const ImageTransportType = root.lookupType("WAMediaTransport.ImageTransport");
31
- const VideoTransportType = root.lookupType("WAMediaTransport.VideoTransport");
32
- const AudioTransportType = root.lookupType("WAMediaTransport.AudioTransport");
33
- const DocumentTransportType = root.lookupType("WAMediaTransport.DocumentTransport");
34
- const StickerTransportType = root.lookupType("WAMediaTransport.StickerTransport");
35
-
36
- export function decodeMessageTransport(buffer: Buffer): any {
37
- const msg = MsgTransportType.decode(buffer);
38
- return MsgTransportType.toObject(msg, { longs: Number, enums: String, bytes: Buffer });
39
- }
40
-
41
- export function decodeMessageApplication(buffer: Buffer): any {
42
- const msg = MsgApplicationType.decode(buffer);
43
- return MsgApplicationType.toObject(msg, { longs: Number, enums: String, bytes: Buffer });
44
- }
45
-
46
- export function decodeConsumerApplication(buffer: Buffer): any {
47
- const msg = ConsumerAppType.decode(buffer);
48
- return ConsumerAppType.toObject(msg, { longs: String, enums: String, bytes: Buffer });
49
- }
50
-
51
- export function decodeArmadillo(buffer: Buffer): any {
52
- const msg = ArmadilloAppType.decode(buffer);
53
- return ArmadilloAppType.toObject(msg, { longs: String, enums: String, bytes: Buffer });
54
- }
55
-
56
- export function decodeImageTransport(buffer: Buffer): any {
57
- const msg = ImageTransportType.decode(buffer);
58
- return ImageTransportType.toObject(msg, { longs: Number, enums: String, bytes: Buffer });
59
- }
60
-
61
- export function decodeVideoTransport(buffer: Buffer): any {
62
- const msg = VideoTransportType.decode(buffer);
63
- return VideoTransportType.toObject(msg, { longs: Number, enums: String, bytes: Buffer });
64
- }
65
-
66
- export function decodeAudioTransport(buffer: Buffer): any {
67
- const msg = AudioTransportType.decode(buffer);
68
- return AudioTransportType.toObject(msg, { longs: Number, enums: String, bytes: Buffer });
69
- }
70
-
71
- export function decodeDocumentTransport(buffer: Buffer): any {
72
- const msg = DocumentTransportType.decode(buffer);
73
- return DocumentTransportType.toObject(msg, { longs: Number, enums: String, bytes: Buffer });
74
- }
75
-
76
- export function decodeStickerTransport(buffer: Buffer): any {
77
- const msg = StickerTransportType.decode(buffer);
78
- return StickerTransportType.toObject(msg, { longs: Number, enums: String, bytes: Buffer });
79
- }
80
-
81
- export function encodeICDCIdentityList(data: {
82
- seq: number;
83
- timestamp: number;
84
- devices: Buffer[];
85
- signingDeviceIndex: number;
86
- }): Buffer {
87
- const msg = ICDCIdentityListType.create(data);
88
- return Buffer.from(ICDCIdentityListType.encode(msg).finish());
89
- }
90
-
91
- export function encodeSignedICDCIdentityList(data: {
92
- details: Buffer;
93
- signature: Buffer;
94
- }): Buffer {
95
- const msg = SignedICDCIdentityListType.create(data);
96
- return Buffer.from(SignedICDCIdentityListType.encode(msg).finish());
97
- }
98
-
99
- export function decodeICDCFetchResponse(buffer: Buffer): any {
100
- // Not a proto, but we might need it
101
- }
@@ -1,4 +0,0 @@
1
- export const FB_MESSAGE_VERSION = 3;
2
- export const FB_MESSAGE_APPLICATION_VERSION = 2;
3
- export const FB_CONSUMER_MESSAGE_VERSION = 1;
4
- export const FB_ARMADILLO_MESSAGE_VERSION = 1;
@@ -1,15 +0,0 @@
1
- /**
2
- * Message layer public barrel.
3
- *
4
- * Keep imports here stable while implementations live under proto/, builders/,
5
- * and codecs/. The top-level compatibility shim re-exports this barrel.
6
- */
7
-
8
- export type { MediaFields, MessageTransportOptions } from "../../models/e2ee.ts";
9
- export * from "./constants.ts";
10
- export { ProtoWriter } from "./proto/proto-writer.ts";
11
- export * from "./builders/client-payload.ts";
12
- export * from "./builders/consumer-application.ts";
13
- export * from "./builders/message-application.ts";
14
- export * from "./builders/message-transport.ts";
15
- export * from "./codecs/protobuf-codecs.ts";
@@ -1,140 +0,0 @@
1
- /**
2
- * E2EE PreKey Manager - Layer 2 (Signal Protocol)
3
- *
4
- * Manages prekey generation and the IQ stanzas needed to:
5
- * - Upload prekeys to the WhatsApp/Messenger server on registration
6
- * - Fetch prekey bundles for recipients before establishing sessions
7
- *
8
- */
9
-
10
- import {
11
- PrivateKey,
12
- PreKeyRecord,
13
- SignedPreKeyRecord,
14
- PreKeyBundle,
15
- KEMKeyPair,
16
- KEMPublicKey,
17
- PublicKey,
18
- } from "@signalapp/libsignal-client";
19
- import type { DeviceStore } from "../store/device-store.ts";
20
- import { type GeneratedPreKey, type PreKeyUploadPayload, type RawPreKeyBundle } from "../../models/e2ee.ts";
21
- export type { GeneratedPreKey, PreKeyUploadPayload, RawPreKeyBundle };
22
-
23
- export const INITIAL_PREKEY_COUNT = 812; // First registration batch
24
- export const WANTED_PREKEY_COUNT = 50; // Normal replenishment batch
25
- export const MIN_PREKEY_COUNT = 5; // Trigger replenishment below this
26
-
27
- // Cast Uint8Array -> Buffer for strict libsignal params
28
- const u8 = (b: Uint8Array | Buffer): Buffer => Buffer.isBuffer(b) ? b : Buffer.from(b.buffer, b.byteOffset, b.byteLength);
29
-
30
-
31
- /** Generate `count` fresh one-time prekeys and store them in the DeviceStore. */
32
- export async function generatePreKeys(
33
- store: DeviceStore,
34
- count: number = WANTED_PREKEY_COUNT,
35
- ): Promise<GeneratedPreKey[]> {
36
- const result: GeneratedPreKey[] = [];
37
- for (let i = 0; i < count; i++) {
38
- const id = store.allocPreKeyId();
39
- const priv = PrivateKey.generate();
40
- const record = PreKeyRecord.new(id, priv.getPublicKey(), priv);
41
- await store.savePreKey(id, record);
42
- result.push({ id, record });
43
- }
44
- return result;
45
- }
46
-
47
- /** Generate or refresh the signed prekey and store it. */
48
- export async function generateSignedPreKey(store: DeviceStore): Promise<SignedPreKeyRecord> {
49
- const identityPriv = await store.getIdentityKey();
50
- const priv = PrivateKey.generate();
51
- const pub = priv.getPublicKey();
52
- const sig = identityPriv.sign(pub.serialize());
53
- const id = store.signedPreKeyId + 1;
54
- const record = SignedPreKeyRecord.new(id, Date.now(), pub, priv, u8(sig));
55
- await store.saveSignedPreKey(id, record);
56
- return record;
57
- }
58
-
59
-
60
- export async function buildPreKeyUploadPayload(
61
- store: DeviceStore,
62
- preKeys: GeneratedPreKey[],
63
- ): Promise<PreKeyUploadPayload> {
64
- const idPair = await store.getIdentityKeyPair();
65
- const sPreKey = await store.getSignedPreKey(store.signedPreKeyId);
66
-
67
- return {
68
- registrationId: store.registrationId,
69
- // Use serialize() (33-byte DER) for consistency - server accepts both formats
70
- // getPublicKeyBytes() returns 32-byte raw; serialize() returns 33-byte compressed DER
71
- identityKey: idPair.publicKey.serialize(),
72
- signedPreKey: {
73
- keyId: sPreKey.id(),
74
- publicKey: sPreKey.publicKey().serialize(),
75
- signature: sPreKey.signature(),
76
- },
77
- preKeys: preKeys.map(pk => ({
78
- keyId: pk.id,
79
- publicKey: pk.record.publicKey().serialize(),
80
- })),
81
- };
82
- }
83
-
84
-
85
- export function buildPreKeyBundle(raw: RawPreKeyBundle): PreKeyBundle {
86
- const identityKey = PublicKey_deserialize(raw.identityKey);
87
- const spkPub = PublicKey_deserialize(raw.signedPreKey.publicKey);
88
-
89
- // Messenger doesn't use Kyber PQ prekeys, but PreKeyBundle.new() requires
90
- // real Kyber objects with a valid signature.
91
- // If the raw bundle includes a pre-computed Kyber field, use it directly.
92
- // Otherwise generate a dummy Kyber key and sign it with a new ephemeral identity
93
- // key just to satisfy the API. This is safe for Messenger since the server
94
- // never sends Kyber material and the recipient won't verify it.
95
- let kyberPub: import("@signalapp/libsignal-client").KEMPublicKey | null = null;
96
- let kyberSig: Uint8Array | null = null;
97
- let kyberKeyId: number | null = null;
98
-
99
- if (raw.kyberPreKey) {
100
- kyberPub = KEMPublicKey.deserialize(u8(raw.kyberPreKey.publicKey));
101
- kyberSig = u8(raw.kyberPreKey.signature);
102
- kyberKeyId = raw.kyberPreKey.keyId;
103
- }
104
-
105
- if (raw.preKey) {
106
- const pkPub = PublicKey_deserialize(raw.preKey.publicKey);
107
- return PreKeyBundle.new(
108
- raw.registrationId,
109
- raw.deviceId,
110
- raw.preKey.keyId,
111
- pkPub,
112
- raw.signedPreKey.keyId,
113
- spkPub,
114
- u8(raw.signedPreKey.signature),
115
- identityKey,
116
- kyberKeyId as any,
117
- kyberPub as any,
118
- kyberSig as any,
119
- );
120
- }
121
-
122
- return PreKeyBundle.new(
123
- raw.registrationId,
124
- raw.deviceId,
125
- null,
126
- null,
127
- raw.signedPreKey.keyId,
128
- spkPub,
129
- u8(raw.signedPreKey.signature),
130
- identityKey,
131
- kyberKeyId as any,
132
- kyberPub as any,
133
- kyberSig as any,
134
- );
135
- }
136
-
137
- // Sync PublicKey deserialize helper (libsignal 0.92 is sync)
138
- function PublicKey_deserialize(bytes: Uint8Array): PublicKey {
139
- return PublicKey.deserialize(u8(bytes));
140
- }
@@ -1,365 +0,0 @@
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
- }
@@ -1,47 +0,0 @@
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
- }
@@ -1,12 +0,0 @@
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
- }