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,156 +0,0 @@
1
- import type { E2EEClient } from "./e2ee-client.ts";
2
- import type { OutboundMessageCache, RecentE2EEOutgoing } from "./outbound-message-cache.ts";
3
- import { parseMessengerJid, sameMessengerUser } from "./fanout-planner.ts";
4
- import type { FacebookE2EESocket } from "../transport/noise/noise-socket.ts";
5
- import { encodeNode, marshal, type Node } from "../transport/binary/wa-binary.ts";
6
- import type { RawPreKeyBundle } from "../../models/e2ee.ts";
7
- import { now, str } from "../../utils/fca-utils.ts";
8
- import { logger } from "../../utils/logger.ts";
9
-
10
- export interface E2EERetryManagerOptions {
11
- cache: OutboundMessageCache;
12
- getClient: () => E2EEClient;
13
- getSocket: () => FacebookE2EESocket | null;
14
- getSelfJid: () => string;
15
- getPreKeyBundle: (jid: string) => Promise<RawPreKeyBundle>;
16
- }
17
-
18
- /** Handles Messenger E2EE retry receipts using recent outbound encrypted payloads. */
19
- export class E2EERetryManager {
20
- constructor(private readonly opts: E2EERetryManagerOptions) {}
21
-
22
- async handleReceipt(node: Node): Promise<void> {
23
- const retryNode = this.findChild(node, "retry");
24
- const messageId = str(retryNode?.attrs?.id || node.attrs.id);
25
- if (!messageId) return;
26
-
27
- const cached = this.opts.cache.get(messageId);
28
- if (!cached) {
29
- logger.warn("E2EERetryManager", `Received retry receipt for unknown/out-of-cache E2EE message ${messageId}`);
30
- return;
31
- }
32
-
33
- const requesterJid = this.resolveRetryRequesterJid(node, cached);
34
- if (!requesterJid) {
35
- logger.warn("E2EERetryManager", `Cannot resolve retry requester for E2EE message ${messageId}`);
36
- return;
37
- }
38
-
39
- const retryCount = Number(retryNode?.attrs?.count ?? "1") || 1;
40
- if (retryCount >= 10) {
41
- logger.warn("E2EERetryManager", `Ignoring retry receipt #${retryCount} for ${messageId}`);
42
- return;
43
- }
44
-
45
- const e2eeClient = this.opts.getClient();
46
- const selfJid = this.opts.getSelfJid();
47
-
48
- const retryBundle = this.preKeyBundleFromRetryReceipt(node, requesterJid);
49
- if (retryBundle) {
50
- await e2eeClient.establishSession(requesterJid, retryBundle);
51
- } else if (!(await e2eeClient.hasSession(requesterJid))) {
52
- const bundle = await this.opts.getPreKeyBundle(requesterJid);
53
- await e2eeClient.establishSession(requesterJid, bundle);
54
- }
55
-
56
- const t = String(retryNode?.attrs?.t || node.attrs.t || Math.floor(now() / 1000));
57
- let encrypted: { type: "msg" | "pkmsg"; ciphertext: Buffer };
58
- const attrs: Record<string, string> = {
59
- to: cached.chatJid,
60
- type: cached.messageType,
61
- id: messageId,
62
- t,
63
- };
64
-
65
- if (cached.kind === "group") {
66
- attrs.participant = requesterJid;
67
- const skdm = await e2eeClient.createSenderKeyDistributionPayload(cached.chatJid, selfJid);
68
- encrypted = await e2eeClient.encryptMessageAppForDevice(requesterJid, selfJid, cached.messageApp, {
69
- skdm,
70
- backupDirective: { messageId, actionType: "UPSERT" },
71
- });
72
- } else {
73
- attrs.device_fanout = "false";
74
- if (node.attrs.participant) attrs.participant = node.attrs.participant;
75
- encrypted = await e2eeClient.encryptMessageAppForDevice(requesterJid, selfJid, cached.messageApp, {
76
- dsm: sameMessengerUser(requesterJid, selfJid) ? { destinationJid: cached.chatJid, phash: "" } : undefined,
77
- });
78
- }
79
-
80
- const msgNode = encodeNode("message", attrs, [
81
- encodeNode("enc", { v: "3", type: encrypted.type, count: String(retryCount) }, encrypted.ciphertext),
82
- encodeNode("franking", {}, [
83
- encodeNode("franking_tag", {}, cached.frankingTag),
84
- ]),
85
- ]);
86
-
87
- await this.opts.getSocket()?.sendFrame(marshal(msgNode));
88
- logger.info("E2EERetryManager", `Resent E2EE message ${messageId} for retry #${retryCount} to ${requesterJid}`);
89
- }
90
-
91
- private resolveRetryRequesterJid(node: Node, cached: RecentE2EEOutgoing): string {
92
- if (cached.kind === "group") {
93
- return str(node.attrs.participant || node.attrs.recipient);
94
- }
95
- return str(node.attrs.participant || node.attrs.from || node.attrs.recipient || cached.chatJid);
96
- }
97
-
98
- private preKeyBundleFromRetryReceipt(node: Node, jid: string): RawPreKeyBundle | null {
99
- const keysNode = this.findChild(node, "keys");
100
- if (!keysNode) return null;
101
-
102
- const registration = this.findChild(node, "registration")?.content;
103
- const identity = this.findChild(keysNode, "identity")?.content;
104
- const keyNode = this.findChild(keysNode, "key");
105
- const skeyNode = this.findChild(keysNode, "skey");
106
- const signedPreKeyId = this.findChild(skeyNode, "id")?.content;
107
- const signedPreKeyValue = this.findChild(skeyNode, "value")?.content;
108
- const signedPreKeySignature = this.findChild(skeyNode, "signature")?.content;
109
- const preKeyId = this.findChild(keyNode, "id")?.content;
110
- const preKeyValue = this.findChild(keyNode, "value")?.content;
111
-
112
- if (!Buffer.isBuffer(registration) || !Buffer.isBuffer(identity) || !Buffer.isBuffer(signedPreKeyValue) || !Buffer.isBuffer(signedPreKeySignature)) {
113
- return null;
114
- }
115
-
116
- const bundle: RawPreKeyBundle = {
117
- registrationId: registration.length === 4 ? registration.readUInt32BE(0) : 0,
118
- deviceId: parseMessengerJid(jid).device,
119
- identityKey: this.keyWithSignalPrefix(identity),
120
- signedPreKey: {
121
- keyId: this.readSignalKeyId(signedPreKeyId),
122
- publicKey: this.keyWithSignalPrefix(signedPreKeyValue),
123
- signature: signedPreKeySignature,
124
- },
125
- };
126
-
127
- if (Buffer.isBuffer(preKeyValue)) {
128
- bundle.preKey = {
129
- keyId: this.readSignalKeyId(preKeyId),
130
- publicKey: this.keyWithSignalPrefix(preKeyValue),
131
- };
132
- }
133
-
134
- return bundle;
135
- }
136
-
137
- private findChild(node: any, tag: string): any | null {
138
- if (!node) return null;
139
- if (node.tag === tag) return node;
140
- const children = Array.isArray(node.content) ? node.content : [];
141
- for (const child of children) {
142
- const found = this.findChild(child, tag);
143
- if (found) return found;
144
- }
145
- return null;
146
- }
147
-
148
- private keyWithSignalPrefix(value: Buffer): Buffer {
149
- return value.length === 32 ? Buffer.concat([Buffer.from([5]), value]) : value;
150
- }
151
-
152
- private readSignalKeyId(value: unknown): number {
153
- if (!Buffer.isBuffer(value) || value.length === 0) return 0;
154
- return value.readUIntBE(0, Math.min(value.length, 3));
155
- }
156
- }
@@ -1,230 +0,0 @@
1
- import { createHash } from 'crypto';
2
- import { PrivateKey, PublicKey } from '@signalapp/libsignal-client';
3
-
4
- /**
5
- * Facebook-specific Protocol Utilities
6
- *
7
- * This module contains helpers for handling Facebook's variant of the Signal protocol,
8
- * including varint encoding/decoding, UUID manipulation, and signature spoofing.
9
- */
10
-
11
- export interface FBProtobufSKMSG {
12
- id: number;
13
- iteration: number;
14
- ciphertext: Buffer;
15
- }
16
-
17
- export interface FBProtobufSKDM {
18
- chainId: number;
19
- iteration: number;
20
- chainKey: Buffer;
21
- signingPublicKey?: Buffer;
22
- }
23
-
24
- /**
25
- * Encodes a number into a varint (variable-length integer).
26
- * Uses arithmetic operations to safely handle large 32-bit unsigned integers.
27
- */
28
- export function encodeVarint(v: number): Buffer {
29
- const res: number[] = [];
30
- while (v >= 0x80) {
31
- res.push((v % 128) | 0x80);
32
- v = Math.floor(v / 128);
33
- }
34
- res.push(v);
35
- return Buffer.from(res);
36
- }
37
-
38
- /**
39
- * Decodes a varint from a buffer at a given position.
40
- */
41
- export function decodeVarint(buf: Buffer | Uint8Array, pos: number): { value: number; length: number } {
42
- let value = 0;
43
- let shift = 1;
44
- let length = 0;
45
- while (true) {
46
- const byte = buf[pos + length];
47
- if (byte === undefined) throw new Error("Unexpected EOF in varint");
48
- value += (byte & 0x7f) * shift;
49
- length++;
50
- if (!(byte & 0x80)) break;
51
- shift *= 128;
52
- if (length > 10) throw new Error("Varint too long");
53
- }
54
- return { value, length };
55
- }
56
-
57
- /**
58
- * Converts a 16-byte UUID buffer to a string.
59
- */
60
- export function uuidStringify(buf: Buffer | Uint8Array): string {
61
- const hex = Buffer.from(buf).toString('hex');
62
- return [
63
- hex.slice(0, 8),
64
- hex.slice(8, 12),
65
- hex.slice(12, 16),
66
- hex.slice(16, 20),
67
- hex.slice(20, 32)
68
- ].join('-');
69
- }
70
-
71
- /**
72
- * Parses a UUID string into a 16-byte buffer.
73
- */
74
- export function uuidParse(uuid: string): Buffer {
75
- return Buffer.from(uuid.replace(/-/g, ''), 'hex');
76
- }
77
-
78
- /**
79
- * Generates a deterministic Mock Private Key based on the sender's JID.
80
- * This is used to satisfy libsignal-client's signature requirements for FB messages.
81
- */
82
- export function getMockPrivateKey(jid: string): PrivateKey {
83
- const hash = createHash('sha256').update(`MOCK_SIG_KEY:${jid}`).digest();
84
- return PrivateKey.deserialize(hash);
85
- }
86
-
87
- /**
88
- * Derives a stable distributionId (UUID) from a group and sender JID.
89
- * Facebook often lacks the distributionId in SKDM packets.
90
- */
91
- export function stableDistributionId(groupJid: string, senderJid: string): string {
92
- const raw = `${groupJid}:${senderJid}`;
93
- const bytes = Buffer.alloc(16);
94
- for (let i = 0; i < raw.length && i < 16; i++) {
95
- bytes[i] = raw.charCodeAt(i) & 0xff;
96
- }
97
- // Set UUID version 4 markers (though it's not a real v4 UUID)
98
- bytes[6] = (bytes[6]! & 0x0f) | 0x40;
99
- bytes[8] = (bytes[8]! & 0x3f) | 0x80;
100
- return uuidStringify(bytes);
101
- }
102
-
103
- /**
104
- * Parses a Facebook-style Protobuf SKMSG.
105
- */
106
- export function parseFBProtobufSKMSG(buf: Buffer): FBProtobufSKMSG | null {
107
- let pos = 0;
108
- let id = 0;
109
- let iteration = 0;
110
- let ciphertext = Buffer.alloc(0);
111
-
112
- try {
113
- while (pos < buf.length) {
114
- const { value: tagValue, length: tagLen } = decodeVarint(buf, pos);
115
- pos += tagLen;
116
- const field = tagValue >> 3;
117
-
118
- if (field === 1) { // id
119
- const { value, length } = decodeVarint(buf, pos);
120
- id = value;
121
- pos += length;
122
- } else if (field === 2) { // iteration
123
- const { value, length } = decodeVarint(buf, pos);
124
- iteration = value;
125
- pos += length;
126
- } else if (field === 3) { // ciphertext
127
- const { value, length } = decodeVarint(buf, pos);
128
- pos += length;
129
- ciphertext = buf.slice(pos, pos + value);
130
- pos += value;
131
- } else {
132
- // Skip unknown
133
- const tag = tagValue & 0x07;
134
- if (tag === 0) {
135
- const { length } = decodeVarint(buf, pos);
136
- pos += length;
137
- } else if (tag === 2) {
138
- const { value, length } = decodeVarint(buf, pos);
139
- pos += length + value;
140
- } else break;
141
- }
142
- }
143
- return { id, iteration, ciphertext };
144
- } catch (e) {
145
- return null;
146
- }
147
- }
148
-
149
- /**
150
- * Parses a Facebook-style Protobuf SKDM.
151
- */
152
- export function parseFBProtobufSKDM(buf: Buffer): FBProtobufSKDM | null {
153
- let pos = 0;
154
- let chainId = 0;
155
- let iteration = 0;
156
- let chainKey = Buffer.alloc(0);
157
- let signingPublicKey: Buffer | undefined;
158
-
159
- try {
160
- while (pos < buf.length) {
161
- const { value: tagValue, length: tagLen } = decodeVarint(buf, pos);
162
- pos += tagLen;
163
- const field = tagValue >> 3;
164
-
165
- if (field === 1) {
166
- const { value, length } = decodeVarint(buf, pos);
167
- chainId = value;
168
- pos += length;
169
- } else if (field === 2) {
170
- const { value, length } = decodeVarint(buf, pos);
171
- iteration = value;
172
- pos += length;
173
- } else if (field === 3) {
174
- const { value, length } = decodeVarint(buf, pos);
175
- pos += length;
176
- chainKey = buf.slice(pos, pos + value);
177
- pos += value;
178
- } else if (field === 4) {
179
- const { value, length } = decodeVarint(buf, pos);
180
- pos += length;
181
- signingPublicKey = buf.slice(pos, pos + value);
182
- pos += value;
183
- } else {
184
- const tag = tagValue & 0x07;
185
- if (tag === 0) {
186
- const { length } = decodeVarint(buf, pos);
187
- pos += length;
188
- } else if (tag === 2) {
189
- const { value, length } = decodeVarint(buf, pos);
190
- pos += length + value;
191
- } else break;
192
- }
193
- }
194
- return { chainId, iteration, chainKey, signingPublicKey };
195
- } catch (e) {
196
- return null;
197
- }
198
- }
199
-
200
- /**
201
- * Re-encodes a Facebook-style SenderKeyMessage into a standard Signal Protobuf.
202
- * FB messages often lack a signature and use a different tag structure.
203
- */
204
- export function wrapAsSignalSKMSG(params: {
205
- distributionId: string;
206
- id: number;
207
- iteration: number;
208
- ciphertext: Buffer;
209
- senderJid: string;
210
- }): Buffer {
211
- const { distributionId, id, iteration, ciphertext, senderJid } = params;
212
-
213
- const uuidBuf = uuidParse(distributionId);
214
- const protoChunks = [
215
- Buffer.from([0x0a, 0x10]), uuidBuf, // Tag 1 (distributionId)
216
- Buffer.from([0x10]), encodeVarint(id), // Tag 2 (id)
217
- Buffer.from([0x18]), encodeVarint(iteration), // Tag 3 (iteration)
218
- Buffer.from([0x22]), encodeVarint(ciphertext.length), ciphertext, // Tag 4 (ct)
219
- ];
220
-
221
- const protobuf = Buffer.concat(protoChunks);
222
- const header = Buffer.from([0x33]); // Protocol version
223
- const toSign = Buffer.concat([header, protobuf]);
224
-
225
- // Sign using our deterministic Mock Key
226
- const privKey = getMockPrivateKey(senderJid);
227
- const signature = privKey.sign(toSign);
228
-
229
- return Buffer.concat([toSign, signature]);
230
- }
@@ -1,31 +0,0 @@
1
- import { ProtoWriter } from "../message/message-builder.ts";
2
-
3
- export interface ICDCIdentityListOptions {
4
- seq: number;
5
- timestamp: number;
6
- devices: Uint8Array[];
7
- signingDeviceIndex: number;
8
- }
9
-
10
- export function encodeICDCIdentityList(opts: ICDCIdentityListOptions): Buffer {
11
- const w = new ProtoWriter();
12
- w.varint(1, opts.seq);
13
- w.uint64_varint(2, BigInt(opts.timestamp));
14
- for (const device of opts.devices) {
15
- w.bytes(3, Buffer.from(device));
16
- }
17
- w.varint(4, opts.signingDeviceIndex);
18
- return w.build();
19
- }
20
-
21
- export interface SignedICDCIdentityListOptions {
22
- details: Buffer;
23
- signature: Buffer;
24
- }
25
-
26
- export function encodeSignedICDCIdentityList(opts: SignedICDCIdentityListOptions): Buffer {
27
- const w = new ProtoWriter();
28
- w.bytes(1, opts.details);
29
- w.bytes(2, opts.signature);
30
- return w.build();
31
- }
package/src/e2ee/index.ts DELETED
@@ -1,76 +0,0 @@
1
- /**
2
- * Public API for the E2EE module.
3
- * Import from here - don't import internal files directly.
4
- */
5
-
6
- // Client orchestrator
7
- export { E2EEClient } from "./application/e2ee-client.ts";
8
- export type {
9
- E2EESendTextOptions,
10
- E2EESendTextResult,
11
- E2EEEncryptMediaOptions,
12
- E2EEEncryptMediaResult,
13
- E2EEDecryptMediaOptions,
14
- } from "./application/e2ee-client.ts";
15
-
16
- // Device store
17
- export { DeviceStore } from "./store/device-store.ts";
18
- export type { DeviceJSON, NoiseKeyPair } from "./store/device-store.ts";
19
-
20
- // Media crypto (can be used standalone)
21
- export { encryptMedia, decryptMedia, expandMediaKey, sha256, MediaType, MmsType } from "./media/media-crypto.ts";
22
- export type { MediaTypeKey, MediaKeys, EncryptMediaResult, DecryptMediaOptions } from "./media/media-crypto.ts";
23
-
24
- // Media upload
25
- export { uploadMedia } from "./media/media-upload.ts";
26
- export type { MediaUploadConfig, MediaUploadResult, MmsTypeStr } from "./media/media-upload.ts";
27
-
28
- // Signal manager (lower level)
29
- export {
30
- jidToAddress,
31
- addressToJidKey,
32
- establishSession,
33
- encryptDM,
34
- decryptDM,
35
- decryptDMPreKey,
36
- encryptGroup,
37
- decryptGroup,
38
- createSenderKeyDistributionMessage,
39
- processSKDM,
40
- hasSession,
41
- } from "./signal/signal-manager.ts";
42
-
43
- // PreKey manager
44
- export {
45
- generatePreKeys,
46
- generateSignedPreKey,
47
- buildPreKeyUploadPayload,
48
- buildPreKeyBundle,
49
- INITIAL_PREKEY_COUNT,
50
- WANTED_PREKEY_COUNT,
51
- MIN_PREKEY_COUNT,
52
- } from "./signal/prekey-manager.ts";
53
- export type { GeneratedPreKey, PreKeyUploadPayload, RawPreKeyBundle } from "./signal/prekey-manager.ts";
54
-
55
- // Message builder
56
- export {
57
- encodeTextMessage,
58
- encodeImageMessage,
59
- encodeVideoMessage,
60
- encodeAudioMessage,
61
- encodeDocumentMessage,
62
- encodeStickerMessage,
63
- encodeReactionMessage,
64
- encodeEditMessage,
65
- encodeRevokeMessage,
66
- encodeMessageApplication,
67
- encodeMessageTransport,
68
- FB_MESSAGE_VERSION,
69
- FB_MESSAGE_APPLICATION_VERSION,
70
- FB_CONSUMER_MESSAGE_VERSION,
71
- } from "./message/message-builder.ts";
72
- export type { MediaFields, MessageTransportOptions } from "./message/message-builder.ts";
73
-
74
- // Noise handshake
75
- export { doHandshake, WA_CERT_PUB_KEY, WA_HEADER } from "./transport/noise/noise-handshake.ts";
76
- export type { NoiseSocket, RawWebSocket, HandshakeResult } from "./transport/noise/noise-handshake.ts";
@@ -1,27 +0,0 @@
1
- /**
2
- * Internal/dev barrel for E2EE implementation details.
3
- *
4
- * Public consumers should prefer `src/e2ee/index.ts` or the package root.
5
- * This file intentionally exposes lower-level protocol modules for tests,
6
- * debugging, and advanced integrations.
7
- */
8
-
9
- export * from "./application/e2ee-client.ts";
10
- export * from "./application/fanout-planner.ts";
11
- export * from "./application/outbound-message-cache.ts";
12
- export * from "./application/prekey-maintenance.ts";
13
- export * from "./application/retry-manager.ts";
14
- export * from "./store/device-json.ts";
15
- export * from "./store/device-repository.ts";
16
- export * from "./store/device-store.ts";
17
- export * from "./transport/binary/wa-binary.ts";
18
- export * from "./transport/dgw/dgw-socket.ts";
19
- export * from "./transport/noise/noise-handshake.ts";
20
- export * from "./transport/noise/noise-socket.ts";
21
- export * from "./signal/prekey-manager.ts";
22
- export * from "./signal/signal-manager.ts";
23
- export * from "./message/message-builder.ts";
24
- export * from "./media/media-crypto.ts";
25
- export * from "./media/media-upload.ts";
26
- export * from "./facebook/facebook-protocol-utils.ts";
27
- export { encodeICDCIdentityList as encodeFacebookICDCIdentityList, encodeSignedICDCIdentityList as encodeFacebookSignedICDCIdentityList } from "./facebook/icdc-payload.ts";
@@ -1,147 +0,0 @@
1
- /**
2
- * E2EE Media Crypto - Layer 4
3
- *
4
- * Implements the AES-256-CBC + HMAC-SHA256 + HKDF media scheme.
5
- *
6
- * No Signal Protocol needed here - this is pure symmetric crypto.
7
- */
8
-
9
- import { createCipheriv, createDecipheriv, createHmac, hkdfSync, randomBytes, createHash } from "node:crypto";
10
- import type { DecryptMediaOptions, EncryptMediaResult, MediaKeys } from "../../models/media.ts";
11
- export type { DecryptMediaOptions, EncryptMediaResult, MediaKeys };
12
-
13
- // Media type -> HKDF info string
14
-
15
- export const MediaType = {
16
- image: "WhatsApp Image Keys",
17
- video: "WhatsApp Video Keys",
18
- audio: "WhatsApp Audio Keys",
19
- document: "WhatsApp Document Keys",
20
- sticker: "WhatsApp Image Keys", // same as image
21
- history: "WhatsApp History Keys",
22
- appstate: "WhatsApp App State Keys",
23
- } as const;
24
-
25
- export type MediaTypeKey = keyof typeof MediaType;
26
-
27
- // MMS type strings used in upload/download URLs
28
- export const MmsType: Record<MediaTypeKey, string> = {
29
- image: "image",
30
- video: "video",
31
- audio: "ptt", // Messenger only allows PTT (push-to-talk), not generic audio
32
- document: "document",
33
- sticker: "image",
34
- history: "md-msg-hist",
35
- appstate: "md-app-state",
36
- };
37
-
38
- // Key derivation
39
-
40
-
41
- /**
42
- * HKDF-SHA256 expand of mediaKey into iv + cipherKey + macKey + refKey (112 bytes total).
43
- */
44
- export function expandMediaKey(mediaKey: Buffer, type: MediaTypeKey): MediaKeys {
45
- const info = MediaType[type];
46
- const expanded = Buffer.from(
47
- hkdfSync("sha256", mediaKey, Buffer.alloc(0), info, 112),
48
- );
49
- return {
50
- iv: expanded.subarray(0, 16),
51
- cipherKey: expanded.subarray(16, 48),
52
- macKey: expanded.subarray(48, 80),
53
- refKey: expanded.subarray(80, 112),
54
- };
55
- }
56
-
57
- // Encrypt
58
-
59
-
60
- /**
61
- * Encrypt media for upload.
62
- */
63
- export function encryptMedia(plaintext: Buffer, type: MediaTypeKey): EncryptMediaResult {
64
- const mediaKey = randomBytes(32);
65
- const { iv, cipherKey, macKey } = expandMediaKey(mediaKey, type);
66
-
67
- // AES-256-CBC
68
- const cipher = createCipheriv("aes-256-cbc", cipherKey, iv);
69
- const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
70
-
71
- // HMAC-SHA256(macKey, iv ‖ ciphertext), take first 10 bytes
72
- const mac = createHmac("sha256", macKey)
73
- .update(iv)
74
- .update(ciphertext)
75
- .digest()
76
- .subarray(0, 10);
77
-
78
- const dataToUpload = Buffer.concat([ciphertext, mac]);
79
-
80
- return {
81
- mediaKey,
82
- fileSHA256: Buffer.from(createHash("sha256").update(plaintext).digest()),
83
- fileEncSHA256: Buffer.from(createHash("sha256").update(dataToUpload).digest()),
84
- fileLength: plaintext.length,
85
- dataToUpload,
86
- };
87
- }
88
-
89
- // Decrypt
90
-
91
- const MEDIA_MAC_LENGTH = 10;
92
-
93
- /**
94
- * Decrypt downloaded E2EE media.
95
- */
96
- export function decryptMedia(opts: DecryptMediaOptions): Buffer {
97
- const { data, mediaKey, type, fileSHA256: expectedFileSHA256, fileEncSHA256: expectedFileEncSHA256 } = opts;
98
-
99
- if (data.length <= MEDIA_MAC_LENGTH) {
100
- throw new Error(`Media data too short (${data.length} bytes)`);
101
- }
102
-
103
- // Optional: verify encrypted SHA256 (checksum of entire downloaded blob)
104
- if (expectedFileEncSHA256) {
105
- const actual = createHash("sha256").update(data).digest();
106
- if (!actual.equals(expectedFileEncSHA256)) {
107
- throw new Error("Invalid media enc SHA256 - data corrupted or tampered");
108
- }
109
- }
110
-
111
- const ciphertext = data.subarray(0, -MEDIA_MAC_LENGTH);
112
- const mac = data.subarray(-MEDIA_MAC_LENGTH);
113
-
114
- const { iv, cipherKey, macKey } = expandMediaKey(mediaKey, type);
115
-
116
- // Verify HMAC
117
- const expectedMac = createHmac("sha256", macKey)
118
- .update(iv)
119
- .update(ciphertext)
120
- .digest()
121
- .subarray(0, MEDIA_MAC_LENGTH);
122
-
123
- if (!expectedMac.equals(mac)) {
124
- throw new Error("Invalid media HMAC - data corrupted or wrong key");
125
- }
126
-
127
- // AES-256-CBC decrypt
128
- const decipher = createDecipheriv("aes-256-cbc", cipherKey, iv);
129
- const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
130
-
131
- // Optional: verify plaintext SHA256
132
- if (expectedFileSHA256) {
133
- const actual = createHash("sha256").update(plaintext).digest();
134
- if (!actual.equals(expectedFileSHA256)) {
135
- throw new Error("Invalid media SHA256 - file corrupted after decryption");
136
- }
137
- }
138
-
139
- return plaintext;
140
- }
141
-
142
- // Helpers
143
-
144
- /** SHA256 helper */
145
- export function sha256(data: Buffer): Buffer {
146
- return Buffer.from(createHash("sha256").update(data).digest());
147
- }