whalibmob 5.5.21 → 5.5.23

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.
@@ -1,10 +1,11 @@
1
1
  'use strict';
2
2
 
3
- const crypto = require('crypto');
3
+ const crypto = require('crypto');
4
+ const { Mutex } = require('async-mutex');
4
5
  const path = require('path');
5
6
  const fs = require('fs');
6
7
  const { BinaryNode } = require('../BinaryNode');
7
- const { DeviceManager } = require('../DeviceManager');
8
+ const { DeviceManager, jidStrToObj } = require('../DeviceManager');
8
9
  const {
9
10
  encodeMessage,
10
11
  encodeText, encodeImageMessage, encodeVideoMessage,
@@ -178,13 +179,19 @@ function computePhash(jids) {
178
179
 
179
180
  class MessageSender {
180
181
  constructor(client) {
181
- this._client = client;
182
- this._socket = client._socket;
183
- this._store = client._store;
184
- this._signal = client._signal;
185
- this._devMgr = client._devMgr;
186
- this._mediaHosts = [];
187
- this._mediaAuth = '';
182
+ this._client = client;
183
+ this._socket = client._socket;
184
+ this._store = client._store;
185
+ this._signal = client._signal;
186
+ this._devMgr = client._devMgr;
187
+ this._mediaHosts = [];
188
+ this._mediaAuth = '';
189
+ // Mutex that serialises Signal encryption across concurrent sends.
190
+ // Signal session state is shared per-process; concurrent encrypt calls on
191
+ // the same session can corrupt the ratchet chain and produce undecryptable
192
+ // ciphertexts. Holding the lock for the duration of the encrypt block
193
+ // (not the full send) keeps serialisation cost minimal.
194
+ this._encryptMutex = new Mutex();
188
195
  }
189
196
 
190
197
  // Called by Client when media connection is established
@@ -553,12 +560,42 @@ class MessageSender {
553
560
  // Primary devices can always use pkmsg to establish new Signal sessions —
554
561
  // they simply omit the device-identity node (server accepts this for primaries).
555
562
  const allowPkmsg = true;
556
- const [recipientDevices, ownDevices] = await Promise.all([
563
+
564
+ // First, run the phone-based usync (and own devices) in parallel.
565
+ // IMPORTANT: _pnToLid may be empty RIGHT NOW but gets populated during this
566
+ // await — incoming messages from the recipient (which arrive while we wait
567
+ // for the usync timeout) tell us their LID JID via from=LID + sender_pn attrs.
568
+ // We therefore check _pnToLid AFTER this await, not before.
569
+ const [_recipientDevicesByPhone, ownDevices] = await Promise.all([
557
570
  this._devMgr.bulkEnsureSessions([recipientPhone], this._signal, allowPkmsg),
558
571
  this._devMgr.ensureOwnDeviceSessions(ownPhone, this._signal, allowPkmsg)
559
572
  ]);
560
573
 
561
- const otherJids = recipientDevices.length > 0 ? recipientDevices : [toJid];
574
+ // ── LID routing fix ───────────────────────────────────────────────────────
575
+ // Check _pnToLid NOW — after the await above, incoming messages from the
576
+ // recipient during the usync wait will have populated this map.
577
+ // For LID-migrated accounts, the message `to` field AND Signal participants
578
+ // must use the LID JID. Phone-JID messages are silently accepted by the
579
+ // server but never delivered to the recipient's LID-registered device.
580
+ const lidUser = this._client._pnToLid && this._client._pnToLid.get(recipientPhone);
581
+ const routingToJid = lidUser ? `${lidUser}@lid` : toJid;
582
+
583
+ process.stderr.write('[DBG] DM_ROUTE phone=' + recipientPhone +
584
+ ' routing=' + routingToJid + (lidUser ? ' (LID)' : ' (PN)') + '\n');
585
+
586
+ let otherJids;
587
+ if (lidUser) {
588
+ // Fetch bundle + build Signal session for the LID JID.
589
+ // skipUsync=true: we already know the LID from _pnToLid — skip a second
590
+ // 15 s usync round-trip and go straight to bundle fetch (device 0 primary).
591
+ const lidDevices = await this._devMgr.bulkEnsureSessionsForLid(
592
+ lidUser, this._signal, allowPkmsg, /* skipUsync */ true);
593
+ otherJids = lidDevices.length > 0 ? lidDevices : [routingToJid];
594
+ } else {
595
+ otherJids = _recipientDevicesByPhone.length > 0 ? _recipientDevicesByPhone : [toJid];
596
+ }
597
+ // ─────────────────────────────────────────────────────────────────────────
598
+
562
599
  const ownLinkedJids = ownDevices.filter(j => j !== ownMainJid);
563
600
 
564
601
  const allParticipants = [...otherJids, ...ownLinkedJids];
@@ -568,16 +605,22 @@ class MessageSender {
568
605
  ? encodeDeviceSentMessage(toJid, plaintext, phash)
569
606
  : null;
570
607
 
571
- const [otherEncrypted, ownEncrypted] = await Promise.all([
572
- this._signal.bulkEncryptForDevices(otherJids, plaintext),
573
- ownLinkedJids.length > 0
574
- ? this._signal.bulkEncryptForDevices(ownLinkedJids, dsmBuf)
575
- : Promise.resolve([])
576
- ]);
608
+ // Acquire mutex before touching Signal sessions — concurrent sends on the
609
+ // same session corrupt the ratchet chain and produce undecryptable messages.
610
+ const [otherEncrypted, ownEncrypted] = await this._encryptMutex.runExclusive(async () => {
611
+ const other = await this._signal.bulkEncryptForDevices(otherJids, plaintext);
612
+ const own = ownLinkedJids.length > 0
613
+ ? await this._signal.bulkEncryptForDevices(ownLinkedJids, dsmBuf)
614
+ : [];
615
+ return [other, own];
616
+ });
577
617
 
578
618
  const encryptedList = [...otherEncrypted, ...ownEncrypted];
579
619
 
580
- const stanzaAttrs = { to: toJid, id: msgId, type: mediaType, t: String(msNow()) };
620
+ // Encode the routing JID as a binary JID object (JID_PAIR for @lid / @s.whatsapp.net).
621
+ // Raw UTF-8 strings are not recognised by the server for LID recipients, causing
622
+ // the message to be accepted (server ACK) but never delivered to the device.
623
+ const stanzaAttrs = { to: jidStrToObj(routingToJid), id: msgId, type: mediaType, t: String(msNow()) };
581
624
  if (phash) stanzaAttrs.phash = phash;
582
625
  if (options.edit) stanzaAttrs.edit = String(options.edit);
583
626
 
@@ -599,6 +642,19 @@ class MessageSender {
599
642
 
600
643
  const msgNode = new BinaryNode('message', stanzaAttrs, msgContent);
601
644
 
645
+ // Debug: log outgoing stanza details
646
+ process.stderr.write('[DBG] DM_SEND to=' + toJid +
647
+ ' otherJids=[' + otherJids.join(',') + ']' +
648
+ ' ownLinked=[' + ownLinkedJids.join(',') + ']' +
649
+ ' encrypted=' + encryptedList.length +
650
+ ' hasPkmsg=' + hasPkmsg +
651
+ ' hasAdv=' + !!(advBytes) +
652
+ '\n');
653
+ if (encryptedList.length > 0) {
654
+ process.stderr.write('[DBG] DM_PARTICIPANTS ' +
655
+ encryptedList.map(e => e.jid + '(' + e.type + ')').join(', ') + '\n');
656
+ }
657
+
602
658
  // Cache plaintext so Client can re-send with fresh session on recipient retry
603
659
  if (this._client._sentMsgCache) {
604
660
  this._client._sentMsgCache.set(msgId, {
@@ -675,13 +731,18 @@ class MessageSender {
675
731
  const existingSkdmMap = skStore.getSKDMMap(groupJid);
676
732
  const skdmRecipients = allTargets.filter(jid => !existingSkdmMap[jid]);
677
733
 
734
+ // Acquire mutex for all group encryption — SKDM fanout and senderKey encrypt
735
+ // share the same Signal session state and must not run concurrently.
678
736
  let skdmEncrypted = [];
679
- if (skdmRecipients.length > 0) {
680
- skdmEncrypted = await this._signal.bulkEncryptForDevices(skdmRecipients, skdmMsg);
681
- skStore.markSKDMSent(groupJid, skdmRecipients);
682
- }
737
+ let skmsgCiphertext;
738
+ await this._encryptMutex.runExclusive(async () => {
739
+ if (skdmRecipients.length > 0) {
740
+ skdmEncrypted = await this._signal.bulkEncryptForDevices(skdmRecipients, skdmMsg);
741
+ skStore.markSKDMSent(groupJid, skdmRecipients);
742
+ }
743
+ skmsgCiphertext = this._signal.senderKeyEncrypt(groupJid, senderIdentity, plaintext);
744
+ });
683
745
 
684
- const skmsgCiphertext = this._signal.senderKeyEncrypt(groupJid, senderIdentity, plaintext);
685
746
  const phash = phashTargets.length > 0 ? computePhash(phashTargets) : null;
686
747
 
687
748
  const skdmHasPkmsg = skdmEncrypted.some(e => e.type === 'pkmsg');
@@ -699,7 +760,7 @@ class MessageSender {
699
760
  msgContent.push(new BinaryNode('enc', skmsgAttrs, Buffer.isBuffer(skmsgCiphertext) ? skmsgCiphertext : Buffer.from(skmsgCiphertext)));
700
761
 
701
762
  const stanzaAttrs = {
702
- to: groupJid,
763
+ to: jidStrToObj(groupJid),
703
764
  id: msgId,
704
765
  type: mediaType,
705
766
  addressing_mode: groupAddressingMode,
package/lib/noise.js CHANGED
@@ -9,6 +9,7 @@ const { hkdf } = require('@noble/hashes/hkdf');
9
9
  const { MOBILE_PROLOGUE, WHATSAPP_HOST, WHATSAPP_PORT } = require('./constants');
10
10
  const { encodeHandshakeClientHello, encodeHandshakeClientFinish,
11
11
  decodeServerHello, encodeClientPayload } = require('./proto');
12
+ const { parsePhone, getCountryMeta } = require('./Registration');
12
13
 
13
14
  // ─── CertChain validator ────────
14
15
  //
@@ -282,6 +283,10 @@ class NoiseSocket extends EventEmitter {
282
283
  const sharedSS = dhShared(noisePriv, serverHello.ephemeral);
283
284
  this.noiseState.mixKey(sharedSS);
284
285
 
286
+ // Auto-derive MCC/MNC and locale from the registered phone number so the
287
+ // ClientPayload userAgent is never sent with the telltale '000'/'000' values.
288
+ const _phoneMeta = getCountryMeta(parsePhone(this.store.phoneNumber).cc);
289
+
285
290
  const payload = encodeClientPayload({
286
291
  username: BigInt(this.store.phoneNumber),
287
292
  passive: false,
@@ -295,16 +300,16 @@ class NoiseSocket extends EventEmitter {
295
300
  userAgent: {
296
301
  platform: (this.store.device && this.store.device.platform) || 1,
297
302
  version: this.store.version,
298
- mcc: '000',
299
- mnc: '000',
303
+ mcc: _phoneMeta.mcc,
304
+ mnc: _phoneMeta.mnc,
300
305
  osVersion: this.store.device.osVersion,
301
306
  manufacturer: this.store.device.manufacturer,
302
307
  device: this.store.device.model,
303
308
  osBuildNumber: this.store.device.osBuildNumber,
304
309
  phoneId: this.store.fdid.toUpperCase(),
305
310
  releaseChannel: 0,
306
- localeLanguage: 'en',
307
- localeCountry: 'US',
311
+ localeLanguage: _phoneMeta.lg,
312
+ localeCountry: _phoneMeta.lc,
308
313
  deviceType: 0,
309
314
  deviceModelType: this.store.device.modelId
310
315
  }
@@ -26,6 +26,7 @@ class SignalStore {
26
26
  this._signedPreKeys = {};
27
27
  this._identities = {};
28
28
  this._filePath = null;
29
+ this._lidMappings = {}; // phone → lid — persisted alongside sessions
29
30
 
30
31
  // Debounce state
31
32
  this._dirty = false;
@@ -56,9 +57,24 @@ class SignalStore {
56
57
  this._preKeys = raw.preKeys || {};
57
58
  this._signedPreKeys = raw.signedPreKeys || {};
58
59
  this._identities = raw.identities || {};
60
+ this._lidMappings = raw.lidMappings || {}; // phone → lid, persisted
59
61
  } catch (_) {}
60
62
  }
61
63
 
64
+ // Persist a phone ↔ LID mapping. Called any time we learn a new mapping from
65
+ // incoming messages or from a contact usync fallback query.
66
+ setLidMapping(phone, lid) {
67
+ if (!phone || !lid) return;
68
+ if (this._lidMappings[phone] === lid) return; // already stored, skip disk write
69
+ this._lidMappings[phone] = lid;
70
+ this._save();
71
+ }
72
+
73
+ // Return all stored phone → lid mappings (used to populate Client._pnToLid on startup)
74
+ getLidMappings() {
75
+ return this._lidMappings;
76
+ }
77
+
62
78
  // Immediate synchronous flush (used on process exit only)
63
79
  _flushSync() {
64
80
  if (!this._dirty || !this._filePath) return;
@@ -68,7 +84,8 @@ class SignalStore {
68
84
  sessions: this._sessions,
69
85
  preKeys: this._preKeys,
70
86
  signedPreKeys: this._signedPreKeys,
71
- identities: this._identities
87
+ identities: this._identities,
88
+ lidMappings: this._lidMappings
72
89
  });
73
90
  const dir = path.dirname(this._filePath);
74
91
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
@@ -89,7 +106,8 @@ class SignalStore {
89
106
  sessions: this._sessions,
90
107
  preKeys: this._preKeys,
91
108
  signedPreKeys: this._signedPreKeys,
92
- identities: this._identities
109
+ identities: this._identities,
110
+ lidMappings: this._lidMappings
93
111
  });
94
112
  const dir = path.dirname(this._filePath);
95
113
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whalibmob",
3
- "version": "5.5.21",
3
+ "version": "5.5.23",
4
4
  "description": "WhatsApp library for interaction with WhatsApp Mobile API no web",
5
5
  "author": "Kunboruto20",
6
6
  "main": "index.js",