whalibmob 5.9.1 → 5.9.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.
- package/lib/Client.js +89 -97
- package/lib/DeviceManager.js +25 -26
- package/lib/MediaService.js +2 -2
- package/lib/MediaThumbnail.js +5 -7
- package/lib/auth-utils.js +11 -13
- package/lib/messages/MessageSender.js +60 -72
- package/lib/messages/ReportingToken.js +4 -6
- package/lib/messages/TcTokenStore.js +9 -15
- package/lib/noise.js +3 -4
- package/lib/proto/MessageProto.js +4 -4
- package/lib/signal/SignalProtocol.js +4 -4
- package/lib/signal/SignalStore.js +2 -2
- package/package.json +1 -1
package/lib/Client.js
CHANGED
|
@@ -51,8 +51,8 @@ const CALL_VIDEO_PREFIX = 'https://call.whatsapp.com/video/';
|
|
|
51
51
|
const INVITE_LINK_PREFIX = 'https://chat.whatsapp.com/';
|
|
52
52
|
|
|
53
53
|
// The names privacy settings actually go by on the wire, and the friendly
|
|
54
|
-
// aliases this library has always accepted for them.
|
|
55
|
-
//
|
|
54
|
+
// aliases this library has always accepted for them. The wire names are the
|
|
55
|
+
// right-hand column.
|
|
56
56
|
const PRIVACY_NAME_ALIASES = {
|
|
57
57
|
last_seen: 'last', last: 'last',
|
|
58
58
|
profile_picture: 'profile', profile_pic: 'profile', profile: 'profile',
|
|
@@ -80,8 +80,8 @@ const PRIVACY_WIRE_TO_KEY = {
|
|
|
80
80
|
stickers: 'stickers'
|
|
81
81
|
};
|
|
82
82
|
|
|
83
|
-
// Reduce a JID to its bare user
|
|
84
|
-
//
|
|
83
|
+
// Reduce a JID to its bare user. Blocking, and anything else that names an
|
|
84
|
+
// account rather than one of its devices, wants this form.
|
|
85
85
|
function toNonAdJid(input) {
|
|
86
86
|
const s = String(input || '');
|
|
87
87
|
const at = s.indexOf('@');
|
|
@@ -246,14 +246,14 @@ class WhalibmobClient extends EventEmitter {
|
|
|
246
246
|
this._tcTokenStore = null; // TcTokenStore — loaded in init()
|
|
247
247
|
this._inFlightTcTokenIssuance = new Set(); // dedupe concurrent proactive issuePrivacyTokens per JID
|
|
248
248
|
this._inFlight463Recoveries = new Set(); // dedupe concurrent 463-triggered token issuances per JID (separate from proactive)
|
|
249
|
-
// Blue-tick every message as it arrives. Set to false
|
|
250
|
-
//
|
|
249
|
+
// Blue-tick every message as it arrives. Set to false to make read receipts
|
|
250
|
+
// explicit, sent only by a markRead() call.
|
|
251
251
|
this.autoRead = opts.autoRead !== false;
|
|
252
252
|
// Flipped by setOnline(). Delivery receipts sent while this is false carry
|
|
253
253
|
// type="inactive", which the sender's app records but does not render.
|
|
254
254
|
this._sendActiveReceipts = false;
|
|
255
|
-
// Cached privacy settings
|
|
256
|
-
//
|
|
255
|
+
// Cached privacy settings. Warmed after connect and refreshed whenever the
|
|
256
|
+
// server reports a change; markRead reads it.
|
|
257
257
|
this._privacySettings = null;
|
|
258
258
|
if (opts.pino !== undefined) _whaConfigLogger(opts.pino);
|
|
259
259
|
}
|
|
@@ -731,9 +731,9 @@ class WhalibmobClient extends EventEmitter {
|
|
|
731
731
|
this._saveWebStore();
|
|
732
732
|
_whaDbg('[DBG] COMPANION_FINISH adv_secret derived');
|
|
733
733
|
|
|
734
|
-
//
|
|
735
|
-
//
|
|
736
|
-
//
|
|
734
|
+
// Awaited rather than fired and forgotten, so a server rejection surfaces
|
|
735
|
+
// as an error here instead of as a silent stall waiting for a
|
|
736
|
+
// pair-success that is never coming.
|
|
737
737
|
const finishResult = await this._sendIq(new BinaryNode('iq', {
|
|
738
738
|
to: 's.whatsapp.net',
|
|
739
739
|
type: 'set',
|
|
@@ -1810,10 +1810,9 @@ class WhalibmobClient extends EventEmitter {
|
|
|
1810
1810
|
// Companion pairing — these arrive unsolicited, so they are checked before
|
|
1811
1811
|
// the pending-IQ table rather than after it.
|
|
1812
1812
|
//
|
|
1813
|
-
// pair-device is matched on type=set; pair-success is matched on any type
|
|
1814
|
-
//
|
|
1815
|
-
//
|
|
1816
|
-
// drop the stanza that completes the link.
|
|
1813
|
+
// pair-device is matched on type=set; pair-success is matched on any type.
|
|
1814
|
+
// Requiring type=set on pair-success would drop the stanza that completes
|
|
1815
|
+
// the link.
|
|
1817
1816
|
if (this._mode === 'web') {
|
|
1818
1817
|
if (type === 'set' && findChild(node, 'pair-device')) { this._handlePairDevice(node); return; }
|
|
1819
1818
|
if (findChild(node, 'pair-success')) { this._handlePairSuccess(node); return; }
|
|
@@ -2001,8 +2000,8 @@ class WhalibmobClient extends EventEmitter {
|
|
|
2001
2000
|
this._sendReadReceipt(id, fromRaw, partRaw);
|
|
2002
2001
|
|
|
2003
2002
|
// ── pkmsg = peer opened a new Signal session (identity/device change) ──
|
|
2004
|
-
// Re-issue our tcToken to them so the fresh session carries a valid
|
|
2005
|
-
//
|
|
2003
|
+
// Re-issue our tcToken to them so the fresh session carries a valid
|
|
2004
|
+
// token.
|
|
2006
2005
|
if (encType === 'pkmsg' && this._tcTokenStore) {
|
|
2007
2006
|
this._reissueTcTokenAfterIdentityChange(sigJid);
|
|
2008
2007
|
}
|
|
@@ -2250,10 +2249,10 @@ class WhalibmobClient extends EventEmitter {
|
|
|
2250
2249
|
}
|
|
2251
2250
|
|
|
2252
2251
|
// A read receipt only tells the sender we read it when read receipts are on.
|
|
2253
|
-
//
|
|
2254
|
-
//
|
|
2255
|
-
//
|
|
2256
|
-
//
|
|
2252
|
+
// With them off, "read" is downgraded to "read-self", which marks the message
|
|
2253
|
+
// read across our own devices without reporting it to the other side. Nothing
|
|
2254
|
+
// here looked at the setting, so a user who had turned read receipts off
|
|
2255
|
+
// still sent blue ticks.
|
|
2257
2256
|
_readReceiptType(base) {
|
|
2258
2257
|
const settings = this._privacySettings;
|
|
2259
2258
|
if (settings && settings.readReceipts === 'none' && base === 'read') {
|
|
@@ -2264,9 +2263,8 @@ class WhalibmobClient extends EventEmitter {
|
|
|
2264
2263
|
|
|
2265
2264
|
// The key a privacy token is stored and looked up under.
|
|
2266
2265
|
//
|
|
2267
|
-
//
|
|
2268
|
-
//
|
|
2269
|
-
// Baileys' resolveTcTokenJid are the same three lines:
|
|
2266
|
+
// It resolves to the contact's LID, falling back to the plain JID only when
|
|
2267
|
+
// no LID is known:
|
|
2270
2268
|
//
|
|
2271
2269
|
// if (isLidUser(jid)) return jid
|
|
2272
2270
|
// const lid = await getLIDForPN(jid)
|
|
@@ -2289,9 +2287,9 @@ class WhalibmobClient extends EventEmitter {
|
|
|
2289
2287
|
// The in-memory map starts empty and is filled by usync and by incoming
|
|
2290
2288
|
// messages, so a send early in a process — or one that hits a warm device
|
|
2291
2289
|
// cache and never runs usync — could find nothing and fall back to the
|
|
2292
|
-
// phone JID. The mapping is on disk in the Signal store either way,
|
|
2293
|
-
//
|
|
2294
|
-
//
|
|
2290
|
+
// phone JID. The mapping is on disk in the Signal store either way, so
|
|
2291
|
+
// consult that before giving up. The consequence of not doing so is not
|
|
2292
|
+
// cosmetic: the lookup lands on
|
|
2295
2293
|
// a key with no token while a live one sits under the LID, and the message
|
|
2296
2294
|
// goes out without one.
|
|
2297
2295
|
if (!lidUser) {
|
|
@@ -2325,9 +2323,8 @@ class WhalibmobClient extends EventEmitter {
|
|
|
2325
2323
|
//
|
|
2326
2324
|
// Storage is always the LID. Issuance follows AB prop 14303
|
|
2327
2325
|
// (lid_trusted_token_issue_to_lid), whose WA Web default is off — so the IQ
|
|
2328
|
-
// names the phone JID.
|
|
2329
|
-
//
|
|
2330
|
-
// follow WA Web, and more importantly we now follow it in one place: the
|
|
2326
|
+
// names the phone JID. We follow WA Web, and more importantly we now follow
|
|
2327
|
+
// it in one place: the
|
|
2331
2328
|
// send path, the 463 recovery and the post-identity-change re-issue used to
|
|
2332
2329
|
// each pick a different address family for the same contact.
|
|
2333
2330
|
_resolveTcTokenIssuanceJid(jid) {
|
|
@@ -2340,9 +2337,9 @@ class WhalibmobClient extends EventEmitter {
|
|
|
2340
2337
|
|
|
2341
2338
|
// The one way to read a token that is safe to put on the wire.
|
|
2342
2339
|
//
|
|
2343
|
-
//
|
|
2344
|
-
//
|
|
2345
|
-
//
|
|
2340
|
+
// Every attach site goes through here, and an expired token returns nothing.
|
|
2341
|
+
// The expiry check used to be on the message path only; presence subscribe,
|
|
2342
|
+
// group create, participant add and the
|
|
2346
2343
|
// profile-picture query each tested `token.length` and nothing else, so once
|
|
2347
2344
|
// a token aged past the 28-day window those four kept sending it. Same rule
|
|
2348
2345
|
// everywhere now, and the dead token gets cleared on the way out.
|
|
@@ -2373,10 +2370,10 @@ class WhalibmobClient extends EventEmitter {
|
|
|
2373
2370
|
// Delivery receipt — the sender's two grey ticks.
|
|
2374
2371
|
//
|
|
2375
2372
|
// Distinct from the <ack> above: the ack tells the server we took the stanza,
|
|
2376
|
-
// the receipt tells the sender it reached a device.
|
|
2377
|
-
//
|
|
2378
|
-
//
|
|
2379
|
-
//
|
|
2373
|
+
// the receipt tells the sender it reached a device. Both go out for every
|
|
2374
|
+
// incoming message, with the blue ticks left to markRead. Only the ack used
|
|
2375
|
+
// to be sent, jumping straight to type="read", so a sender never saw the grey
|
|
2376
|
+
// stage.
|
|
2380
2377
|
//
|
|
2381
2378
|
// A client that has not marked itself available sends this with
|
|
2382
2379
|
// type="inactive": the sender's app records the delivery but does not render
|
|
@@ -2399,7 +2396,7 @@ class WhalibmobClient extends EventEmitter {
|
|
|
2399
2396
|
//
|
|
2400
2397
|
// Sent automatically after a message decodes unless autoRead is turned off, in
|
|
2401
2398
|
// which case marking a chat read is up to the caller via markRead(). Turning
|
|
2402
|
-
// it off
|
|
2399
|
+
// it off makes read receipts always explicit.
|
|
2403
2400
|
// fromJid / partJid are raw JID objects (AD_JID encoding for correct routing).
|
|
2404
2401
|
_sendReadReceipt(msgId, fromJid, partJid) {
|
|
2405
2402
|
if (!this._socket || !this._connected) return;
|
|
@@ -2543,8 +2540,8 @@ class WhalibmobClient extends EventEmitter {
|
|
|
2543
2540
|
}
|
|
2544
2541
|
|
|
2545
2542
|
// <blocklist action="..." dhash="..." prev_dhash="..."><item jid=... action=.../></blocklist>
|
|
2546
|
-
//
|
|
2547
|
-
//
|
|
2543
|
+
// The action on the node describes the whole change; each child names one JID
|
|
2544
|
+
// and what happened to it.
|
|
2548
2545
|
_handleBlocklistNotification(node) {
|
|
2549
2546
|
const attrs = node.attrs || {};
|
|
2550
2547
|
const changes = [];
|
|
@@ -2587,9 +2584,9 @@ class WhalibmobClient extends EventEmitter {
|
|
|
2587
2584
|
// blocked only ever changed when it did the blocking itself.
|
|
2588
2585
|
const blocklistNode = findChildDeep(node, 'blocklist');
|
|
2589
2586
|
if (blocklistNode) this._handleBlocklistNotification(blocklistNode);
|
|
2590
|
-
// ...and privacy setting changes, which arrive the same way.
|
|
2591
|
-
// re-
|
|
2592
|
-
//
|
|
2587
|
+
// ...and privacy setting changes, which arrive the same way. The settings
|
|
2588
|
+
// are re-read on this and an event raised; nothing was watching the node,
|
|
2589
|
+
// so a change made on the phone never reached the cache.
|
|
2593
2590
|
const privacyNode = findChildDeep(node, 'privacy');
|
|
2594
2591
|
if (privacyNode) this._handlePrivacySettingsNotification(privacyNode);
|
|
2595
2592
|
}
|
|
@@ -2847,9 +2844,9 @@ class WhalibmobClient extends EventEmitter {
|
|
|
2847
2844
|
return;
|
|
2848
2845
|
}
|
|
2849
2846
|
|
|
2850
|
-
// Not every failure kills the session.
|
|
2851
|
-
//
|
|
2852
|
-
//
|
|
2847
|
+
// Not every failure kills the session. These five do — 401, 403, 405, 406,
|
|
2848
|
+
// 409 — and the rest are worth another attempt. A 500 or a 503 is the
|
|
2849
|
+
// server having a bad minute; giving
|
|
2853
2850
|
// up on the session for that would be throwing away a working registration.
|
|
2854
2851
|
const LOGGED_OUT = ['401', '403', '405', '406', '409'];
|
|
2855
2852
|
const spent = LOGGED_OUT.includes(reason);
|
|
@@ -3289,8 +3286,6 @@ class WhalibmobClient extends EventEmitter {
|
|
|
3289
3286
|
* Send a `<iq type='set' xmlns='privacy'>` to issue a trusted-contact token
|
|
3290
3287
|
* for `jid`. Returns the raw result node (or null on timeout).
|
|
3291
3288
|
*
|
|
3292
|
-
* Mirrors Baileys' `issuePrivacyTokens` in messages-send.js.
|
|
3293
|
-
*
|
|
3294
3289
|
* @param {string} jid - Bare or full JID of the conversation partner
|
|
3295
3290
|
* @param {number} timestamp - Unix seconds to use as the token timestamp
|
|
3296
3291
|
*/
|
|
@@ -3378,13 +3373,12 @@ class WhalibmobClient extends EventEmitter {
|
|
|
3378
3373
|
: (typeof tokenNode.content === 'string' && tokenNode.content.length > 0)
|
|
3379
3374
|
? Buffer.from(tokenNode.content, 'base64')
|
|
3380
3375
|
: null;
|
|
3381
|
-
//
|
|
3382
|
-
//
|
|
3383
|
-
// → always prefer fallbackJid; only fall back to a.jid when fallbackJid is absent
|
|
3376
|
+
// In a notification, tokenNode.attrs.jid is our own device JID rather
|
|
3377
|
+
// than the sender's, so fallbackJid wins whenever it is present.
|
|
3384
3378
|
const jid = fallbackJid || String(a.jid);
|
|
3385
3379
|
const t = a.t ? parseInt(String(a.t), 10) : issueTs;
|
|
3386
|
-
//
|
|
3387
|
-
//
|
|
3380
|
+
// Anything that is not a regular user is dropped here, guarding
|
|
3381
|
+
// against a malformed or spoofed notification filing a token under
|
|
3388
3382
|
// the PSA account or a bot.
|
|
3389
3383
|
if (!isRegularTcTokenUser(jid)) {
|
|
3390
3384
|
_whaDbg('[DBG] STORE_TCTOKEN skipped non-user jid=' + jid);
|
|
@@ -3407,8 +3401,8 @@ class WhalibmobClient extends EventEmitter {
|
|
|
3407
3401
|
}
|
|
3408
3402
|
}
|
|
3409
3403
|
|
|
3410
|
-
// ── CRITICAL:
|
|
3411
|
-
// ── regardless of whether server returned token bytes.
|
|
3404
|
+
// ── CRITICAL: senderTimestamp is ALWAYS persisted after issuing, ─────
|
|
3405
|
+
// ── regardless of whether the server returned token bytes. ─────
|
|
3412
3406
|
// ── Without this, shouldSendNewTcToken() returns true every send and ─────
|
|
3413
3407
|
// ── we flood the server with privacy IQs. ─────
|
|
3414
3408
|
// ── For incoming privacy_token notifications saveSenderTs=false so ─────
|
|
@@ -3427,14 +3421,14 @@ class WhalibmobClient extends EventEmitter {
|
|
|
3427
3421
|
/**
|
|
3428
3422
|
* Handle a server-pushed `<notification type='privacy_token'>`.
|
|
3429
3423
|
*
|
|
3430
|
-
* Wire format
|
|
3424
|
+
* Wire format:
|
|
3431
3425
|
* <notification type='privacy_token' from='...' [sender_lid='...@lid']>
|
|
3432
3426
|
* <tokens>
|
|
3433
3427
|
* <token type='trusted_contact' jid='...' t='...'>…bytes…</token>
|
|
3434
3428
|
* </tokens>
|
|
3435
3429
|
* </notification>
|
|
3436
3430
|
*
|
|
3437
|
-
* Key rules
|
|
3431
|
+
* Key rules:
|
|
3438
3432
|
* 1. Must find <tokens> wrapper inside notification; abort if absent.
|
|
3439
3433
|
* 2. storage key = sender_lid (if present and @lid) else PN→LID lookup else from-JID.
|
|
3440
3434
|
* 3. senderTimestamp is NOT updated here — only the peer-token bytes + timestamp.
|
|
@@ -3449,7 +3443,7 @@ class WhalibmobClient extends EventEmitter {
|
|
|
3449
3443
|
_whaDbg('[DBG] PRIVACY_TOKEN_NOTIF_ENTER from=' + String(attrs.from || '') +
|
|
3450
3444
|
' sender_lid=' + String(attrs.sender_lid || ''));
|
|
3451
3445
|
|
|
3452
|
-
// ── 1. Require <tokens> wrapper
|
|
3446
|
+
// ── 1. Require the <tokens> wrapper ────────────────────────────────────────
|
|
3453
3447
|
const content = Array.isArray(node.content) ? node.content : [];
|
|
3454
3448
|
const tokensNode = content.find(n => n && n.description === 'tokens');
|
|
3455
3449
|
if (!tokensNode) {
|
|
@@ -3458,7 +3452,7 @@ class WhalibmobClient extends EventEmitter {
|
|
|
3458
3452
|
}
|
|
3459
3453
|
|
|
3460
3454
|
// ── 2. Resolve storage JID ─────────────────────────────────────────────────
|
|
3461
|
-
// Priority
|
|
3455
|
+
// Priority:
|
|
3462
3456
|
// 1. sender_lid attr if it is a @lid JID
|
|
3463
3457
|
// 2. from user is a known LID user (LID comes as @s.whatsapp.net in notification)
|
|
3464
3458
|
// 3. from is a PN → look up LID in _pnToLid
|
|
@@ -3494,7 +3488,7 @@ class WhalibmobClient extends EventEmitter {
|
|
|
3494
3488
|
* Called when we successfully decrypt a pkmsg — the peer opened a fresh Signal
|
|
3495
3489
|
* session (reset or new device), so we re-issue a privacy token so they can
|
|
3496
3490
|
* reach us. Only fires if we've previously issued a token (senderTimestamp
|
|
3497
|
-
* present and not expired)
|
|
3491
|
+
* present and not expired).
|
|
3498
3492
|
*
|
|
3499
3493
|
* @param {string} from - Signal-session JID (PN or LID JID of the sender)
|
|
3500
3494
|
*/
|
|
@@ -3516,8 +3510,8 @@ class WhalibmobClient extends EventEmitter {
|
|
|
3516
3510
|
_whaDbg('[DBG] REISSUE_TC_TOKEN_AFTER_IDENTITY_CHANGE jid=' + tcJid +
|
|
3517
3511
|
' prevSenderTs=' + senderTs);
|
|
3518
3512
|
|
|
3519
|
-
//
|
|
3520
|
-
//
|
|
3513
|
+
// The re-issue carries the *previous* senderTimestamp, not a fresh one,
|
|
3514
|
+
// and the result is stored without touching the dedupe state. That
|
|
3521
3515
|
// matters: this is a repair of an existing grant after the peer's
|
|
3522
3516
|
// device changed, not a new grant, so it must not move the 7-day
|
|
3523
3517
|
// bucket that governs regular issuance. We were stamping it with
|
|
@@ -3614,7 +3608,7 @@ class WhalibmobClient extends EventEmitter {
|
|
|
3614
3608
|
* - Skips expired tokens (no point loading a token we'd immediately discard).
|
|
3615
3609
|
* - Storage key follows TcTokenStore convention: normalized bare JID.
|
|
3616
3610
|
* For LID chats (jid ends in @lid) we use the lid JID directly.
|
|
3617
|
-
* For PN chats we try to look up the LID first
|
|
3611
|
+
* For PN chats we try to look up the LID first, since storage is by LID.
|
|
3618
3612
|
*
|
|
3619
3613
|
* @param {object|null} merged - Return value of mergeHistoryToStore()
|
|
3620
3614
|
*/
|
|
@@ -3642,7 +3636,7 @@ class WhalibmobClient extends EventEmitter {
|
|
|
3642
3636
|
continue;
|
|
3643
3637
|
}
|
|
3644
3638
|
|
|
3645
|
-
// Resolve storage JID
|
|
3639
|
+
// Resolve storage JID — storage is always by LID.
|
|
3646
3640
|
// If this chat is a @lid jid, use it directly.
|
|
3647
3641
|
// If it's a PN jid, try to find its LID counterpart.
|
|
3648
3642
|
let storageJid;
|
|
@@ -3801,9 +3795,9 @@ class WhalibmobClient extends EventEmitter {
|
|
|
3801
3795
|
// required in a group; type may be 'read' (default) or 'played'
|
|
3802
3796
|
// to mark a voice note as listened to.
|
|
3803
3797
|
//
|
|
3804
|
-
//
|
|
3805
|
-
//
|
|
3806
|
-
//
|
|
3798
|
+
// The FIRST id goes on the receipt and the rest as <item> children of a
|
|
3799
|
+
// <list> node. The last id used to be used instead, with the items straight
|
|
3800
|
+
// inside <receipt>, which is not a shape the server
|
|
3807
3801
|
// reads back — the extra ids were silently ignored, so only one message per
|
|
3808
3802
|
// call was ever marked. The participant was missing entirely, so a group read
|
|
3809
3803
|
// never told the server whose message had been read.
|
|
@@ -3890,8 +3884,8 @@ class WhalibmobClient extends EventEmitter {
|
|
|
3890
3884
|
|
|
3891
3885
|
// Set global online/offline status.
|
|
3892
3886
|
//
|
|
3893
|
-
//
|
|
3894
|
-
//
|
|
3887
|
+
// This is refused without a push name: the server takes the display name from
|
|
3888
|
+
// this node, and sending it empty makes
|
|
3895
3889
|
// every other user see "-" instead of a name. Going available also flips the
|
|
3896
3890
|
// client into sending active delivery receipts — see _sendDeliveryReceipt.
|
|
3897
3891
|
//
|
|
@@ -3914,9 +3908,9 @@ class WhalibmobClient extends EventEmitter {
|
|
|
3914
3908
|
// Set typing/recording presence in a specific chat.
|
|
3915
3909
|
// presence: 'composing' | 'paused' | 'recording'
|
|
3916
3910
|
//
|
|
3917
|
-
//
|
|
3918
|
-
//
|
|
3919
|
-
//
|
|
3911
|
+
// Both `from` (our own JID) and `to` go on the chatstate node, and recording
|
|
3912
|
+
// is expressed as composing with media="audio" — there is no separate
|
|
3913
|
+
// recording state in the protocol. The `from` was missing
|
|
3920
3914
|
// here, and both JIDs were raw strings, which the server ignores for @lid
|
|
3921
3915
|
// chats, so the indicator never showed for a LID-addressed contact.
|
|
3922
3916
|
setChatPresence(chatJid, presence) {
|
|
@@ -3940,18 +3934,17 @@ class WhalibmobClient extends EventEmitter {
|
|
|
3940
3934
|
|
|
3941
3935
|
// Subscribe to presence updates for contacts.
|
|
3942
3936
|
//
|
|
3943
|
-
//
|
|
3944
|
-
//
|
|
3945
|
-
//
|
|
3946
|
-
//
|
|
3937
|
+
// The contact's privacy token is attached when there is one; without it the
|
|
3938
|
+
// server may decline to send their presence. The server also only forwards
|
|
3939
|
+
// presence to clients that have marked themselves available, so call
|
|
3940
|
+
// setOnline(true) first.
|
|
3947
3941
|
subscribeToPresence(jids) {
|
|
3948
3942
|
if (!this._socket || !this._connected) return;
|
|
3949
3943
|
const arr = Array.isArray(jids) ? jids : [jids];
|
|
3950
3944
|
const { isRegularTcTokenUser } = require('./messages/TcTokenStore');
|
|
3951
3945
|
for (const raw of arr) {
|
|
3952
3946
|
const jid = makeJid(raw);
|
|
3953
|
-
// Groups and newsletters have no privacy token
|
|
3954
|
-
// way before building the node.
|
|
3947
|
+
// Groups and newsletters have no privacy token.
|
|
3955
3948
|
const token = isRegularTcTokenUser(jid) ? this._tcTokenFor(jid) : null;
|
|
3956
3949
|
const children = token ? [new BinaryNode('tctoken', {}, token)] : null;
|
|
3957
3950
|
this._socket.sendNode(new BinaryNode('presence', {
|
|
@@ -4058,8 +4051,8 @@ class WhalibmobClient extends EventEmitter {
|
|
|
4058
4051
|
return (info && info.url) || null;
|
|
4059
4052
|
}
|
|
4060
4053
|
|
|
4061
|
-
// Full profile picture info,
|
|
4062
|
-
//
|
|
4054
|
+
// Full profile picture info: { id, url, type, directPath, hash }, or null
|
|
4055
|
+
// when the user has no
|
|
4063
4056
|
// picture or it has not changed since opts.existingId.
|
|
4064
4057
|
//
|
|
4065
4058
|
// opts: { preview, existingId, isCommunity }
|
|
@@ -4185,8 +4178,8 @@ class WhalibmobClient extends EventEmitter {
|
|
|
4185
4178
|
|
|
4186
4179
|
// Parse the <list> a blocklist query or update comes back with.
|
|
4187
4180
|
//
|
|
4188
|
-
//
|
|
4189
|
-
//
|
|
4181
|
+
// Every child is walked and its jid taken, whatever the tag is called, with
|
|
4182
|
+
// the dhash kept off the list itself.
|
|
4190
4183
|
// The JIDs are stringified here: the binary decoder hands them back as JID
|
|
4191
4184
|
// objects, and while those print correctly they are not strings, so the array
|
|
4192
4185
|
// this used to return failed any includes() or === against a JID string —
|
|
@@ -4319,9 +4312,9 @@ class WhalibmobClient extends EventEmitter {
|
|
|
4319
4312
|
// 'none' | 'match_last_seen' | 'known' | 'on_standard' | 'off'
|
|
4320
4313
|
// excluded: JIDs to exclude — see the note below
|
|
4321
4314
|
//
|
|
4322
|
-
// The names above are friendly aliases. What goes on the wire is
|
|
4323
|
-
//
|
|
4324
|
-
//
|
|
4315
|
+
// The names above are friendly aliases. What goes on the wire is: last,
|
|
4316
|
+
// profile, status, online, readreceipts, groupadd, calladd, messages,
|
|
4317
|
+
// defense, stickers. Four of the six names this used to
|
|
4325
4318
|
// put on the wire — last_seen, profile_picture, read_receipts, groups_add —
|
|
4326
4319
|
// are not names the server knows, so those settings never changed and the
|
|
4327
4320
|
// discarded reply meant it never said so either.
|
|
@@ -4332,8 +4325,8 @@ class WhalibmobClient extends EventEmitter {
|
|
|
4332
4325
|
Object.keys(PRIVACY_NAME_ALIASES).join(', '));
|
|
4333
4326
|
}
|
|
4334
4327
|
|
|
4335
|
-
//
|
|
4336
|
-
//
|
|
4328
|
+
// WhatsApp Web does not put <user> children on this IQ; the exclusion list
|
|
4329
|
+
// is maintained separately. Kept for callers that pass it,
|
|
4337
4330
|
// and only emitted when they do.
|
|
4338
4331
|
const children = [];
|
|
4339
4332
|
if (excluded && excluded.length > 0) {
|
|
@@ -4364,8 +4357,8 @@ class WhalibmobClient extends EventEmitter {
|
|
|
4364
4357
|
(code ? ' (' + code + (text ? ' ' + text : '') + ')' : ''));
|
|
4365
4358
|
}
|
|
4366
4359
|
|
|
4367
|
-
//
|
|
4368
|
-
//
|
|
4360
|
+
// The change is folded into the cached copy and the whole thing handed back
|
|
4361
|
+
// rather than making the caller re-query.
|
|
4369
4362
|
const key = PRIVACY_WIRE_TO_KEY[name];
|
|
4370
4363
|
this._privacySettings = Object.assign({}, this._privacySettings || {},
|
|
4371
4364
|
{ [key]: String(value) });
|
|
@@ -4394,8 +4387,8 @@ class WhalibmobClient extends EventEmitter {
|
|
|
4394
4387
|
|
|
4395
4388
|
// Who your Status posts go to.
|
|
4396
4389
|
//
|
|
4397
|
-
// Returns [{ type, isDefault, list }] with the default first
|
|
4398
|
-
//
|
|
4390
|
+
// Returns [{ type, isDefault, list }] with the default first. type is
|
|
4391
|
+
// 'contacts', 'blacklist' or 'whitelist'; list
|
|
4399
4392
|
// holds the JIDs the last two are built from. A server that has never been
|
|
4400
4393
|
// told otherwise answers nothing, which means the default: all contacts.
|
|
4401
4394
|
async queryStatusPrivacy() {
|
|
@@ -4436,8 +4429,8 @@ class WhalibmobClient extends EventEmitter {
|
|
|
4436
4429
|
}
|
|
4437
4430
|
|
|
4438
4431
|
// Resolve the status privacy settings into the JIDs a post actually goes to,
|
|
4439
|
-
//
|
|
4440
|
-
//
|
|
4432
|
+
// a whitelist is the list itself, anything else is every known contact minus
|
|
4433
|
+
// the blacklist, and
|
|
4441
4434
|
// our own JID is added so the post lands on our own devices too.
|
|
4442
4435
|
//
|
|
4443
4436
|
// Contacts come from the synced history store — this client has no separate
|
|
@@ -4998,10 +4991,9 @@ class WhalibmobClient extends EventEmitter {
|
|
|
4998
4991
|
// </iq>
|
|
4999
4992
|
//
|
|
5000
4993
|
// The item is addressed by LID, and a block carries the phone number beside
|
|
5001
|
-
// it; an unblock carries only the LID. That is the shape
|
|
5002
|
-
//
|
|
5003
|
-
//
|
|
5004
|
-
// used to do and what the server answered with 400 bad-request.
|
|
4994
|
+
// it; an unblock carries only the LID. That is the shape this server accepts
|
|
4995
|
+
// — sending the plain JID it was handed, which is what this used to do, got a
|
|
4996
|
+
// 400 bad-request back.
|
|
5005
4997
|
//
|
|
5006
4998
|
// Three other things were wrong with the pair this replaces. The jid went out
|
|
5007
4999
|
// as a raw string rather than a binary JID. It was not reduced to its bare
|
package/lib/DeviceManager.js
CHANGED
|
@@ -45,8 +45,8 @@ function jidStrToObj(jidStr) {
|
|
|
45
45
|
return { user, agent: 0, device, server, toString() { return self; } };
|
|
46
46
|
}
|
|
47
47
|
// @lid multi-device (e.g. "112713111982325:2@lid"): AD_JID with the domain in
|
|
48
|
-
// the agent byte — 1 for lid
|
|
49
|
-
//
|
|
48
|
+
// the agent byte — 1 for lid. AD_JID with a domain type is the shape whenever
|
|
49
|
+
// a device is present.
|
|
50
50
|
//
|
|
51
51
|
// This previously packed the device into the user string as a JID_PAIR
|
|
52
52
|
// ("112713111982325:2"), a workaround from when the encoder could not express
|
|
@@ -204,7 +204,7 @@ class DeviceManager {
|
|
|
204
204
|
// It loads the on-disk device cache so the very first send after a process
|
|
205
205
|
// restart is instant — no cold-start usync round-trip needed.
|
|
206
206
|
//
|
|
207
|
-
// Files saved in session dir
|
|
207
|
+
// Files saved in session dir:
|
|
208
208
|
// <phone>.device-cache.json ← combined cache (internal)
|
|
209
209
|
// device-list-447911234567@s.whatsapp.net.json ← per-phone device IDs
|
|
210
210
|
// device-list-139471160877194@lid.json ← per-LID device IDs
|
|
@@ -268,7 +268,7 @@ class DeviceManager {
|
|
|
268
268
|
'utf8'
|
|
269
269
|
);
|
|
270
270
|
|
|
271
|
-
// ── Individual device-list files
|
|
271
|
+
// ── Individual device-list files ───────────────────────────────────────
|
|
272
272
|
for (const [key, ids] of Object.entries(entries)) {
|
|
273
273
|
let fileName;
|
|
274
274
|
if (key.startsWith('lid:')) {
|
|
@@ -411,13 +411,12 @@ class DeviceManager {
|
|
|
411
411
|
return jids.length > 0 ? jids : phones.map(p => makeDeviceJid(p, 0));
|
|
412
412
|
}
|
|
413
413
|
|
|
414
|
-
// ─── USync executor (
|
|
414
|
+
// ─── USync executor (mobile-API transport) ──────────────────────────────────
|
|
415
415
|
//
|
|
416
|
-
//
|
|
417
|
-
//
|
|
418
|
-
//
|
|
419
|
-
//
|
|
420
|
-
// the web protocol — so three adaptations are applied:
|
|
416
|
+
// Builds the <query> and <list> nodes from the USyncQuery protocols/users,
|
|
417
|
+
// sends the IQ, and parses the response through
|
|
418
|
+
// `usyncQuery.parseUSyncQueryResult`. Talking the WhatsApp *mobile* protocol
|
|
419
|
+
// rather than the web one calls for three adaptations:
|
|
421
420
|
//
|
|
422
421
|
// 1. Nodes use whalibmob's BinaryNode (`description` field) instead of the
|
|
423
422
|
// web `{ tag }` object literals.
|
|
@@ -431,7 +430,7 @@ class DeviceManager {
|
|
|
431
430
|
// protocol → <user><contact>+phone</contact></user>. The mobile server
|
|
432
431
|
// ignores the <user jid="..."> form for phone-based device discovery.
|
|
433
432
|
// • Discovery for an already-known routing JID (a LID) uses
|
|
434
|
-
// USyncUser().withId(jid) → <user jid="..."
|
|
433
|
+
// USyncUser().withId(jid) → <user jid="...">.
|
|
435
434
|
async executeUSyncQuery(usyncQuery) {
|
|
436
435
|
if (!usyncQuery || usyncQuery.protocols.length === 0) {
|
|
437
436
|
throw new Error('USyncQuery must have at least one protocol');
|
|
@@ -464,9 +463,9 @@ class DeviceManager {
|
|
|
464
463
|
return { user: (server === 'lid' && device > 0) ? raw : user, server, toString() { return str; } };
|
|
465
464
|
};
|
|
466
465
|
|
|
467
|
-
// <user> nodes — jid attr is set only when the user has no phone
|
|
468
|
-
//
|
|
469
|
-
//
|
|
466
|
+
// <user> nodes — the jid attr is set only when the user has no phone; phone
|
|
467
|
+
// users carry a <contact> child instead, produced by the contact protocol's
|
|
468
|
+
// getUserElement.
|
|
470
469
|
const userNodes = usyncQuery.users.map(user => {
|
|
471
470
|
const children = usyncQuery.protocols
|
|
472
471
|
.map(p => p.getUserElement(user))
|
|
@@ -535,9 +534,9 @@ class DeviceManager {
|
|
|
535
534
|
// Fix C — context and side_list must match _doContactUsync (the working format):
|
|
536
535
|
// context="interactive" + <side_list/> is REQUIRED. context="message" → server ignores IQ.
|
|
537
536
|
async _doUsyncIq(phones) {
|
|
538
|
-
// Build the device-discovery query
|
|
539
|
-
//
|
|
540
|
-
//
|
|
537
|
+
// Build the device-discovery query via USyncQuery and the device / LID /
|
|
538
|
+
// contact protocols, then run it through the mobile-API executor and parse
|
|
539
|
+
// it with the shared parseUSyncQueryResult.
|
|
541
540
|
//
|
|
542
541
|
// Users are addressed by PHONE (USyncUser().withPhone → <contact>+phone</>)
|
|
543
542
|
// because the WA *mobile* server silently drops usync IQs that address
|
|
@@ -626,9 +625,9 @@ class DeviceManager {
|
|
|
626
625
|
for (const dev of deviceList) {
|
|
627
626
|
const devId = Number.isFinite(dev.id) ? dev.id : 0;
|
|
628
627
|
const keyIdx = Number.isFinite(dev.keyIndex) ? dev.keyIndex : -1;
|
|
629
|
-
//
|
|
630
|
-
//
|
|
631
|
-
//
|
|
628
|
+
// Keep device 0, and non-zero devices that carry a valid
|
|
629
|
+
// key-index — otherwise the encrypt IQ returns a bad-request or an
|
|
630
|
+
// empty bundle for that device.
|
|
632
631
|
if (devId === 0 || keyIdx > 0) {
|
|
633
632
|
this._dcAdd(cachePhone, devId);
|
|
634
633
|
}
|
|
@@ -883,8 +882,8 @@ class DeviceManager {
|
|
|
883
882
|
// expressed as <ourLid>:<device>@lid.
|
|
884
883
|
//
|
|
885
884
|
// Device 0 is excluded exactly as it is on the phone path: it is the device
|
|
886
|
-
// doing the sending, and
|
|
887
|
-
//
|
|
885
|
+
// doing the sending, and the sender device is never one of its own
|
|
886
|
+
// participants.
|
|
888
887
|
async ensureOwnLidDeviceSessions(lidUser, ownPhone, signalProto, allowPkmsg = true) {
|
|
889
888
|
if (!lidUser) return [];
|
|
890
889
|
const cacheKey = 'lid:' + lidUser;
|
|
@@ -977,7 +976,7 @@ class DeviceManager {
|
|
|
977
976
|
async _doContactUsync(phones) {
|
|
978
977
|
// Contact-only fallback (device/lid protocols omitted): the plain contact
|
|
979
978
|
// query is the form the mobile server reliably answers when the richer
|
|
980
|
-
// device query times out. Built via the shared
|
|
979
|
+
// device query times out. Built via the shared USyncQuery builder.
|
|
981
980
|
const query = new USyncQuery()
|
|
982
981
|
.withContext('interactive')
|
|
983
982
|
.withContactProtocol();
|
|
@@ -1031,9 +1030,9 @@ class DeviceManager {
|
|
|
1031
1030
|
const jidStr = typeof jid === 'string' ? jid : String(jid);
|
|
1032
1031
|
|
|
1033
1032
|
// Discovery for an already-known routing JID (a LID) — addressed via
|
|
1034
|
-
// USyncUser().withId(jid) → <user jid="..."
|
|
1035
|
-
//
|
|
1036
|
-
//
|
|
1033
|
+
// USyncUser().withId(jid) → <user jid="...">. executeUSyncQuery encodes
|
|
1034
|
+
// the jid attr as a JID object so the mobile binary encoder emits proper
|
|
1035
|
+
// JID_PAIR bytes.
|
|
1037
1036
|
const query = new USyncQuery()
|
|
1038
1037
|
.withContext('interactive')
|
|
1039
1038
|
.withDeviceProtocol()
|
package/lib/MediaService.js
CHANGED
|
@@ -119,8 +119,8 @@ function decryptMedia(encryptedWithMac, mediaKey, keyName) {
|
|
|
119
119
|
|
|
120
120
|
// Upload encrypted media, trying every CDN host in turn.
|
|
121
121
|
//
|
|
122
|
-
// refreshAuth (optional)
|
|
123
|
-
//
|
|
122
|
+
// refreshAuth (optional): when a host answers but the upload yields no usable
|
|
123
|
+
// URL — the signature of an expired or
|
|
124
124
|
// rejected auth token — the auth is force-refreshed once and the hosts are
|
|
125
125
|
// retried with the fresh credentials. Without this a token that expires between
|
|
126
126
|
// the TTL check and the upload burns through every host and fails permanently
|
package/lib/MediaThumbnail.js
CHANGED
|
@@ -8,9 +8,8 @@
|
|
|
8
8
|
// drawn, so the recipient gets the grey placeholder with a download arrow and
|
|
9
9
|
// has to tap it before seeing anything.
|
|
10
10
|
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
// the same, with the difference that none of it is mandatory:
|
|
11
|
+
// The preview is the source resized to 32 pixels wide at JPEG quality 50, with
|
|
12
|
+
// the original dimensions read from the same decode. None of it is mandatory:
|
|
14
13
|
//
|
|
15
14
|
// • Dimensions are read straight out of the file header — JPEG, PNG, WebP,
|
|
16
15
|
// GIF and BMP — with no library at all.
|
|
@@ -31,8 +30,8 @@ const path = require('path');
|
|
|
31
30
|
const crypto = require('crypto');
|
|
32
31
|
const { execFile } = require('child_process');
|
|
33
32
|
|
|
34
|
-
//
|
|
35
|
-
//
|
|
33
|
+
// The preview is deliberately tiny; it is only ever shown blurred behind the
|
|
34
|
+
// real image while that downloads.
|
|
36
35
|
const THUMB_WIDTH = 32;
|
|
37
36
|
const THUMB_QUALITY = 50;
|
|
38
37
|
|
|
@@ -358,8 +357,7 @@ async function videoThumbnail(input, opts) {
|
|
|
358
357
|
} catch (_) {}
|
|
359
358
|
}
|
|
360
359
|
|
|
361
|
-
//
|
|
362
|
-
// with the aspect ratio kept.
|
|
360
|
+
// First frame, scaled to the preview width with the aspect ratio kept.
|
|
363
361
|
const dest = tmpFile('.jpg');
|
|
364
362
|
try {
|
|
365
363
|
const ok = await run('ffmpeg', [
|
package/lib/auth-utils.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
// ─── auth-utils.js ────────────────────────────────────────────────────────────
|
|
4
4
|
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
5
|
+
// Four utilities built around whalibmob's architecture (method-based
|
|
6
|
+
// SignalStore, Buffer key pairs, etc.):
|
|
7
7
|
//
|
|
8
8
|
// 1. makeCacheableSignalKeyStore — wraps SignalStore with NodeCache (5-min TTL)
|
|
9
9
|
// and a Mutex for all read/write operations
|
|
@@ -591,7 +591,6 @@ function addTransactionCapability(state, logger, opts) {
|
|
|
591
591
|
* Returns the authenticated user's JID (phone@s.whatsapp.net) or throws
|
|
592
592
|
* a descriptive error if the store is not yet fully authenticated.
|
|
593
593
|
*
|
|
594
|
-
* Equivalent to Baileys' assertMeId(creds) but for whalibmob's store format.
|
|
595
594
|
* Use this anywhere we'd otherwise reach for `store.phoneNumber` directly,
|
|
596
595
|
* to fail fast with a clear error instead of a silent null-reference crash.
|
|
597
596
|
*
|
|
@@ -623,12 +622,12 @@ function assertMeId(store) {
|
|
|
623
622
|
* Creates a fresh set of whalibmob auth credentials (key pairs, registration ID,
|
|
624
623
|
* device identifiers) for a given phone number.
|
|
625
624
|
*
|
|
626
|
-
*
|
|
627
|
-
*
|
|
625
|
+
* Returns whalibmob's store format (Buffer key pairs, signedPreKey as
|
|
626
|
+
* { id, public, private, signature }).
|
|
628
627
|
*
|
|
629
|
-
*
|
|
630
|
-
*
|
|
631
|
-
* code that
|
|
628
|
+
* A few extra fields (nextPreKeyId, firstUnuploadedPreKeyId, accountSyncCounter,
|
|
629
|
+
* accountSettings, advSecretKey, etc.) are appended for account sync and for
|
|
630
|
+
* application code that expects them.
|
|
632
631
|
*
|
|
633
632
|
* @param {string|number} phoneNumber — full international number (e.g. "40712345678")
|
|
634
633
|
* @param {object} options — optional overrides:
|
|
@@ -643,9 +642,8 @@ function initAuthCreds(phoneNumber, options) {
|
|
|
643
642
|
// Use whalibmob's own key-generation machinery (curve25519-js, proper prefixes)
|
|
644
643
|
const store = createNewStore(phoneNumber);
|
|
645
644
|
|
|
646
|
-
// ── Extra
|
|
647
|
-
//
|
|
648
|
-
// application code that was originally written against Baileys.
|
|
645
|
+
// ── Extra credential fields ──────────────────────────────────────────────────
|
|
646
|
+
// Not required internally, but commonly expected by application code.
|
|
649
647
|
|
|
650
648
|
/** Next pre-key ID to generate (monotonically increasing). */
|
|
651
649
|
store.nextPreKeyId = 1;
|
|
@@ -656,7 +654,7 @@ function initAuthCreds(phoneNumber, options) {
|
|
|
656
654
|
/** Sync counter used in account-sync IQ stanzas. */
|
|
657
655
|
store.accountSyncCounter = 0;
|
|
658
656
|
|
|
659
|
-
/** Account-level settings
|
|
657
|
+
/** Account-level settings. */
|
|
660
658
|
store.accountSettings = { unarchiveChats: false };
|
|
661
659
|
|
|
662
660
|
/** Processed history messages — used to avoid duplicate handling on reconnect. */
|
|
@@ -664,7 +662,7 @@ function initAuthCreds(phoneNumber, options) {
|
|
|
664
662
|
|
|
665
663
|
/**
|
|
666
664
|
* 32-byte random secret used for ADV (multi-device) poll encryption.
|
|
667
|
-
* Stored as a base64 string
|
|
665
|
+
* Stored as a base64 string.
|
|
668
666
|
*/
|
|
669
667
|
store.advSecretKey = crypto.randomBytes(32).toString('base64');
|
|
670
668
|
|
|
@@ -43,8 +43,7 @@ function isGroupJid(jid) {
|
|
|
43
43
|
|
|
44
44
|
// A Status/Story is posted as a message to status@broadcast, and it travels the
|
|
45
45
|
// SenderKey path a group message does — one skmsg plus a distribution to every
|
|
46
|
-
// recipient — not the 1:1 path.
|
|
47
|
-
// whatsmeow routes BroadcastServer into sendGroup for the same reason.
|
|
46
|
+
// recipient — not the 1:1 path. A Status and a group are the same case here.
|
|
48
47
|
const STATUS_BROADCAST_JID = 'status@broadcast';
|
|
49
48
|
|
|
50
49
|
function isStatusBroadcast(jid) {
|
|
@@ -196,18 +195,14 @@ function adString(jid) {
|
|
|
196
195
|
return `${user}.0:${device}@${server}`;
|
|
197
196
|
}
|
|
198
197
|
|
|
199
|
-
// Participant-list hash v2,
|
|
200
|
-
//
|
|
201
|
-
// BYTES of the digest — eight characters, prefixed "2:".
|
|
198
|
+
// Participant-list hash v2: sort the AD strings, sha256 the concatenation, then
|
|
199
|
+
// base64 the FIRST SIX BYTES of the digest — eight characters, prefixed "2:".
|
|
202
200
|
//
|
|
203
|
-
// This previously
|
|
204
|
-
//
|
|
205
|
-
//
|
|
206
|
-
//
|
|
207
|
-
//
|
|
208
|
-
// which is why the discrepancy has gone unnoticed there. whalibmob did put it
|
|
209
|
-
// on the wire, so every multi-device send carried a hash the server could not
|
|
210
|
-
// match against its own view of the participant list.
|
|
201
|
+
// This previously hashed plain JID strings and took the first six CHARACTERS of
|
|
202
|
+
// the base64 of the whole digest, which is a different, six-character value over
|
|
203
|
+
// different input. It goes on the wire, so every multi-device send carried a
|
|
204
|
+
// hash the server could not match against its own view of the participant
|
|
205
|
+
// list.
|
|
211
206
|
function computePhash(jids) {
|
|
212
207
|
const sorted = [...new Set(jids.map(adString))].sort();
|
|
213
208
|
const hash = crypto.createHash('sha256').update(sorted.join('')).digest();
|
|
@@ -341,7 +336,7 @@ class MessageSender {
|
|
|
341
336
|
gifPlayback: options.gifPlayback || false,
|
|
342
337
|
contextInfo: options.contextInfo
|
|
343
338
|
}));
|
|
344
|
-
//
|
|
339
|
+
// A video with gifPlayback is labelled mediatype "gif", not "video".
|
|
345
340
|
return this._sendMessage(toJid, msgId, vidBuf, 'media',
|
|
346
341
|
{ ...options, _mediaSubtype: options.gifPlayback ? 'gif' : 'video' });
|
|
347
342
|
}
|
|
@@ -459,8 +454,8 @@ class MessageSender {
|
|
|
459
454
|
selectableOptionsCount: selectableCount != null ? selectableCount : 0,
|
|
460
455
|
messageSecret: opts.messageSecret || opts.encKey || null
|
|
461
456
|
});
|
|
462
|
-
//
|
|
463
|
-
//
|
|
457
|
+
// The poll has to be paired with a messageContextInfo holding the secret.
|
|
458
|
+
// It is a sibling field of the poll inside Message, not
|
|
464
459
|
// part of the poll — a voter derives their vote key from this copy, so a
|
|
465
460
|
// poll sent without it cannot be voted on.
|
|
466
461
|
const pollBuf = Buffer.concat([
|
|
@@ -497,9 +492,9 @@ class MessageSender {
|
|
|
497
492
|
contextInfo: opts.contextInfo
|
|
498
493
|
}));
|
|
499
494
|
// A location is a plain message as far as the envelope is concerned.
|
|
500
|
-
//
|
|
501
|
-
//
|
|
502
|
-
//
|
|
495
|
+
// The stanza types are text, media, reaction and poll — LocationMessage is
|
|
496
|
+
// in none of the media cases, so it is text. Sending type="location" hands
|
|
497
|
+
// the server a type it has no case for.
|
|
503
498
|
return this._sendMessage(toJid, msgId, locBuf, 'text', opts);
|
|
504
499
|
}
|
|
505
500
|
|
|
@@ -922,9 +917,8 @@ class MessageSender {
|
|
|
922
917
|
const allowPkmsg = true;
|
|
923
918
|
|
|
924
919
|
// ── Addressing family ─────────────────────────────────────────────────────
|
|
925
|
-
//
|
|
926
|
-
//
|
|
927
|
-
// never converts between the two:
|
|
920
|
+
// The whole address family comes from the JID the caller passed, and the
|
|
921
|
+
// two are never converted between:
|
|
928
922
|
//
|
|
929
923
|
// const targetUserServer = isLid ? 'lid' : 's.whatsapp.net'
|
|
930
924
|
// const senderIdentity = isLid && meLid ? <ourLid> : <ourPn>
|
|
@@ -939,9 +933,9 @@ class MessageSender {
|
|
|
939
933
|
// The recipient's devices followed the caller's JID but ours were always
|
|
940
934
|
// enumerated by phone, so a DM to an @lid went out with @lid recipient
|
|
941
935
|
// entries and @s.whatsapp.net entries for our own linked devices in the same
|
|
942
|
-
// <participants> list.
|
|
943
|
-
//
|
|
944
|
-
//
|
|
936
|
+
// <participants> list. The pair has to be [to, ownID] with ownID switched to
|
|
937
|
+
// our LID for @lid destinations; a participant list the server cannot
|
|
938
|
+
// reconcile is what comes back as 479.
|
|
945
939
|
const isLidTarget = toJid.endsWith('@lid');
|
|
946
940
|
const routingToJid = toJid;
|
|
947
941
|
const ownLidUser = this._client._myLid ? phoneFromJid(this._client._myLid) : null;
|
|
@@ -955,8 +949,7 @@ class MessageSender {
|
|
|
955
949
|
}
|
|
956
950
|
|
|
957
951
|
// Enumerate the recipient's devices and our own in parallel, both in the
|
|
958
|
-
// address family the destination JID dictates
|
|
959
|
-
// getUSyncDevices([senderIdentity, jid]) call.
|
|
952
|
+
// address family the destination JID dictates.
|
|
960
953
|
let otherJids, ownLinkedJids, ownPrimaryJid;
|
|
961
954
|
if (isLidTarget) {
|
|
962
955
|
const lidUser = phoneFromJid(toJid);
|
|
@@ -981,8 +974,7 @@ class MessageSender {
|
|
|
981
974
|
ownPrimaryJid = this._client._ownDeviceJid();
|
|
982
975
|
}
|
|
983
976
|
|
|
984
|
-
// The device doing the sending is never one of its own recipients.
|
|
985
|
-
// Baileys ("Skipping exact sender device") and whatsmeow drop it explicitly.
|
|
977
|
+
// The device doing the sending is never one of its own recipients.
|
|
986
978
|
const ownPrimaryAddr = adString(ownPrimaryJid);
|
|
987
979
|
otherJids = otherJids.filter(j => adString(j) !== ownPrimaryAddr);
|
|
988
980
|
ownLinkedJids = ownLinkedJids.filter(j => adString(j) !== ownPrimaryAddr);
|
|
@@ -999,12 +991,12 @@ class MessageSender {
|
|
|
999
991
|
// ─────────────────────────────────────────────────────────────────────────
|
|
1000
992
|
|
|
1001
993
|
// The hash covers every device the server enumerates for this pair of users,
|
|
1002
|
-
// our own primary included —
|
|
1003
|
-
//
|
|
1004
|
-
//
|
|
994
|
+
// our own primary included — the sending device is skipped only when it
|
|
995
|
+
// comes to encrypting. Excluding it here produced a hash over a different
|
|
996
|
+
// set than the server's.
|
|
1005
997
|
//
|
|
1006
|
-
// It is not put on the stanza:
|
|
1007
|
-
//
|
|
998
|
+
// It is not put on the stanza: phash belongs on a group message, not a 1:1
|
|
999
|
+
// one. It is carried inside the (encrypted)
|
|
1008
1000
|
// DeviceSentMessage for our own devices, and compared against the phash the
|
|
1009
1001
|
// server echoes back on the ack so a drifted device cache gets flushed.
|
|
1010
1002
|
const phash = computePhash([...otherJids, ownPrimaryJid, ...ownLinkedJids]);
|
|
@@ -1115,8 +1107,7 @@ class MessageSender {
|
|
|
1115
1107
|
|
|
1116
1108
|
// The server echoes its own participant hash on the ack. A difference means
|
|
1117
1109
|
// its device list for this chat is not the one we just fanned out to, so the
|
|
1118
|
-
// cached list is stale — drop it and let the next send re-run usync
|
|
1119
|
-
// as whatsmeow does when the hashes disagree.
|
|
1110
|
+
// cached list is stale — drop it and let the next send re-run usync.
|
|
1120
1111
|
if (dispatchResult && dispatchResult.phash && dispatchResult.phash !== phash) {
|
|
1121
1112
|
_whaDbg('[DBG] PHASH_MISMATCH ours=' + phash + ' server=' + dispatchResult.phash +
|
|
1122
1113
|
' to=' + toJid + ' — flushing device cache');
|
|
@@ -1136,10 +1127,9 @@ class MessageSender {
|
|
|
1136
1127
|
// Two targets never get one. WhatsApp's own announcement account
|
|
1137
1128
|
// (0@s.whatsapp.net) and the bot numbers, including Meta AI, are not
|
|
1138
1129
|
// contacts you can be "timelocked" against, and no real client issues to
|
|
1139
|
-
// them
|
|
1140
|
-
//
|
|
1141
|
-
//
|
|
1142
|
-
// not a reach-out. Attaching an existing token to them is still correct
|
|
1130
|
+
// them. Protocol messages (a revoke, an ephemeral-timer change) are out
|
|
1131
|
+
// too — they are bookkeeping, not a reach-out. Attaching an existing token
|
|
1132
|
+
// to them is still correct
|
|
1143
1133
|
// and still happens above — this only governs asking for a new one.
|
|
1144
1134
|
const isProtocolMsg = !!(options && options._protocol);
|
|
1145
1135
|
if (tcStore && isRegularTcTokenUser(tcJid) && !isProtocolMsg &&
|
|
@@ -1219,9 +1209,9 @@ class MessageSender {
|
|
|
1219
1209
|
}
|
|
1220
1210
|
|
|
1221
1211
|
// Determine addressing mode for this group (lid = modern groups, pn = legacy).
|
|
1222
|
-
// A Status carries none:
|
|
1223
|
-
//
|
|
1224
|
-
//
|
|
1212
|
+
// A Status carries none: addressing_mode is set only for the group server,
|
|
1213
|
+
// and a Status goes to ordinary contacts, so everything below stays in
|
|
1214
|
+
// phone space.
|
|
1225
1215
|
const groupAddressingMode = isStatus
|
|
1226
1216
|
? null
|
|
1227
1217
|
: (this._client._groupAddressingMode.get(groupJid) || 'lid');
|
|
@@ -1258,9 +1248,8 @@ class MessageSender {
|
|
|
1258
1248
|
// out of the member walk and put back through the own-device path below.
|
|
1259
1249
|
// Left in, the walk enumerates our devices starting at device 0 — the very
|
|
1260
1250
|
// device doing the sending — and it lands in <participants> as a recipient
|
|
1261
|
-
// of our own SenderKey.
|
|
1262
|
-
//
|
|
1263
|
-
// `jid == ownJID || jid == ownLID`.
|
|
1251
|
+
// of our own SenderKey. The exact sender device has to be skipped, matched
|
|
1252
|
+
// against both our phone JID and our LID.
|
|
1264
1253
|
//
|
|
1265
1254
|
// The phone branch already dropped us via `p !== ownPhone`; the LID branch
|
|
1266
1255
|
// never did, so this only ever misfired on LID-addressed groups.
|
|
@@ -1313,10 +1302,10 @@ class MessageSender {
|
|
|
1313
1302
|
]);
|
|
1314
1303
|
|
|
1315
1304
|
// Every device this group resolves to, our own primary included, deduped so
|
|
1316
|
-
// a device reached through more than one path is counted once.
|
|
1317
|
-
//
|
|
1318
|
-
//
|
|
1319
|
-
//
|
|
1305
|
+
// a device reached through more than one path is counted once. The hash
|
|
1306
|
+
// covers the whole enumeration, sending device and all, with hosted
|
|
1307
|
+
// endpoints dropped first since those are bot-side and never real
|
|
1308
|
+
// participants.
|
|
1320
1309
|
const isHostedJid = (jid) => {
|
|
1321
1310
|
const s = String(jid);
|
|
1322
1311
|
return s.endsWith('@hosted') || s.endsWith('@hosted.lid');
|
|
@@ -1334,14 +1323,14 @@ class MessageSender {
|
|
|
1334
1323
|
[...memberDevices, ...lidDevices, ...ownTargets, ownSelfJid]
|
|
1335
1324
|
)].filter(jid => !isHostedJid(jid));
|
|
1336
1325
|
|
|
1337
|
-
// The participant hash the server checks the fan-out against.
|
|
1338
|
-
//
|
|
1339
|
-
//
|
|
1326
|
+
// The participant hash the server checks the fan-out against. It goes on
|
|
1327
|
+
// group stanzas and nowhere else, and is computed over the enumeration
|
|
1328
|
+
// above rather than over the encryption targets,
|
|
1340
1329
|
// so the sending device counts here even though it is not a recipient.
|
|
1341
1330
|
const phash = computePhash(enumeratedDevices);
|
|
1342
1331
|
|
|
1343
|
-
// Targets to actually encrypt for: everything but the sending device
|
|
1344
|
-
//
|
|
1332
|
+
// Targets to actually encrypt for: everything but the sending device, and
|
|
1333
|
+
// it is skipped at this point rather than earlier.
|
|
1345
1334
|
// Only the device actually sending. On a companion link that is not the
|
|
1346
1335
|
// account's phone — the phone is a peer that has to receive the SenderKey
|
|
1347
1336
|
// like everyone else, so excluding the bare number here would leave the
|
|
@@ -1364,8 +1353,8 @@ class MessageSender {
|
|
|
1364
1353
|
|
|
1365
1354
|
const skStore = this._signal.senderKeyStore;
|
|
1366
1355
|
const existingSkdmMap = skStore.getSKDMMap(groupJid);
|
|
1367
|
-
//
|
|
1368
|
-
//
|
|
1356
|
+
// Exclusions when choosing SenderKey recipients: hosted (bot-side) users
|
|
1357
|
+
// and device 99, which is a server placeholder
|
|
1369
1358
|
// rather than a real endpoint, must never receive a distribution.
|
|
1370
1359
|
const isSkdmEligible = (jid) => {
|
|
1371
1360
|
const s = String(jid);
|
|
@@ -1408,10 +1397,10 @@ class MessageSender {
|
|
|
1408
1397
|
msgContent.push(new BinaryNode('device-identity', {}, advBytes));
|
|
1409
1398
|
}
|
|
1410
1399
|
if (skdmEncrypted.length > 0) {
|
|
1411
|
-
//
|
|
1412
|
-
//
|
|
1413
|
-
//
|
|
1414
|
-
//
|
|
1400
|
+
// The same extra attrs — mediatype included — belong on the SenderKey
|
|
1401
|
+
// participant nodes as on the skmsg, so an image sent to a group carries
|
|
1402
|
+
// mediatype on both. Passing null here left the SKDM <enc> nodes
|
|
1403
|
+
// unlabelled.
|
|
1415
1404
|
msgContent.push(DeviceManager.buildParticipantsNode(skdmEncrypted, mediaSubtype));
|
|
1416
1405
|
}
|
|
1417
1406
|
const skmsgAttrs = { type: 'skmsg', v: '2' };
|
|
@@ -1429,10 +1418,9 @@ class MessageSender {
|
|
|
1429
1418
|
_whaDbg('[DBG] REPORTING_TOKEN attached to=' + groupJid + ' id=' + msgId);
|
|
1430
1419
|
}
|
|
1431
1420
|
|
|
1432
|
-
// phash belongs on a group stanza and only on a group stanza
|
|
1433
|
-
//
|
|
1434
|
-
//
|
|
1435
|
-
// back on the ack when they differ.
|
|
1421
|
+
// phash belongs on a group stanza and only on a group stanza. It tells the
|
|
1422
|
+
// server which device set we fanned out to; the server compares it against
|
|
1423
|
+
// its own and echoes its hash back on the ack when they differ.
|
|
1436
1424
|
const stanzaAttrs = {
|
|
1437
1425
|
to: jidStrToObj(groupJid),
|
|
1438
1426
|
id: msgId,
|
|
@@ -1464,9 +1452,9 @@ class MessageSender {
|
|
|
1464
1452
|
|
|
1465
1453
|
// A hash on the ack that differs from ours means the server's participant
|
|
1466
1454
|
// list for this group is not the one we just fanned out to, so some members
|
|
1467
|
-
// did not get the message.
|
|
1468
|
-
// signal
|
|
1469
|
-
//
|
|
1455
|
+
// did not get the message. The group cache is dropped on exactly this
|
|
1456
|
+
// signal, and the device lists behind the members with it, so the next send
|
|
1457
|
+
// re-runs group metadata and usync rather than repeating the miss.
|
|
1470
1458
|
if (dispatchResult && dispatchResult.phash && dispatchResult.phash !== phash) {
|
|
1471
1459
|
_whaDbg('[DBG] GROUP_PHASH_MISMATCH ours=' + phash +
|
|
1472
1460
|
' server=' + dispatchResult.phash + ' group=' + groupJid +
|
|
@@ -1485,7 +1473,7 @@ class MessageSender {
|
|
|
1485
1473
|
|
|
1486
1474
|
// ─── Dispatch and wait for ack ────────────────────────────────────────────
|
|
1487
1475
|
//
|
|
1488
|
-
// Error 463 recovery
|
|
1476
|
+
// Error 463 recovery:
|
|
1489
1477
|
// 1. 463 ack arrives → issue privacy token via _issuePrivacyTokens
|
|
1490
1478
|
// 2. Wait for token to be stored in _tcTokenStore
|
|
1491
1479
|
// 3. Re-send the SAME message node with a new msgId — the tctoken node
|
|
@@ -1496,7 +1484,7 @@ class MessageSender {
|
|
|
1496
1484
|
//
|
|
1497
1485
|
// Uses a SEPARATE _inFlight463Recoveries Set (not shared with the proactive
|
|
1498
1486
|
// _inFlightTcTokenIssuance) so 463 recovery and proactive issuance never
|
|
1499
|
-
// block each other.
|
|
1487
|
+
// block each other.
|
|
1500
1488
|
|
|
1501
1489
|
_dispatchAndAck(node, msgId, _isRetry) {
|
|
1502
1490
|
const self = this;
|
|
@@ -1659,8 +1647,8 @@ class MessageSender {
|
|
|
1659
1647
|
key: { remoteJid: toJid, fromMe: !!fromMe, id: origMsgId }
|
|
1660
1648
|
});
|
|
1661
1649
|
const msgBuf = encodeMessage('protocol', revokePayload);
|
|
1662
|
-
// ProtocolMessage
|
|
1663
|
-
//
|
|
1650
|
+
// A ProtocolMessage travels as stanza type "text"; there is no "protocol"
|
|
1651
|
+
// stanza type.
|
|
1664
1652
|
// _protocol marks it for the tcToken issuance gate — see _sendDMMessage.
|
|
1665
1653
|
return this._sendMessage(toJid, msgId, msgBuf, 'text',
|
|
1666
1654
|
{ ...opts, edit: editBit, _protocol: true });
|
|
@@ -16,10 +16,9 @@
|
|
|
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
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
// token when a report is actually filed, neither choice is observable on send.
|
|
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.
|
|
23
22
|
|
|
24
23
|
const crypto = require('crypto');
|
|
25
24
|
const { hkdf } = require('@noble/hashes/hkdf');
|
|
@@ -121,8 +120,7 @@ function encodeVarint(value) {
|
|
|
121
120
|
// The result is sorted by field number, which is what makes the token
|
|
122
121
|
// independent of the order the encoder happened to write the fields in.
|
|
123
122
|
//
|
|
124
|
-
// Returns null on malformed input — the caller then attaches no token
|
|
125
|
-
// the same thing Baileys does.
|
|
123
|
+
// Returns null on malformed input — the caller then attaches no token.
|
|
126
124
|
function extractReportingTokenContent(data, cfg) {
|
|
127
125
|
const out = [];
|
|
128
126
|
let i = 0;
|
|
@@ -3,16 +3,14 @@
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
5
|
|
|
6
|
-
// 7-day buckets, 4 buckets = ~28-day rolling window
|
|
6
|
+
// 7-day buckets, 4 buckets = ~28-day rolling window
|
|
7
7
|
const TC_BUCKET_DURATION = 604800;
|
|
8
8
|
const TC_NUM_BUCKETS = 4;
|
|
9
|
-
//
|
|
10
|
-
//
|
|
9
|
+
// Expired rows are pruned out of the token store at most once a day; it is a
|
|
10
|
+
// JSON file, so there is nothing to gain from doing it more often.
|
|
11
11
|
const TC_PRUNE_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
12
12
|
|
|
13
13
|
// The two phone-number ranges WhatsApp uses for bots, including Meta AI.
|
|
14
|
-
// Byte-for-byte the same expression in whatsmeow (botUserRegex) and in
|
|
15
|
-
// Baileys (BOT_PHONE_REGEX).
|
|
16
14
|
const BOT_USER_RE = /^1313555\d{4}$|^131655500\d{2}$/;
|
|
17
15
|
|
|
18
16
|
function tcTokenExpired(timestamp) {
|
|
@@ -35,9 +33,8 @@ function shouldSendNewTcToken(senderTimestamp) {
|
|
|
35
33
|
|
|
36
34
|
// Whether a JID is a real person we may hold a trusted-contact token for.
|
|
37
35
|
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
// a user address (phone or LID), not the PSA pseudo-contact 0@…, not a bot.
|
|
36
|
+
// The rule is: a user address (phone or LID), not the PSA pseudo-contact 0@…,
|
|
37
|
+
// and not a bot.
|
|
41
38
|
// Without it we issue a privacy IQ to WhatsApp's own announcement account and
|
|
42
39
|
// to Meta AI, which no real client ever does.
|
|
43
40
|
function isRegularTcTokenUser(jid) {
|
|
@@ -78,10 +75,8 @@ class TcTokenStore {
|
|
|
78
75
|
|
|
79
76
|
// Drop entries that are past the 28-day window on both counts.
|
|
80
77
|
//
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
// whose token has expired but whose senderTimestamp is still inside the
|
|
84
|
-
// window has to stay: that timestamp is what keeps us from re-issuing on
|
|
78
|
+
// An entry whose token has expired but whose senderTimestamp is still inside
|
|
79
|
+
// the window has to stay: that timestamp is what keeps us from re-issuing on
|
|
85
80
|
// every single send.
|
|
86
81
|
pruneExpired() {
|
|
87
82
|
let removed = 0;
|
|
@@ -134,15 +129,14 @@ class TcTokenStore {
|
|
|
134
129
|
}
|
|
135
130
|
|
|
136
131
|
get(jid) {
|
|
137
|
-
//
|
|
138
|
-
// to once a day. Same here.
|
|
132
|
+
// Pruning happens on the read path, rate-limited to once a day.
|
|
139
133
|
this._maybePrune();
|
|
140
134
|
return this._map.get(normalizeJidForTcToken(jid)) || null;
|
|
141
135
|
}
|
|
142
136
|
|
|
143
137
|
// Returns true when the token was accepted.
|
|
144
138
|
//
|
|
145
|
-
// Two rejections
|
|
139
|
+
// Two rejections:
|
|
146
140
|
// - a token with no timestamp is already expired by definition, so storing
|
|
147
141
|
// it only wastes a slot and guarantees a miss on the next send;
|
|
148
142
|
// - a token older than the one on file must never overwrite it. Token
|
package/lib/noise.js
CHANGED
|
@@ -449,10 +449,9 @@ class NoiseSocket extends EventEmitter {
|
|
|
449
449
|
// for a bug in the client; the credentials were rejected by the
|
|
450
450
|
// server, which is a different thing and has different remedies.
|
|
451
451
|
//
|
|
452
|
-
//
|
|
453
|
-
//
|
|
454
|
-
//
|
|
455
|
-
// the session is finished and retrying with it is pointless.
|
|
452
|
+
// WhatsApp publishes none of this, so the meanings below come from
|
|
453
|
+
// observing the server. Codes marked "wipe" mean the session is
|
|
454
|
+
// finished and retrying with it is pointless.
|
|
456
455
|
const FAILURE = {
|
|
457
456
|
'400': { wipe: false, text: 'generic failure.' },
|
|
458
457
|
'401': { wipe: true, text: 'logged out. The session was unpaired ' +
|
|
@@ -226,10 +226,10 @@ function encodePollOption(name) {
|
|
|
226
226
|
// PollCreationMessage { encKey=1, name=2, options=3, selectableOptionsCount=4 }
|
|
227
227
|
//
|
|
228
228
|
// The 32-byte secret was being written to encKey, inside the poll itself.
|
|
229
|
-
//
|
|
230
|
-
// Message.messageContextInfo.messageSecret
|
|
231
|
-
//
|
|
232
|
-
//
|
|
229
|
+
// encKey has to stay unset; the secret belongs in
|
|
230
|
+
// Message.messageContextInfo.messageSecret, which is the copy a voter derives
|
|
231
|
+
// their vote key from. In the wrong place it reached nobody, so the poll could
|
|
232
|
+
// not be voted on.
|
|
233
233
|
function encodePollCreationMessage(opts) {
|
|
234
234
|
const messageSecret = opts.messageSecret || opts.encKey ||
|
|
235
235
|
require('crypto').randomBytes(32);
|
|
@@ -278,15 +278,15 @@ class SignalProtocol {
|
|
|
278
278
|
|
|
279
279
|
// ─── SenderKey group encrypt / decrypt ────────────────────────────────────
|
|
280
280
|
//
|
|
281
|
-
// Uses WaSignalGroup GroupCipher/GroupSessionBuilder
|
|
281
|
+
// Uses WaSignalGroup GroupCipher/GroupSessionBuilder.
|
|
282
282
|
// SenderKeyName format: groupId::senderId::0 (device 0 for group identity).
|
|
283
283
|
|
|
284
284
|
_senderKeyName(groupId, senderId) {
|
|
285
285
|
// A SenderKey belongs to a device, not to an account. Every device that
|
|
286
286
|
// posts to a group distributes its own key, so the name has to carry the
|
|
287
|
-
// device index
|
|
288
|
-
//
|
|
289
|
-
//
|
|
287
|
+
// device index — otherwise two devices of the same person share one slot
|
|
288
|
+
// and the second one to speak overwrites the first, leaving its messages
|
|
289
|
+
// undecryptable.
|
|
290
290
|
//
|
|
291
291
|
// Bare JIDs still resolve to device 0, so everything already on disk keeps
|
|
292
292
|
// the exact same key string.
|
|
@@ -209,8 +209,8 @@ class SignalStore {
|
|
|
209
209
|
// SessionCipher.encrypt — which throws "No open session". bulkEncryptForDevices
|
|
210
210
|
// drops whatever it cannot encrypt, so the stanza went out with a <to> entry
|
|
211
211
|
// missing for that device and the server nacked the whole send with ack 479.
|
|
212
|
-
//
|
|
213
|
-
//
|
|
212
|
+
// The fix is to validate the record itself on every check rather than
|
|
213
|
+
// trusting that a cached entry still holds an open session.
|
|
214
214
|
hasSession(encodedAddress) {
|
|
215
215
|
if (!this._openSessionMemo) this._openSessionMemo = new Map();
|
|
216
216
|
const data = this._sessions[encodedAddress];
|