whalibmob 5.33.29 → 5.33.30

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.
@@ -27,7 +27,7 @@ const { uploadMedia } = require('../MediaService');
27
27
  const { imageThumbnail, videoThumbnail, probeImageSize } = require('../MediaThumbnail');
28
28
  const { tcTokenExpired, shouldSendNewTcToken, normalizeJidForTcToken,
29
29
  isRegularTcTokenUser } = require('./TcTokenStore');
30
- const { buildReportingNode } = require('./ReportingToken');
30
+ const { buildReportingNode, ensureMessageSecret } = require('./ReportingToken');
31
31
 
32
32
  // ─── Helpers ──────────────────────────────────────────────────────────────────
33
33
 
@@ -937,6 +937,13 @@ class MessageSender {
937
937
  const recipientPhone = phoneFromJid(toJid);
938
938
  const ownPhone = String(this._store.phoneNumber);
939
939
  const ownMainJid = `${ownPhone}@s.whatsapp.net`;
940
+
941
+ // Stamp the message secret the native client puts on every reportable
942
+ // message, before the plaintext is wrapped in a DeviceSentMessage, cached
943
+ // for retries, or encrypted. A no-op for messages that carry no reporting
944
+ // token or already hold a secret (a poll), so it only ever adds the field
945
+ // that was missing from plain text and media. See ensureMessageSecret.
946
+ plaintext = ensureMessageSecret(plaintext);
940
947
  const mediaSubtype = options._mediaSubtype || null;
941
948
  const decryptFail = resolveDecryptFail(mediaType, options);
942
949
 
@@ -1080,8 +1087,11 @@ class MessageSender {
1080
1087
  // it and have the server verify the reported text is what was actually
1081
1088
  // sent. Sending the secret without the token is a combination no real
1082
1089
  // client produces.
1090
+ // Our own identity in the same address family as the destination — the
1091
+ // original sender half of the token's key derivation.
1092
+ const ownReportingJid = isLidTarget ? `${ownLidUser}@lid` : ownMainJid;
1083
1093
  const reportingNode = buildReportingNode({
1084
- plaintext, msgId, remoteJid: routingToJid, BinaryNode
1094
+ plaintext, msgId, remoteJid: routingToJid, senderJid: ownReportingJid, BinaryNode
1085
1095
  });
1086
1096
  if (reportingNode) {
1087
1097
  msgContent.push(reportingNode);
@@ -1233,6 +1243,12 @@ class MessageSender {
1233
1243
  const mediaSubtype = options._mediaSubtype || null;
1234
1244
  const decryptFail = resolveDecryptFail(mediaType, options);
1235
1245
 
1246
+ // Same message secret the native client stamps on reportable group
1247
+ // messages, before the plaintext is cached or encrypted. A no-op for a poll
1248
+ // (already carries one) or a reaction (carries no token). See
1249
+ // ensureMessageSecret.
1250
+ plaintext = ensureMessageSecret(plaintext);
1251
+
1236
1252
  const isStatus = isStatusBroadcast(groupJid);
1237
1253
 
1238
1254
  // A Status has no group metadata; its recipients come from the account's
@@ -1495,7 +1511,7 @@ class MessageSender {
1495
1511
  // is the common case: it has a message secret, so it needs the token that
1496
1512
  // lets a report on it be verified.
1497
1513
  const groupReportingNode = buildReportingNode({
1498
- plaintext, msgId, remoteJid: groupJid, BinaryNode
1514
+ plaintext, msgId, remoteJid: groupJid, senderJid: ownJid, BinaryNode
1499
1515
  });
1500
1516
  if (groupReportingNode) {
1501
1517
  msgContent.push(groupReportingNode);
@@ -16,13 +16,17 @@
16
16
  // not change the token. REPORTING_FIELDS below is that subset, and it is the
17
17
  // part that has to match byte for byte or the token is worthless.
18
18
  //
19
- // The key is derived using the destination JID for both halves. Since the
20
- // server only checks the token when a report is actually filed, the choice is
21
- // not observable on send.
19
+ // The key is derived from the message secret, keyed by the two identities of
20
+ // the exchange: our own JID as the original sender and the destination JID as
21
+ // the party the modification is addressed to — the same binding the native
22
+ // client uses (origMsgSender = sender, modificationSender = recipient). The
23
+ // server recomputes with those same two identities when a report is filed, so
24
+ // getting them wrong leaves a token that no report could ever verify.
22
25
 
23
26
  const crypto = require('crypto');
24
27
  const { hkdfSha256 } = require('../hkdf');
25
28
  const { sha256 } = require('@noble/hashes/sha256');
29
+ const { encodeMessageContextInfo } = require('../proto/MessageProto');
26
30
 
27
31
  // Protobuf field numbers on Message that the token is computed over.
28
32
  // f field number
@@ -315,6 +319,38 @@ function shouldIncludeReportingToken(present) {
315
319
  !present.has(F_POLL_UPDATE);
316
320
  }
317
321
 
322
+ /**
323
+ * Give an outgoing message the message secret the native client puts on
324
+ * everything reportable, so the reporting token below can actually be built.
325
+ *
326
+ * The native client mints a random 32-byte secret into
327
+ * MessageContextInfo.messageSecret for every message that carries a reporting
328
+ * token, and reuses an existing one when the message already has it (a poll
329
+ * writes its own secret there). Without this step ordinary text and media go
330
+ * out with no secret and therefore no <reporting> node at all — a shape no real
331
+ * client produces, and the gap this function closes.
332
+ *
333
+ * Returns the plaintext unchanged when the message is one that carries no token
334
+ * (reactions, poll votes) or already has a secret; otherwise returns a new
335
+ * buffer with field 35 appended. Field 35 is not part of REPORTING_FIELDS, so
336
+ * appending it never changes the token's content hash. On malformed input the
337
+ * plaintext is returned untouched.
338
+ *
339
+ * @param {Buffer} plaintext encoded Message protobuf, before encryption
340
+ * @returns {Buffer}
341
+ */
342
+ function ensureMessageSecret(plaintext) {
343
+ if (!Buffer.isBuffer(plaintext)) return plaintext;
344
+ const info = inspectMessage(plaintext);
345
+ if (!info.ok) return plaintext;
346
+ if (!shouldIncludeReportingToken(info.present)) return plaintext;
347
+ if (info.messageSecret && info.messageSecret.length) return plaintext;
348
+
349
+ const ctx = encodeMessageContextInfo({ messageSecret: crypto.randomBytes(32) });
350
+ if (!ctx || ctx.length === 0) return plaintext;
351
+ return Buffer.concat([plaintext, ctx]);
352
+ }
353
+
318
354
  /**
319
355
  * Build the <reporting> node for an outgoing message, or null when there is
320
356
  * nothing to attach.
@@ -322,21 +358,25 @@ function shouldIncludeReportingToken(present) {
322
358
  * @param {object} opts
323
359
  * plaintext encoded Message protobuf, exactly as it will be encrypted
324
360
  * msgId the stanza id
325
- * remoteJid the destination JID
326
- * participant set only on a targeted resend; undefined otherwise
361
+ * remoteJid the destination JID (recipient for a DM, group JID for a group)
362
+ * senderJid our own JID — the original sender in the key derivation;
363
+ * falls back to remoteJid when unset
327
364
  * BinaryNode the node constructor
328
365
  */
329
366
  function buildReportingNode(opts) {
330
- const { plaintext, msgId, remoteJid, participant, BinaryNode } = opts;
367
+ const { plaintext, msgId, remoteJid, senderJid, BinaryNode } = opts;
331
368
  if (!Buffer.isBuffer(plaintext) || !msgId || !remoteJid) return null;
332
369
 
333
370
  const info = inspectMessage(plaintext);
334
371
  if (!info.ok || !info.messageSecret || !info.messageSecret.length) return null;
335
372
  if (!shouldIncludeReportingToken(info.present)) return null;
336
373
 
337
- // fromMe is always true here — we only ever build this for our own sends.
338
- const from = String(remoteJid);
339
- const to = String(participant || remoteJid);
374
+ // fromMe is always true here — we only ever build this for our own sends, so
375
+ // the original sender is us and the modification is addressed to the
376
+ // destination. Keyed the other way (both halves the destination), the token
377
+ // was present but no filed report could verify it.
378
+ const from = String(senderJid || remoteJid); // origMsgSender = us
379
+ const to = String(remoteJid); // modificationSender = destination
340
380
 
341
381
  const reportingSecret = generateMsgSecretKey(
342
382
  ENC_SECRET_REPORT_TOKEN, msgId, from, to, info.messageSecret
@@ -355,6 +395,7 @@ function buildReportingNode(opts) {
355
395
 
356
396
  module.exports = {
357
397
  buildReportingNode,
398
+ ensureMessageSecret,
358
399
  shouldIncludeReportingToken,
359
400
  extractReportingTokenContent,
360
401
  generateMsgSecretKey,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whalibmob",
3
- "version": "5.33.29",
3
+ "version": "5.33.30",
4
4
  "description": "Node.js library for WhatsApp — register a number over SMS, or link as a companion by QR. Signal E2E encryption, media, groups, channels.",
5
5
  "author": "Kunboruto20",
6
6
  "main": "index.js",