whalibmob 5.7.1 → 5.7.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.
@@ -25,6 +25,7 @@ const { uploadMedia } = require('../MediaService');
25
25
  const { imageThumbnail, videoThumbnail, probeImageSize } = require('../MediaThumbnail');
26
26
  const { tcTokenExpired, shouldSendNewTcToken, normalizeJidForTcToken,
27
27
  isRegularTcTokenUser } = require('./TcTokenStore');
28
+ const { buildReportingNode } = require('./ReportingToken');
28
29
 
29
30
  // ─── Helpers ──────────────────────────────────────────────────────────────────
30
31
 
@@ -1014,6 +1015,19 @@ class MessageSender {
1014
1015
  msgContent = [...devIdNodes, participantsNode];
1015
1016
  }
1016
1017
 
1018
+ // ─── Reporting token ─────────────────────────────────────────────────────
1019
+ // Rides on any message carrying a message secret, so a recipient can report
1020
+ // it and have the server verify the reported text is what was actually
1021
+ // sent. Sending the secret without the token is a combination no real
1022
+ // client produces.
1023
+ const reportingNode = buildReportingNode({
1024
+ plaintext, msgId, remoteJid: routingToJid, BinaryNode
1025
+ });
1026
+ if (reportingNode) {
1027
+ msgContent.push(reportingNode);
1028
+ _whaDbg('[DBG] REPORTING_TOKEN attached to=' + routingToJid + ' id=' + msgId);
1029
+ }
1030
+
1017
1031
  // ─── tcToken (error 463 / reachout timelock defense) ─────────────────────
1018
1032
  // WhatsApp counts every DM without a <tctoken> node as a "reaching out"
1019
1033
  // event. Once enough accumulate the server returns error 463 (timelock).
@@ -1372,6 +1386,17 @@ class MessageSender {
1372
1386
  if (mediaSubtype) skmsgAttrs.mediatype = mediaSubtype;
1373
1387
  msgContent.push(new BinaryNode('enc', skmsgAttrs, Buffer.isBuffer(skmsgCiphertext) ? skmsgCiphertext : Buffer.from(skmsgCiphertext)));
1374
1388
 
1389
+ // Same reporting token a direct message carries. A poll posted to a group
1390
+ // is the common case: it has a message secret, so it needs the token that
1391
+ // lets a report on it be verified.
1392
+ const groupReportingNode = buildReportingNode({
1393
+ plaintext, msgId, remoteJid: groupJid, BinaryNode
1394
+ });
1395
+ if (groupReportingNode) {
1396
+ msgContent.push(groupReportingNode);
1397
+ _whaDbg('[DBG] REPORTING_TOKEN attached to=' + groupJid + ' id=' + msgId);
1398
+ }
1399
+
1375
1400
  // phash belongs on a group stanza and only on a group stanza — whatsmeow's
1376
1401
  // sendGroup sets it, sendDM does not. It tells the server which device set we
1377
1402
  // fanned out to; the server compares it against its own and echoes its hash
@@ -0,0 +1,364 @@
1
+ 'use strict';
2
+
3
+ // The reporting token that rides on messages carrying a message secret.
4
+ //
5
+ // It exists so a message can be reported for abuse without the server being
6
+ // able to read it. The sender attaches an HMAC over the message's own content,
7
+ // keyed by material derived from the message secret. If the recipient later
8
+ // reports the message, they hand the server the plaintext; the server cannot
9
+ // decrypt anything itself, but it can recompute the HMAC and confirm the text
10
+ // being reported is exactly what was sent rather than something the reporter
11
+ // made up.
12
+ //
13
+ // The HMAC is not taken over the whole message. WhatsApp hashes a fixed subset
14
+ // of protobuf fields — the ones that carry reportable content — copied out in
15
+ // field-number order, so that fields which vary between devices or over time do
16
+ // not change the token. REPORTING_FIELDS below is that subset, and it is the
17
+ // part that has to match byte for byte or the token is worthless.
18
+ //
19
+ // Ported field-for-field from Baileys' reporting-utils. whatsmeow derives the
20
+ // key from (ownID, to) where Baileys uses the destination JID for both halves;
21
+ // the reference followed here is Baileys, and since the server only checks the
22
+ // token when a report is actually filed, neither choice is observable on send.
23
+
24
+ const crypto = require('crypto');
25
+ const { hkdf } = require('@noble/hashes/hkdf');
26
+ const { sha256 } = require('@noble/hashes/sha256');
27
+
28
+ // Protobuf field numbers on Message that the token is computed over.
29
+ // f field number
30
+ // m descend into it but keep every subfield
31
+ // s descend into it and keep only these subfields
32
+ const REPORTING_FIELDS = [
33
+ { f: 1 },
34
+ { f: 3, s: [{ f: 2 }, { f: 3 }, { f: 8 }, { f: 11 }, { f: 17, s: [{ f: 21 }, { f: 22 }] }, { f: 25 }] },
35
+ { f: 4, s: [{ f: 1 }, { f: 16 }, { f: 17, s: [{ f: 21 }, { f: 22 }] }] },
36
+ { f: 5, s: [{ f: 3 }, { f: 4 }, { f: 5 }, { f: 16 }, { f: 17, s: [{ f: 21 }, { f: 22 }] }] },
37
+ { f: 6, s: [{ f: 1 }, { f: 17, s: [{ f: 21 }, { f: 22 }] }, { f: 30 }] },
38
+ { f: 7, s: [{ f: 2 }, { f: 7 }, { f: 10 }, { f: 17, s: [{ f: 21 }, { f: 22 }] }, { f: 20 }] },
39
+ { f: 8, s: [{ f: 2 }, { f: 7 }, { f: 9 }, { f: 17, s: [{ f: 21 }, { f: 22 }] }, { f: 21 }] },
40
+ { f: 9, s: [{ f: 2 }, { f: 6 }, { f: 7 }, { f: 13 }, { f: 17, s: [{ f: 21 }, { f: 22 }] }, { f: 20 }] },
41
+ { f: 12, s: [{ f: 1 }, { f: 2 }, { f: 14, m: true }, { f: 15 }] },
42
+ { f: 18, s: [{ f: 6 }, { f: 16 }, { f: 17, s: [{ f: 21 }, { f: 22 }] }] },
43
+ { f: 26, s: [{ f: 4 }, { f: 5 }, { f: 8 }, { f: 13 }, { f: 17, s: [{ f: 21 }, { f: 22 }] }] },
44
+ { f: 28, s: [{ f: 1 }, { f: 2 }, { f: 4 }, { f: 5 }, { f: 6 }, { f: 7, s: [{ f: 21 }, { f: 22 }] }] },
45
+ { f: 37, s: [{ f: 1, m: true }] },
46
+ { f: 49, s: [{ f: 2 }, { f: 3, s: [{ f: 1 }, { f: 2 }] }, { f: 5, s: [{ f: 21 }, { f: 22 }] },
47
+ { f: 8, s: [{ f: 1 }, { f: 2 }] }] },
48
+ { f: 53, s: [{ f: 1, m: true }] },
49
+ { f: 55, s: [{ f: 1, m: true }] },
50
+ { f: 58, s: [{ f: 1, m: true }] },
51
+ { f: 59, s: [{ f: 1, m: true }] },
52
+ { f: 60, s: [{ f: 2 }, { f: 3, s: [{ f: 1 }, { f: 2 }] }, { f: 5, s: [{ f: 21 }, { f: 22 }] },
53
+ { f: 8, s: [{ f: 1 }, { f: 2 }] }] },
54
+ { f: 64, s: [{ f: 2 }, { f: 3, s: [{ f: 1 }, { f: 2 }] }, { f: 5, s: [{ f: 21 }, { f: 22 }] },
55
+ { f: 8, s: [{ f: 1 }, { f: 2 }] }] },
56
+ { f: 66, s: [{ f: 2 }, { f: 6 }, { f: 7 }, { f: 13 }, { f: 17, s: [{ f: 21 }, { f: 22 }] }, { f: 20 }] },
57
+ { f: 74, s: [{ f: 1, m: true }] },
58
+ { f: 87, s: [{ f: 1, m: true }] },
59
+ { f: 88, s: [{ f: 1 }, { f: 2, s: [{ f: 1 }] }, { f: 3, s: [{ f: 21 }, { f: 22 }] }] },
60
+ { f: 92, s: [{ f: 1, m: true }] },
61
+ { f: 93, s: [{ f: 1, m: true }] },
62
+ { f: 94, s: [{ f: 1, m: true }] }
63
+ ];
64
+
65
+ function compileFields(fields) {
66
+ const map = new Map();
67
+ for (const f of fields) {
68
+ map.set(f.f, { m: f.m, children: f.s ? compileFields(f.s) : undefined });
69
+ }
70
+ return map;
71
+ }
72
+
73
+ const COMPILED = compileFields(REPORTING_FIELDS);
74
+ const EMPTY_MAP = new Map();
75
+
76
+ const ENC_SECRET_REPORT_TOKEN = 'Report Token';
77
+
78
+ const WIRE = { VARINT: 0, FIXED64: 1, BYTES: 2, FIXED32: 5 };
79
+
80
+ // Message field numbers that matter here.
81
+ const F_MESSAGE_CONTEXT_INFO = 35;
82
+ const F_REACTION = 46;
83
+ const F_POLL_UPDATE = 50;
84
+ const F_ENC_REACTION = 56;
85
+ const F_ENC_EVENT_RESPONSE = 76;
86
+ const F_CTX_MESSAGE_SECRET = 3; // inside MessageContextInfo
87
+
88
+ // ─── varints ─────────────────────────────────────────────────────────────────
89
+
90
+ function decodeVarint(buffer, offset) {
91
+ let value = 0, bytes = 0, shift = 0;
92
+ while (offset + bytes < buffer.length) {
93
+ const current = buffer[offset + bytes];
94
+ value |= (current & 0x7f) << shift;
95
+ bytes++;
96
+ if ((current & 0x80) === 0) return { value, bytes, ok: true };
97
+ shift += 7;
98
+ if (shift > 35) return { value: 0, bytes: 0, ok: false };
99
+ }
100
+ return { value: 0, bytes: 0, ok: false };
101
+ }
102
+
103
+ function encodeVarint(value) {
104
+ const parts = [];
105
+ let remaining = value >>> 0;
106
+ while (remaining > 0x7f) {
107
+ parts.push((remaining & 0x7f) | 0x80);
108
+ remaining >>>= 7;
109
+ }
110
+ parts.push(remaining);
111
+ return Buffer.from(parts);
112
+ }
113
+
114
+ // ─── selective protobuf extraction ───────────────────────────────────────────
115
+ //
116
+ // Walks the encoded message and copies out only the configured fields, keeping
117
+ // their original bytes. Length-delimited fields with a config of their own are
118
+ // re-encoded around whatever their own walk produced, so a nested field that
119
+ // contributes nothing disappears entirely rather than leaving an empty wrapper.
120
+ //
121
+ // The result is sorted by field number, which is what makes the token
122
+ // independent of the order the encoder happened to write the fields in.
123
+ //
124
+ // Returns null on malformed input — the caller then attaches no token, which is
125
+ // the same thing Baileys does.
126
+ function extractReportingTokenContent(data, cfg) {
127
+ const out = [];
128
+ let i = 0;
129
+
130
+ while (i < data.length) {
131
+ const tag = decodeVarint(data, i);
132
+ if (!tag.ok) return null;
133
+
134
+ const fieldNum = tag.value >> 3;
135
+ const wireType = tag.value & 0x7;
136
+ const fieldStart = i;
137
+ i += tag.bytes;
138
+
139
+ const fieldCfg = cfg.get(fieldNum);
140
+
141
+ const pushSlice = (end) => {
142
+ if (end > data.length) return false;
143
+ out.push({ num: fieldNum, bytes: data.subarray(fieldStart, end) });
144
+ i = end;
145
+ return true;
146
+ };
147
+ const skip = (end) => {
148
+ if (end > data.length) return false;
149
+ i = end;
150
+ return true;
151
+ };
152
+
153
+ if (wireType === WIRE.VARINT) {
154
+ const v = decodeVarint(data, i);
155
+ if (!v.ok) return null;
156
+ const end = i + v.bytes;
157
+ if (!fieldCfg) { if (!skip(end)) return null; continue; }
158
+ if (!pushSlice(end)) return null;
159
+ continue;
160
+ }
161
+
162
+ if (wireType === WIRE.FIXED64) {
163
+ const end = i + 8;
164
+ if (!fieldCfg) { if (!skip(end)) return null; continue; }
165
+ if (!pushSlice(end)) return null;
166
+ continue;
167
+ }
168
+
169
+ if (wireType === WIRE.FIXED32) {
170
+ const end = i + 4;
171
+ if (!fieldCfg) { if (!skip(end)) return null; continue; }
172
+ if (!pushSlice(end)) return null;
173
+ continue;
174
+ }
175
+
176
+ if (wireType === WIRE.BYTES) {
177
+ const len = decodeVarint(data, i);
178
+ if (!len.ok) return null;
179
+ const valStart = i + len.bytes;
180
+ const valEnd = valStart + len.value;
181
+ if (valEnd > data.length) return null;
182
+
183
+ if (!fieldCfg) { i = valEnd; continue; }
184
+
185
+ if (fieldCfg.m || fieldCfg.children) {
186
+ const sub = extractReportingTokenContent(
187
+ data.subarray(valStart, valEnd), fieldCfg.children || EMPTY_MAP
188
+ );
189
+ if (sub === null) return null;
190
+ if (sub.length > 0) {
191
+ out.push({
192
+ num: fieldNum,
193
+ bytes: Buffer.concat([encodeVarint(tag.value), encodeVarint(sub.length), sub])
194
+ });
195
+ }
196
+ i = valEnd;
197
+ continue;
198
+ }
199
+
200
+ out.push({ num: fieldNum, bytes: data.subarray(fieldStart, valEnd) });
201
+ i = valEnd;
202
+ continue;
203
+ }
204
+
205
+ return null;
206
+ }
207
+
208
+ if (out.length === 0) return Buffer.alloc(0);
209
+ out.sort((a, b) => a.num - b.num);
210
+ return Buffer.concat(out.map(f => f.bytes));
211
+ }
212
+
213
+ // ─── key derivation ──────────────────────────────────────────────────────────
214
+ //
215
+ // The same construction WhatsApp uses for every secret derived from a message
216
+ // secret — poll votes, reactions, and this. The use-case string is what keeps
217
+ // them from colliding.
218
+ function generateMsgSecretKey(modificationType, origMsgId, origMsgSender, modificationSender, origMsgSecret) {
219
+ const useCaseSecret = Buffer.concat([
220
+ Buffer.from(String(origMsgId), 'utf8'),
221
+ Buffer.from(String(origMsgSender), 'utf8'),
222
+ Buffer.from(String(modificationSender), 'utf8'),
223
+ Buffer.from(modificationType, 'utf8')
224
+ ]);
225
+ return Buffer.from(hkdf(sha256, origMsgSecret, Buffer.alloc(32), useCaseSecret, 32));
226
+ }
227
+
228
+ // ─── reading what we need out of the encoded message ─────────────────────────
229
+
230
+ // Shallow scan for the top-level field numbers present, plus the message
231
+ // secret if MessageContextInfo carries one. Avoids decoding the whole message
232
+ // just to answer two questions about it.
233
+ function inspectMessage(plaintext) {
234
+ const present = new Set();
235
+ let messageSecret = null;
236
+ let i = 0;
237
+
238
+ while (i < plaintext.length) {
239
+ const tag = decodeVarint(plaintext, i);
240
+ if (!tag.ok) return { present, messageSecret, ok: false };
241
+ const fieldNum = tag.value >> 3;
242
+ const wireType = tag.value & 0x7;
243
+ i += tag.bytes;
244
+ present.add(fieldNum);
245
+
246
+ if (wireType === WIRE.VARINT) {
247
+ const v = decodeVarint(plaintext, i);
248
+ if (!v.ok) return { present, messageSecret, ok: false };
249
+ i += v.bytes;
250
+ } else if (wireType === WIRE.FIXED64) {
251
+ i += 8;
252
+ } else if (wireType === WIRE.FIXED32) {
253
+ i += 4;
254
+ } else if (wireType === WIRE.BYTES) {
255
+ const len = decodeVarint(plaintext, i);
256
+ if (!len.ok) return { present, messageSecret, ok: false };
257
+ const valStart = i + len.bytes;
258
+ const valEnd = valStart + len.value;
259
+ if (valEnd > plaintext.length) return { present, messageSecret, ok: false };
260
+ if (fieldNum === F_MESSAGE_CONTEXT_INFO) {
261
+ messageSecret = readMessageSecret(plaintext.subarray(valStart, valEnd));
262
+ }
263
+ i = valEnd;
264
+ } else {
265
+ return { present, messageSecret, ok: false };
266
+ }
267
+ }
268
+ return { present, messageSecret, ok: true };
269
+ }
270
+
271
+ function readMessageSecret(ctxBuf) {
272
+ let i = 0;
273
+ while (i < ctxBuf.length) {
274
+ const tag = decodeVarint(ctxBuf, i);
275
+ if (!tag.ok) return null;
276
+ const fieldNum = tag.value >> 3;
277
+ const wireType = tag.value & 0x7;
278
+ i += tag.bytes;
279
+
280
+ if (wireType === WIRE.VARINT) {
281
+ const v = decodeVarint(ctxBuf, i);
282
+ if (!v.ok) return null;
283
+ i += v.bytes;
284
+ } else if (wireType === WIRE.FIXED64) {
285
+ i += 8;
286
+ } else if (wireType === WIRE.FIXED32) {
287
+ i += 4;
288
+ } else if (wireType === WIRE.BYTES) {
289
+ const len = decodeVarint(ctxBuf, i);
290
+ if (!len.ok) return null;
291
+ const valStart = i + len.bytes;
292
+ const valEnd = valStart + len.value;
293
+ if (valEnd > ctxBuf.length) return null;
294
+ if (fieldNum === F_CTX_MESSAGE_SECRET) return ctxBuf.subarray(valStart, valEnd);
295
+ i = valEnd;
296
+ } else {
297
+ return null;
298
+ }
299
+ }
300
+ return null;
301
+ }
302
+
303
+ /**
304
+ * Whether a message of this shape carries a reporting token at all.
305
+ *
306
+ * Reactions and poll votes are excluded: they are modifications of somebody
307
+ * else's message, and the token belongs to the message being modified.
308
+ */
309
+ function shouldIncludeReportingToken(present) {
310
+ return !present.has(F_REACTION) &&
311
+ !present.has(F_ENC_REACTION) &&
312
+ !present.has(F_ENC_EVENT_RESPONSE) &&
313
+ !present.has(F_POLL_UPDATE);
314
+ }
315
+
316
+ /**
317
+ * Build the <reporting> node for an outgoing message, or null when there is
318
+ * nothing to attach.
319
+ *
320
+ * @param {object} opts
321
+ * plaintext encoded Message protobuf, exactly as it will be encrypted
322
+ * msgId the stanza id
323
+ * remoteJid the destination JID
324
+ * participant set only on a targeted resend; undefined otherwise
325
+ * BinaryNode the node constructor
326
+ */
327
+ function buildReportingNode(opts) {
328
+ const { plaintext, msgId, remoteJid, participant, BinaryNode } = opts;
329
+ if (!Buffer.isBuffer(plaintext) || !msgId || !remoteJid) return null;
330
+
331
+ const info = inspectMessage(plaintext);
332
+ if (!info.ok || !info.messageSecret || !info.messageSecret.length) return null;
333
+ if (!shouldIncludeReportingToken(info.present)) return null;
334
+
335
+ // fromMe is always true here — we only ever build this for our own sends.
336
+ const from = String(remoteJid);
337
+ const to = String(participant || remoteJid);
338
+
339
+ const reportingSecret = generateMsgSecretKey(
340
+ ENC_SECRET_REPORT_TOKEN, msgId, from, to, info.messageSecret
341
+ );
342
+
343
+ const content = extractReportingTokenContent(plaintext, COMPILED);
344
+ if (!content || content.length === 0) return null;
345
+
346
+ const token = crypto.createHmac('sha256', reportingSecret)
347
+ .update(content).digest().subarray(0, 16);
348
+
349
+ return new BinaryNode('reporting', {}, [
350
+ new BinaryNode('reporting_token', { v: '2' }, token)
351
+ ]);
352
+ }
353
+
354
+ module.exports = {
355
+ buildReportingNode,
356
+ shouldIncludeReportingToken,
357
+ extractReportingTokenContent,
358
+ generateMsgSecretKey,
359
+ inspectMessage,
360
+ decodeVarint,
361
+ encodeVarint,
362
+ REPORTING_FIELDS,
363
+ COMPILED
364
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whalibmob",
3
- "version": "5.7.1",
3
+ "version": "5.7.2",
4
4
  "description": "WhatsApp library for interaction with WhatsApp Mobile API no web",
5
5
  "author": "Kunboruto50",
6
6
  "main": "index.js",