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.
- package/README.md +151 -0
- package/cli.js +13 -7
- package/index.js +12 -1
- package/lib/Client.js +19 -0
- package/lib/DeviceManager.js +461 -52
- package/lib/Registration.js +10 -5
- package/lib/auth-utils.js +693 -0
- package/lib/messages/MessageSender.js +85 -24
- package/lib/noise.js +9 -4
- package/lib/signal/SignalStore.js +20 -2
- package/package.json +1 -1
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const 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
|
|
182
|
-
this._socket
|
|
183
|
-
this._store
|
|
184
|
-
this._signal
|
|
185
|
-
this._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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
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
|
-
|
|
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
|
-
|
|
680
|
-
|
|
681
|
-
|
|
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:
|
|
299
|
-
mnc:
|
|
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:
|
|
307
|
-
localeCountry:
|
|
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 });
|