fb-messenger-e2ee 0.1.0 → 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 +34 -6
- 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,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";
|
package/src/e2ee/internal.ts
DELETED
|
@@ -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
|
-
}
|