@ryuu-reinzz/baileys 3.0.0-beta.2 → 3.0.0-beta.21
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/LICENSE +1 -1
- package/README.md +6 -0
- package/WAProto/fix-imports.js +70 -18
- package/WAProto/index.js +197 -160
- package/lib/Defaults/index.js +17 -4
- package/lib/Signal/libsignal.js +63 -2
- package/lib/Signal/lid-mapping.js +170 -70
- package/lib/Socket/Client/websocket.js +5 -1
- package/lib/Socket/business.js +11 -8
- package/lib/Socket/chats.js +55 -28
- package/lib/Socket/index.js +0 -6
- package/lib/Socket/messages-recv.js +152 -75
- package/lib/Socket/messages-send.js +230 -148
- package/lib/Socket/socket.js +69 -15
- package/lib/Utils/auth-utils.js +53 -20
- package/lib/Utils/chat-utils.js +100 -51
- package/lib/Utils/crypto.js +2 -26
- package/lib/Utils/event-buffer.js +33 -7
- package/lib/Utils/generics.js +4 -1
- package/lib/Utils/history.js +46 -5
- package/lib/Utils/identity-change-handler.js +49 -0
- package/lib/Utils/index.js +2 -0
- package/lib/Utils/lt-hash.js +2 -42
- package/lib/Utils/make-mutex.js +20 -27
- package/lib/Utils/message-retry-manager.js +58 -5
- package/lib/Utils/messages-media.js +151 -40
- package/lib/Utils/messages.js +43 -23
- package/lib/Utils/noise-handler.js +139 -85
- package/lib/Utils/process-message.js +57 -14
- package/lib/Utils/reporting-utils.js +258 -0
- package/lib/Utils/sync-action-utils.js +48 -0
- package/lib/Utils/tc-token-utils.js +18 -0
- package/lib/Utils/use-sqlite-auth-state.js +122 -0
- package/lib/WABinary/decode.js +24 -0
- package/lib/WABinary/encode.js +5 -1
- package/lib/WABinary/generic-utils.js +19 -8
- package/package.json +7 -2
|
@@ -3,69 +3,129 @@ import { proto } from '../../WAProto/index.js';
|
|
|
3
3
|
import { NOISE_MODE, WA_CERT_DETAILS } from '../Defaults/index.js';
|
|
4
4
|
import { decodeBinaryNode } from '../WABinary/index.js';
|
|
5
5
|
import { aesDecryptGCM, aesEncryptGCM, Curve, hkdf, sha256 } from './crypto.js';
|
|
6
|
+
const IV_LENGTH = 12;
|
|
7
|
+
const EMPTY_BUFFER = Buffer.alloc(0);
|
|
6
8
|
const generateIV = (counter) => {
|
|
7
|
-
const iv = new ArrayBuffer(
|
|
9
|
+
const iv = new ArrayBuffer(IV_LENGTH);
|
|
8
10
|
new DataView(iv).setUint32(8, counter);
|
|
9
11
|
return new Uint8Array(iv);
|
|
10
12
|
};
|
|
13
|
+
class TransportState {
|
|
14
|
+
constructor(encKey, decKey) {
|
|
15
|
+
this.encKey = encKey;
|
|
16
|
+
this.decKey = decKey;
|
|
17
|
+
this.readCounter = 0;
|
|
18
|
+
this.writeCounter = 0;
|
|
19
|
+
this.iv = new Uint8Array(IV_LENGTH);
|
|
20
|
+
}
|
|
21
|
+
encrypt(plaintext) {
|
|
22
|
+
const c = this.writeCounter++;
|
|
23
|
+
this.iv[8] = (c >>> 24) & 0xff;
|
|
24
|
+
this.iv[9] = (c >>> 16) & 0xff;
|
|
25
|
+
this.iv[10] = (c >>> 8) & 0xff;
|
|
26
|
+
this.iv[11] = c & 0xff;
|
|
27
|
+
return aesEncryptGCM(plaintext, this.encKey, this.iv, EMPTY_BUFFER);
|
|
28
|
+
}
|
|
29
|
+
decrypt(ciphertext) {
|
|
30
|
+
const c = this.readCounter++;
|
|
31
|
+
this.iv[8] = (c >>> 24) & 0xff;
|
|
32
|
+
this.iv[9] = (c >>> 16) & 0xff;
|
|
33
|
+
this.iv[10] = (c >>> 8) & 0xff;
|
|
34
|
+
this.iv[11] = c & 0xff;
|
|
35
|
+
return aesDecryptGCM(ciphertext, this.decKey, this.iv, EMPTY_BUFFER);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
11
38
|
export const makeNoiseHandler = ({ keyPair: { private: privateKey, public: publicKey }, NOISE_HEADER, logger, routingInfo }) => {
|
|
12
39
|
logger = logger.child({ class: 'ns' });
|
|
40
|
+
const data = Buffer.from(NOISE_MODE);
|
|
41
|
+
let hash = data.byteLength === 32 ? data : sha256(data);
|
|
42
|
+
let salt = hash;
|
|
43
|
+
let encKey = hash;
|
|
44
|
+
let decKey = hash;
|
|
45
|
+
let counter = 0;
|
|
46
|
+
let sentIntro = false;
|
|
47
|
+
let inBytes = Buffer.alloc(0);
|
|
48
|
+
let transport = null;
|
|
49
|
+
let isWaitingForTransport = false;
|
|
50
|
+
let pendingOnFrame = null;
|
|
51
|
+
let introHeader;
|
|
52
|
+
if (routingInfo) {
|
|
53
|
+
introHeader = Buffer.alloc(7 + routingInfo.byteLength + NOISE_HEADER.length);
|
|
54
|
+
introHeader.write('ED', 0, 'utf8');
|
|
55
|
+
introHeader.writeUint8(0, 2);
|
|
56
|
+
introHeader.writeUint8(1, 3);
|
|
57
|
+
introHeader.writeUint8(routingInfo.byteLength >> 16, 4);
|
|
58
|
+
introHeader.writeUint16BE(routingInfo.byteLength & 65535, 5);
|
|
59
|
+
introHeader.set(routingInfo, 7);
|
|
60
|
+
introHeader.set(NOISE_HEADER, 7 + routingInfo.byteLength);
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
introHeader = Buffer.from(NOISE_HEADER);
|
|
64
|
+
}
|
|
13
65
|
const authenticate = (data) => {
|
|
14
|
-
if (!
|
|
66
|
+
if (!transport) {
|
|
15
67
|
hash = sha256(Buffer.concat([hash, data]));
|
|
16
68
|
}
|
|
17
69
|
};
|
|
18
70
|
const encrypt = (plaintext) => {
|
|
19
|
-
|
|
20
|
-
|
|
71
|
+
if (transport) {
|
|
72
|
+
return transport.encrypt(plaintext);
|
|
73
|
+
}
|
|
74
|
+
const result = aesEncryptGCM(plaintext, encKey, generateIV(counter++), hash);
|
|
21
75
|
authenticate(result);
|
|
22
76
|
return result;
|
|
23
77
|
};
|
|
24
78
|
const decrypt = (ciphertext) => {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const iv = generateIV(isFinished ? readCounter : writeCounter);
|
|
28
|
-
const result = aesDecryptGCM(ciphertext, decKey, iv, hash);
|
|
29
|
-
if (isFinished) {
|
|
30
|
-
readCounter += 1;
|
|
31
|
-
}
|
|
32
|
-
else {
|
|
33
|
-
writeCounter += 1;
|
|
79
|
+
if (transport) {
|
|
80
|
+
return transport.decrypt(ciphertext);
|
|
34
81
|
}
|
|
82
|
+
const result = aesDecryptGCM(ciphertext, decKey, generateIV(counter++), hash);
|
|
35
83
|
authenticate(ciphertext);
|
|
36
84
|
return result;
|
|
37
85
|
};
|
|
38
|
-
const localHKDF =
|
|
39
|
-
const key =
|
|
40
|
-
return [key.
|
|
86
|
+
const localHKDF = (data) => {
|
|
87
|
+
const key = hkdf(Buffer.from(data), 64, { salt, info: '' });
|
|
88
|
+
return [key.subarray(0, 32), key.subarray(32)];
|
|
41
89
|
};
|
|
42
|
-
const mixIntoKey =
|
|
43
|
-
const [write, read] =
|
|
90
|
+
const mixIntoKey = (data) => {
|
|
91
|
+
const [write, read] = localHKDF(data);
|
|
44
92
|
salt = write;
|
|
45
93
|
encKey = read;
|
|
46
94
|
decKey = read;
|
|
47
|
-
|
|
48
|
-
writeCounter = 0;
|
|
95
|
+
counter = 0;
|
|
49
96
|
};
|
|
50
97
|
const finishInit = async () => {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
98
|
+
isWaitingForTransport = true;
|
|
99
|
+
const [write, read] = localHKDF(new Uint8Array(0));
|
|
100
|
+
transport = new TransportState(write, read);
|
|
101
|
+
isWaitingForTransport = false;
|
|
102
|
+
logger.trace('Noise handler transitioned to Transport state');
|
|
103
|
+
if (pendingOnFrame) {
|
|
104
|
+
logger.trace({ length: inBytes.length }, 'Flushing buffered frames after transport ready');
|
|
105
|
+
await processData(pendingOnFrame);
|
|
106
|
+
pendingOnFrame = null;
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
const processData = async (onFrame) => {
|
|
110
|
+
let size;
|
|
111
|
+
while (true) {
|
|
112
|
+
if (inBytes.length < 3)
|
|
113
|
+
return;
|
|
114
|
+
size = (inBytes[0] << 16) | (inBytes[1] << 8) | inBytes[2];
|
|
115
|
+
if (inBytes.length < size + 3)
|
|
116
|
+
return;
|
|
117
|
+
let frame = inBytes.subarray(3, size + 3);
|
|
118
|
+
inBytes = inBytes.subarray(size + 3);
|
|
119
|
+
if (transport) {
|
|
120
|
+
const result = transport.decrypt(frame);
|
|
121
|
+
frame = await decodeBinaryNode(result);
|
|
122
|
+
}
|
|
123
|
+
if (logger.level === 'trace') {
|
|
124
|
+
logger.trace({ msg: frame?.attrs?.id }, 'recv frame');
|
|
125
|
+
}
|
|
126
|
+
onFrame(frame);
|
|
127
|
+
}
|
|
58
128
|
};
|
|
59
|
-
const data = Buffer.from(NOISE_MODE);
|
|
60
|
-
let hash = data.byteLength === 32 ? data : sha256(data);
|
|
61
|
-
let salt = hash;
|
|
62
|
-
let encKey = hash;
|
|
63
|
-
let decKey = hash;
|
|
64
|
-
let readCounter = 0;
|
|
65
|
-
let writeCounter = 0;
|
|
66
|
-
let isFinished = false;
|
|
67
|
-
let sentIntro = false;
|
|
68
|
-
let inBytes = Buffer.alloc(0);
|
|
69
129
|
authenticate(NOISE_HEADER);
|
|
70
130
|
authenticate(publicKey);
|
|
71
131
|
return {
|
|
@@ -74,73 +134,67 @@ export const makeNoiseHandler = ({ keyPair: { private: privateKey, public: publi
|
|
|
74
134
|
authenticate,
|
|
75
135
|
mixIntoKey,
|
|
76
136
|
finishInit,
|
|
77
|
-
processHandshake:
|
|
137
|
+
processHandshake: ({ serverHello }, noiseKey) => {
|
|
78
138
|
authenticate(serverHello.ephemeral);
|
|
79
|
-
|
|
139
|
+
mixIntoKey(Curve.sharedKey(privateKey, serverHello.ephemeral));
|
|
80
140
|
const decStaticContent = decrypt(serverHello.static);
|
|
81
|
-
|
|
141
|
+
mixIntoKey(Curve.sharedKey(privateKey, decStaticContent));
|
|
82
142
|
const certDecoded = decrypt(serverHello.payload);
|
|
83
|
-
const { intermediate: certIntermediate
|
|
84
|
-
//
|
|
85
|
-
|
|
143
|
+
const { intermediate: certIntermediate, leaf } = proto.CertChain.decode(certDecoded);
|
|
144
|
+
// leaf
|
|
145
|
+
if (!leaf?.details || !leaf?.signature) {
|
|
146
|
+
throw new Boom('invalid noise leaf certificate', { statusCode: 400 });
|
|
147
|
+
}
|
|
148
|
+
if (!certIntermediate?.details || !certIntermediate?.signature) {
|
|
149
|
+
throw new Boom('invalid noise intermediate certificate', { statusCode: 400 });
|
|
150
|
+
}
|
|
151
|
+
const details = proto.CertChain.NoiseCertificate.Details.decode(certIntermediate.details);
|
|
152
|
+
const { issuerSerial } = details;
|
|
153
|
+
const verify = Curve.verify(details.key, leaf.details, leaf.signature);
|
|
154
|
+
const verifyIntermediate = Curve.verify(WA_CERT_DETAILS.PUBLIC_KEY, certIntermediate.details, certIntermediate.signature);
|
|
155
|
+
if (!verify) {
|
|
156
|
+
throw new Boom('noise certificate signature invalid', { statusCode: 400 });
|
|
157
|
+
}
|
|
158
|
+
if (!verifyIntermediate) {
|
|
159
|
+
throw new Boom('noise intermediate certificate signature invalid', { statusCode: 400 });
|
|
160
|
+
}
|
|
86
161
|
if (issuerSerial !== WA_CERT_DETAILS.SERIAL) {
|
|
87
162
|
throw new Boom('certification match failed', { statusCode: 400 });
|
|
88
163
|
}
|
|
89
164
|
const keyEnc = encrypt(noiseKey.public);
|
|
90
|
-
|
|
165
|
+
mixIntoKey(Curve.sharedKey(noiseKey.private, serverHello.ephemeral));
|
|
91
166
|
return keyEnc;
|
|
92
167
|
},
|
|
93
168
|
encodeFrame: (data) => {
|
|
94
|
-
if (
|
|
95
|
-
data = encrypt(data);
|
|
96
|
-
}
|
|
97
|
-
let header;
|
|
98
|
-
if (routingInfo) {
|
|
99
|
-
header = Buffer.alloc(7);
|
|
100
|
-
header.write('ED', 0, 'utf8');
|
|
101
|
-
header.writeUint8(0, 2);
|
|
102
|
-
header.writeUint8(1, 3);
|
|
103
|
-
header.writeUint8(routingInfo.byteLength >> 16, 4);
|
|
104
|
-
header.writeUint16BE(routingInfo.byteLength & 65535, 5);
|
|
105
|
-
header = Buffer.concat([header, routingInfo, NOISE_HEADER]);
|
|
106
|
-
}
|
|
107
|
-
else {
|
|
108
|
-
header = Buffer.from(NOISE_HEADER);
|
|
169
|
+
if (transport) {
|
|
170
|
+
data = transport.encrypt(data);
|
|
109
171
|
}
|
|
110
|
-
const
|
|
111
|
-
const
|
|
172
|
+
const dataLen = data.byteLength;
|
|
173
|
+
const introSize = sentIntro ? 0 : introHeader.length;
|
|
174
|
+
const frame = Buffer.allocUnsafe(introSize + 3 + dataLen);
|
|
112
175
|
if (!sentIntro) {
|
|
113
|
-
frame.set(
|
|
176
|
+
frame.set(introHeader);
|
|
114
177
|
sentIntro = true;
|
|
115
178
|
}
|
|
116
|
-
frame
|
|
117
|
-
frame
|
|
179
|
+
frame[introSize] = (dataLen >>> 16) & 0xff;
|
|
180
|
+
frame[introSize + 1] = (dataLen >>> 8) & 0xff;
|
|
181
|
+
frame[introSize + 2] = dataLen & 0xff;
|
|
118
182
|
frame.set(data, introSize + 3);
|
|
119
183
|
return frame;
|
|
120
184
|
},
|
|
121
185
|
decodeFrame: async (newData, onFrame) => {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
let size = getBytesSize();
|
|
133
|
-
while (size && inBytes.length >= size + 3) {
|
|
134
|
-
let frame = inBytes.slice(3, size + 3);
|
|
135
|
-
inBytes = inBytes.slice(size + 3);
|
|
136
|
-
if (isFinished) {
|
|
137
|
-
const result = decrypt(frame);
|
|
138
|
-
frame = await decodeBinaryNode(result);
|
|
139
|
-
}
|
|
140
|
-
logger.trace({ msg: frame?.attrs?.id }, 'recv frame');
|
|
141
|
-
onFrame(frame);
|
|
142
|
-
size = getBytesSize();
|
|
186
|
+
if (isWaitingForTransport) {
|
|
187
|
+
inBytes = Buffer.concat([inBytes, newData]);
|
|
188
|
+
pendingOnFrame = onFrame;
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
if (inBytes.length === 0) {
|
|
192
|
+
inBytes = Buffer.from(newData);
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
inBytes = Buffer.concat([inBytes, newData]);
|
|
143
196
|
}
|
|
197
|
+
await processData(onFrame);
|
|
144
198
|
}
|
|
145
199
|
};
|
|
146
200
|
};
|
|
@@ -167,7 +167,13 @@ const processMessage = async (message, { shouldProcessHistoryMsg, placeholderRes
|
|
|
167
167
|
]
|
|
168
168
|
});
|
|
169
169
|
}
|
|
170
|
-
const data = await downloadAndProcessHistorySyncNotification(histNotification, options);
|
|
170
|
+
const data = await downloadAndProcessHistorySyncNotification(histNotification, options, logger);
|
|
171
|
+
if (data.lidPnMappings?.length) {
|
|
172
|
+
logger?.debug({ count: data.lidPnMappings.length }, 'processing LID-PN mappings from history sync');
|
|
173
|
+
await signalRepository.lidMapping
|
|
174
|
+
.storeLIDPNMappings(data.lidPnMappings)
|
|
175
|
+
.catch(err => logger?.warn({ err }, 'failed to store LID-PN mappings from history sync'));
|
|
176
|
+
}
|
|
171
177
|
ev.emit('messaging-history.set', {
|
|
172
178
|
...data,
|
|
173
179
|
isLatest: histNotification.syncType !== proto.HistorySync.HistorySyncType.ON_DEMAND ? isLatest : undefined,
|
|
@@ -215,23 +221,48 @@ const processMessage = async (message, { shouldProcessHistoryMsg, placeholderRes
|
|
|
215
221
|
case proto.Message.ProtocolMessage.Type.PEER_DATA_OPERATION_REQUEST_RESPONSE_MESSAGE:
|
|
216
222
|
const response = protocolMsg.peerDataOperationRequestResponseMessage;
|
|
217
223
|
if (response) {
|
|
218
|
-
await placeholderResendCache?.del(response.stanzaId);
|
|
219
224
|
// TODO: IMPLEMENT HISTORY SYNC ETC (sticker uploads etc.).
|
|
220
|
-
const
|
|
225
|
+
const peerDataOperationResult = response.peerDataOperationResult || [];
|
|
221
226
|
for (const result of peerDataOperationResult) {
|
|
222
|
-
const
|
|
227
|
+
const retryResponse = result?.placeholderMessageResendResponse;
|
|
228
|
+
//eslint-disable-next-line max-depth
|
|
229
|
+
if (!retryResponse?.webMessageInfoBytes) {
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
223
232
|
//eslint-disable-next-line max-depth
|
|
224
|
-
|
|
233
|
+
try {
|
|
225
234
|
const webMessageInfo = proto.WebMessageInfo.decode(retryResponse.webMessageInfoBytes);
|
|
226
|
-
|
|
227
|
-
//
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
+
const msgId = webMessageInfo.key?.id;
|
|
236
|
+
// Retrieve cached original message data (preserves LID details,
|
|
237
|
+
// timestamps, etc. that the phone may omit in its PDO response)
|
|
238
|
+
const cachedData = msgId ? await placeholderResendCache?.get(msgId) : undefined;
|
|
239
|
+
//eslint-disable-next-line max-depth
|
|
240
|
+
if (msgId) {
|
|
241
|
+
await placeholderResendCache?.del(msgId);
|
|
242
|
+
}
|
|
243
|
+
let finalMsg;
|
|
244
|
+
//eslint-disable-next-line max-depth
|
|
245
|
+
if (cachedData && typeof cachedData === 'object') {
|
|
246
|
+
// Apply decoded message content onto cached metadata (preserves LID etc.)
|
|
247
|
+
cachedData.message = webMessageInfo.message;
|
|
248
|
+
//eslint-disable-next-line max-depth
|
|
249
|
+
if (webMessageInfo.messageTimestamp) {
|
|
250
|
+
cachedData.messageTimestamp = webMessageInfo.messageTimestamp;
|
|
251
|
+
}
|
|
252
|
+
finalMsg = cachedData;
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
finalMsg = webMessageInfo;
|
|
256
|
+
}
|
|
257
|
+
logger?.debug({ msgId, requestId: response.stanzaId }, 'received placeholder resend');
|
|
258
|
+
ev.emit('messages.upsert', {
|
|
259
|
+
messages: [finalMsg],
|
|
260
|
+
type: 'notify',
|
|
261
|
+
requestId: response.stanzaId
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
catch (err) {
|
|
265
|
+
logger?.warn({ err, stanzaId: response.stanzaId }, 'failed to decode placeholder resend response');
|
|
235
266
|
}
|
|
236
267
|
}
|
|
237
268
|
}
|
|
@@ -254,6 +285,18 @@ const processMessage = async (message, { shouldProcessHistoryMsg, placeholderRes
|
|
|
254
285
|
}
|
|
255
286
|
]);
|
|
256
287
|
break;
|
|
288
|
+
case proto.Message.ProtocolMessage.Type.GROUP_MEMBER_LABEL_CHANGE:
|
|
289
|
+
const labelAssociationMsg = protocolMsg.memberLabel;
|
|
290
|
+
if (labelAssociationMsg?.label) {
|
|
291
|
+
ev.emit('group.member-tag.update', {
|
|
292
|
+
groupId: chat.id,
|
|
293
|
+
label: labelAssociationMsg.label,
|
|
294
|
+
participant: message.key.participant,
|
|
295
|
+
participantAlt: message.key.participantAlt,
|
|
296
|
+
messageTimestamp: Number(message.messageTimestamp)
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
break;
|
|
257
300
|
case proto.Message.ProtocolMessage.Type.LID_MIGRATION_MAPPING_SYNC:
|
|
258
301
|
const encodedPayload = protocolMsg.lidMigrationMappingSyncMessage?.encodedMappingPayload;
|
|
259
302
|
const { pnToLidMappings, chatDbMigrationTimestamp } = proto.LIDMigrationMappingSyncPayload.decode(encodedPayload);
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { createHmac } from 'crypto';
|
|
2
|
+
import { proto } from '../../WAProto/index.js';
|
|
3
|
+
import { hkdf } from './crypto.js';
|
|
4
|
+
const reportingFields = [
|
|
5
|
+
{ f: 1 },
|
|
6
|
+
{
|
|
7
|
+
f: 3,
|
|
8
|
+
s: [{ f: 2 }, { f: 3 }, { f: 8 }, { f: 11 }, { f: 17, s: [{ f: 21 }, { f: 22 }] }, { f: 25 }]
|
|
9
|
+
},
|
|
10
|
+
{ f: 4, s: [{ f: 1 }, { f: 16 }, { f: 17, s: [{ f: 21 }, { f: 22 }] }] },
|
|
11
|
+
{ f: 5, s: [{ f: 3 }, { f: 4 }, { f: 5 }, { f: 16 }, { f: 17, s: [{ f: 21 }, { f: 22 }] }] },
|
|
12
|
+
{ f: 6, s: [{ f: 1 }, { f: 17, s: [{ f: 21 }, { f: 22 }] }, { f: 30 }] },
|
|
13
|
+
{ f: 7, s: [{ f: 2 }, { f: 7 }, { f: 10 }, { f: 17, s: [{ f: 21 }, { f: 22 }] }, { f: 20 }] },
|
|
14
|
+
{ f: 8, s: [{ f: 2 }, { f: 7 }, { f: 9 }, { f: 17, s: [{ f: 21 }, { f: 22 }] }, { f: 21 }] },
|
|
15
|
+
{ f: 9, s: [{ f: 2 }, { f: 6 }, { f: 7 }, { f: 13 }, { f: 17, s: [{ f: 21 }, { f: 22 }] }, { f: 20 }] },
|
|
16
|
+
{ f: 12, s: [{ f: 1 }, { f: 2 }, { f: 14, m: true }, { f: 15 }] },
|
|
17
|
+
{ f: 18, s: [{ f: 6 }, { f: 16 }, { f: 17, s: [{ f: 21 }, { f: 22 }] }] },
|
|
18
|
+
{ f: 26, s: [{ f: 4 }, { f: 5 }, { f: 8 }, { f: 13 }, { f: 17, s: [{ f: 21 }, { f: 22 }] }] },
|
|
19
|
+
{ f: 28, s: [{ f: 1 }, { f: 2 }, { f: 4 }, { f: 5 }, { f: 6 }, { f: 7, s: [{ f: 21 }, { f: 22 }] }] },
|
|
20
|
+
{ f: 37, s: [{ f: 1, m: true }] },
|
|
21
|
+
{
|
|
22
|
+
f: 49,
|
|
23
|
+
s: [
|
|
24
|
+
{ f: 2 },
|
|
25
|
+
{ f: 3, s: [{ f: 1 }, { f: 2 }] },
|
|
26
|
+
{ f: 5, s: [{ f: 21 }, { f: 22 }] },
|
|
27
|
+
{ f: 8, s: [{ f: 1 }, { f: 2 }] }
|
|
28
|
+
]
|
|
29
|
+
},
|
|
30
|
+
{ f: 53, s: [{ f: 1, m: true }] },
|
|
31
|
+
{ f: 55, s: [{ f: 1, m: true }] },
|
|
32
|
+
{ f: 58, s: [{ f: 1, m: true }] },
|
|
33
|
+
{ f: 59, s: [{ f: 1, m: true }] },
|
|
34
|
+
{
|
|
35
|
+
f: 60,
|
|
36
|
+
s: [
|
|
37
|
+
{ f: 2 },
|
|
38
|
+
{ f: 3, s: [{ f: 1 }, { f: 2 }] },
|
|
39
|
+
{ f: 5, s: [{ f: 21 }, { f: 22 }] },
|
|
40
|
+
{ f: 8, s: [{ f: 1 }, { f: 2 }] }
|
|
41
|
+
]
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
f: 64,
|
|
45
|
+
s: [
|
|
46
|
+
{ f: 2 },
|
|
47
|
+
{ f: 3, s: [{ f: 1 }, { f: 2 }] },
|
|
48
|
+
{ f: 5, s: [{ f: 21 }, { f: 22 }] },
|
|
49
|
+
{ f: 8, s: [{ f: 1 }, { f: 2 }] }
|
|
50
|
+
]
|
|
51
|
+
},
|
|
52
|
+
{ f: 66, s: [{ f: 2 }, { f: 6 }, { f: 7 }, { f: 13 }, { f: 17, s: [{ f: 21 }, { f: 22 }] }, { f: 20 }] },
|
|
53
|
+
{ f: 74, s: [{ f: 1, m: true }] },
|
|
54
|
+
{ f: 87, s: [{ f: 1, m: true }] },
|
|
55
|
+
{ f: 88, s: [{ f: 1 }, { f: 2, s: [{ f: 1 }] }, { f: 3, s: [{ f: 21 }, { f: 22 }] }] },
|
|
56
|
+
{ f: 92, s: [{ f: 1, m: true }] },
|
|
57
|
+
{ f: 93, s: [{ f: 1, m: true }] },
|
|
58
|
+
{ f: 94, s: [{ f: 1, m: true }] }
|
|
59
|
+
];
|
|
60
|
+
const compileReportingFields = (fields) => {
|
|
61
|
+
const map = new Map();
|
|
62
|
+
for (const f of fields) {
|
|
63
|
+
map.set(f.f, {
|
|
64
|
+
m: f.m,
|
|
65
|
+
children: f.s ? compileReportingFields(f.s) : undefined
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
return map;
|
|
69
|
+
};
|
|
70
|
+
const compiledReportingFields = compileReportingFields(reportingFields);
|
|
71
|
+
const EMPTY_MAP = new Map();
|
|
72
|
+
const ENC_SECRET_REPORT_TOKEN = 'Report Token';
|
|
73
|
+
const WIRE = {
|
|
74
|
+
VARINT: 0,
|
|
75
|
+
FIXED64: 1,
|
|
76
|
+
BYTES: 2,
|
|
77
|
+
FIXED32: 5
|
|
78
|
+
};
|
|
79
|
+
export const shouldIncludeReportingToken = (message) => !message.reactionMessage &&
|
|
80
|
+
!message.encReactionMessage &&
|
|
81
|
+
!message.encEventResponseMessage &&
|
|
82
|
+
!message.pollUpdateMessage;
|
|
83
|
+
const generateMsgSecretKey = (modificationType, origMsgId, origMsgSender, modificationSender, origMsgSecret) => {
|
|
84
|
+
const useCaseSecret = Buffer.concat([
|
|
85
|
+
Buffer.from(origMsgId, 'utf8'),
|
|
86
|
+
Buffer.from(origMsgSender, 'utf8'),
|
|
87
|
+
Buffer.from(modificationSender, 'utf8'),
|
|
88
|
+
Buffer.from(modificationType, 'utf8')
|
|
89
|
+
]);
|
|
90
|
+
return hkdf(origMsgSecret, 32, { info: useCaseSecret.toString('latin1') });
|
|
91
|
+
};
|
|
92
|
+
const extractReportingTokenContent = (data, cfg) => {
|
|
93
|
+
const out = [];
|
|
94
|
+
let i = 0;
|
|
95
|
+
while (i < data.length) {
|
|
96
|
+
const tag = decodeVarint(data, i);
|
|
97
|
+
if (!tag.ok) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
const fieldNum = tag.value >> 3;
|
|
101
|
+
const wireType = tag.value & 0x7;
|
|
102
|
+
const fieldStart = i;
|
|
103
|
+
i += tag.bytes;
|
|
104
|
+
const fieldCfg = cfg.get(fieldNum);
|
|
105
|
+
const pushSlice = (end) => {
|
|
106
|
+
if (end > data.length) {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
out.push({ num: fieldNum, bytes: data.subarray(fieldStart, end) });
|
|
110
|
+
i = end;
|
|
111
|
+
return true;
|
|
112
|
+
};
|
|
113
|
+
const skip = (end) => {
|
|
114
|
+
if (end > data.length) {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
i = end;
|
|
118
|
+
return true;
|
|
119
|
+
};
|
|
120
|
+
if (wireType === WIRE.VARINT) {
|
|
121
|
+
const v = decodeVarint(data, i);
|
|
122
|
+
if (!v.ok) {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
const end = i + v.bytes;
|
|
126
|
+
if (!fieldCfg) {
|
|
127
|
+
if (!skip(end)) {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (!pushSlice(end)) {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (wireType === WIRE.FIXED64) {
|
|
138
|
+
const end = i + 8;
|
|
139
|
+
if (!fieldCfg) {
|
|
140
|
+
if (!skip(end)) {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (!pushSlice(end)) {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (wireType === WIRE.FIXED32) {
|
|
151
|
+
const end = i + 4;
|
|
152
|
+
if (!fieldCfg) {
|
|
153
|
+
if (!skip(end)) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (!pushSlice(end)) {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (wireType === WIRE.BYTES) {
|
|
164
|
+
const len = decodeVarint(data, i);
|
|
165
|
+
if (!len.ok) {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
const valStart = i + len.bytes;
|
|
169
|
+
const valEnd = valStart + len.value;
|
|
170
|
+
if (valEnd > data.length) {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
if (!fieldCfg) {
|
|
174
|
+
i = valEnd;
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
if (fieldCfg.m || fieldCfg.children) {
|
|
178
|
+
const sub = extractReportingTokenContent(data.subarray(valStart, valEnd), fieldCfg.children ?? EMPTY_MAP);
|
|
179
|
+
if (sub === null) {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
if (sub.length > 0) {
|
|
183
|
+
const newTag = encodeVarint(tag.value);
|
|
184
|
+
const newLen = encodeVarint(sub.length);
|
|
185
|
+
out.push({
|
|
186
|
+
num: fieldNum,
|
|
187
|
+
bytes: Buffer.concat([newTag, newLen, sub])
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
i = valEnd;
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
out.push({ num: fieldNum, bytes: data.subarray(fieldStart, valEnd) });
|
|
194
|
+
i = valEnd;
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
if (out.length === 0) {
|
|
200
|
+
return Buffer.alloc(0);
|
|
201
|
+
}
|
|
202
|
+
out.sort((a, b) => a.num - b.num);
|
|
203
|
+
return Buffer.concat(out.map(f => f.bytes));
|
|
204
|
+
};
|
|
205
|
+
const decodeVarint = (buffer, offset) => {
|
|
206
|
+
let value = 0;
|
|
207
|
+
let bytes = 0;
|
|
208
|
+
let shift = 0;
|
|
209
|
+
while (offset + bytes < buffer.length) {
|
|
210
|
+
const current = buffer[offset + bytes];
|
|
211
|
+
value |= (current & 0x7f) << shift;
|
|
212
|
+
bytes++;
|
|
213
|
+
if ((current & 0x80) === 0) {
|
|
214
|
+
return { value, bytes, ok: true };
|
|
215
|
+
}
|
|
216
|
+
shift += 7;
|
|
217
|
+
if (shift > 35) {
|
|
218
|
+
return { value: 0, bytes: 0, ok: false };
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return { value: 0, bytes: 0, ok: false };
|
|
222
|
+
};
|
|
223
|
+
const encodeVarint = (value) => {
|
|
224
|
+
const parts = [];
|
|
225
|
+
let remaining = value >>> 0;
|
|
226
|
+
while (remaining > 0x7f) {
|
|
227
|
+
parts.push((remaining & 0x7f) | 0x80);
|
|
228
|
+
remaining >>>= 7;
|
|
229
|
+
}
|
|
230
|
+
parts.push(remaining);
|
|
231
|
+
return Buffer.from(parts);
|
|
232
|
+
};
|
|
233
|
+
export const getMessageReportingToken = async (msgProtobuf, message, key) => {
|
|
234
|
+
const msgSecret = message.messageContextInfo?.messageSecret;
|
|
235
|
+
if (!msgSecret || !key.id) {
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
const from = key.fromMe ? key.remoteJid : key.participant || key.remoteJid;
|
|
239
|
+
const to = key.fromMe ? key.participant || key.remoteJid : key.remoteJid;
|
|
240
|
+
const reportingSecret = generateMsgSecretKey(ENC_SECRET_REPORT_TOKEN, key.id, from, to, msgSecret);
|
|
241
|
+
const content = extractReportingTokenContent(msgProtobuf, compiledReportingFields);
|
|
242
|
+
if (!content || content.length === 0) {
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
const reportingToken = createHmac('sha256', reportingSecret).update(content).digest().subarray(0, 16);
|
|
246
|
+
return {
|
|
247
|
+
tag: 'reporting',
|
|
248
|
+
attrs: {},
|
|
249
|
+
content: [
|
|
250
|
+
{
|
|
251
|
+
tag: 'reporting_token',
|
|
252
|
+
attrs: { v: '2' },
|
|
253
|
+
content: reportingToken
|
|
254
|
+
}
|
|
255
|
+
]
|
|
256
|
+
};
|
|
257
|
+
};
|
|
258
|
+
//# sourceMappingURL=reporting-utils.js.map
|