whalibmob 5.5.82 → 5.5.83
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/messages/MessageSender.js +48 -12
- package/lib/proto/MessageProto.js +39 -15
- package/lib/signal/SenderKey.js +33 -0
- package/package.json +1 -1
|
@@ -1127,14 +1127,30 @@ class MessageSender {
|
|
|
1127
1127
|
: this._devMgr.ensureOwnDeviceSessions(ownPhone, this._signal)
|
|
1128
1128
|
]);
|
|
1129
1129
|
|
|
1130
|
-
//
|
|
1131
|
-
//
|
|
1132
|
-
//
|
|
1133
|
-
//
|
|
1130
|
+
// Every device this group resolves to, our own primary included, deduped so
|
|
1131
|
+
// a device reached through more than one path is counted once. whatsmeow
|
|
1132
|
+
// hashes GetUserDevices(groupParticipants) in exactly this shape — the whole
|
|
1133
|
+
// enumeration, sending device and all — and drops hosted endpoints first,
|
|
1134
|
+
// since those are bot-side and never real participants.
|
|
1135
|
+
const isHostedJid = (jid) => {
|
|
1136
|
+
const s = String(jid);
|
|
1137
|
+
return s.endsWith('@hosted') || s.endsWith('@hosted.lid');
|
|
1138
|
+
};
|
|
1139
|
+
const enumeratedDevices = [...new Set(
|
|
1140
|
+
[...memberDevices, ...lidDevices, ...ownTargets, selfJid]
|
|
1141
|
+
)].filter(jid => !isHostedJid(jid));
|
|
1142
|
+
|
|
1143
|
+
// The participant hash the server checks the fan-out against. whatsmeow puts
|
|
1144
|
+
// it on group stanzas (sendGroup sets node.Attrs["phash"]) and nowhere else;
|
|
1145
|
+
// it is computed over the enumeration above, not over the encryption targets,
|
|
1146
|
+
// so the sending device counts here even though it is not a recipient.
|
|
1147
|
+
const phash = computePhash(enumeratedDevices);
|
|
1148
|
+
|
|
1149
|
+
// Targets to actually encrypt for: everything but the sending device. Both
|
|
1150
|
+
// Baileys and whatsmeow skip it explicitly at this point and not before.
|
|
1134
1151
|
const selfAddrs = new Set([adString(`${ownPhone}@s.whatsapp.net`)]);
|
|
1135
1152
|
if (myLidUser) selfAddrs.add(adString(`${myLidUser}@lid`));
|
|
1136
|
-
const allTargets =
|
|
1137
|
-
.filter(jid => !selfAddrs.has(adString(jid)));
|
|
1153
|
+
const allTargets = enumeratedDevices.filter(jid => !selfAddrs.has(adString(jid)));
|
|
1138
1154
|
|
|
1139
1155
|
const skStore = this._signal.senderKeyStore;
|
|
1140
1156
|
const existingSkdmMap = skStore.getSKDMMap(groupJid);
|
|
@@ -1192,16 +1208,16 @@ class MessageSender {
|
|
|
1192
1208
|
if (mediaSubtype) skmsgAttrs.mediatype = mediaSubtype;
|
|
1193
1209
|
msgContent.push(new BinaryNode('enc', skmsgAttrs, Buffer.isBuffer(skmsgCiphertext) ? skmsgCiphertext : Buffer.from(skmsgCiphertext)));
|
|
1194
1210
|
|
|
1195
|
-
//
|
|
1196
|
-
//
|
|
1197
|
-
//
|
|
1198
|
-
//
|
|
1199
|
-
// stanza rejected outright.
|
|
1211
|
+
// phash belongs on a group stanza and only on a group stanza — whatsmeow's
|
|
1212
|
+
// sendGroup sets it, sendDM does not. It tells the server which device set we
|
|
1213
|
+
// fanned out to; the server compares it against its own and echoes its hash
|
|
1214
|
+
// back on the ack when they differ.
|
|
1200
1215
|
const stanzaAttrs = {
|
|
1201
1216
|
to: jidStrToObj(groupJid),
|
|
1202
1217
|
id: msgId,
|
|
1203
1218
|
type: mediaType,
|
|
1204
1219
|
addressing_mode: groupAddressingMode,
|
|
1220
|
+
phash,
|
|
1205
1221
|
t: String(msNow())
|
|
1206
1222
|
};
|
|
1207
1223
|
if (options.edit) stanzaAttrs.edit = String(options.edit);
|
|
@@ -1223,7 +1239,27 @@ class MessageSender {
|
|
|
1223
1239
|
}
|
|
1224
1240
|
|
|
1225
1241
|
const msgNode = new BinaryNode('message', stanzaAttrs, msgContent);
|
|
1226
|
-
|
|
1242
|
+
const dispatchResult = await this._dispatchAndAck(msgNode, msgId);
|
|
1243
|
+
|
|
1244
|
+
// A hash on the ack that differs from ours means the server's participant
|
|
1245
|
+
// list for this group is not the one we just fanned out to, so some members
|
|
1246
|
+
// did not get the message. whatsmeow drops its group cache on exactly this
|
|
1247
|
+
// signal; the device lists behind the members are dropped too, so the next
|
|
1248
|
+
// send re-runs group metadata and usync rather than repeating the miss.
|
|
1249
|
+
if (dispatchResult && dispatchResult.phash && dispatchResult.phash !== phash) {
|
|
1250
|
+
_whaDbg('[DBG] GROUP_PHASH_MISMATCH ours=' + phash +
|
|
1251
|
+
' server=' + dispatchResult.phash + ' group=' + groupJid +
|
|
1252
|
+
' — invalidating group metadata and device caches');
|
|
1253
|
+
if (typeof this._client.invalidateGroupMetadata === 'function') {
|
|
1254
|
+
this._client.invalidateGroupMetadata(groupJid);
|
|
1255
|
+
}
|
|
1256
|
+
const staleKeys = [...new Set(enumeratedDevices.map(jid =>
|
|
1257
|
+
String(jid).endsWith('@lid') ? 'lid:' + phoneFromJid(jid) : phoneFromJid(jid)
|
|
1258
|
+
))];
|
|
1259
|
+
if (staleKeys.length) this._devMgr.clearCache(staleKeys);
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
return dispatchResult;
|
|
1227
1263
|
}
|
|
1228
1264
|
|
|
1229
1265
|
// ─── Dispatch and wait for ack ────────────────────────────────────────────
|
|
@@ -60,13 +60,19 @@ function encodeDouble(fieldNum, value) {
|
|
|
60
60
|
return Buffer.concat([t, buf]);
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
// Plain text goes in conversation (field 1). Text that carries a contextInfo —
|
|
64
|
+
// a quoted reply, a mention — has to be an ExtendedTextMessage instead, which is
|
|
65
|
+
// field 6 of Message: { text=1, contextInfo=17 }.
|
|
66
|
+
//
|
|
67
|
+
// The wrapper was being written to field 38, which is orderMessage, so replies
|
|
68
|
+
// and mentions arrived as an unreadable order rather than as text.
|
|
63
69
|
function encodeText(text, contextInfo) {
|
|
64
70
|
if (contextInfo) {
|
|
65
71
|
const inner = Buffer.concat([
|
|
66
72
|
field(1, WIRE_LEN, str(text)),
|
|
67
73
|
field(17, WIRE_LEN, encodeContextInfo(contextInfo))
|
|
68
74
|
]);
|
|
69
|
-
return field(
|
|
75
|
+
return field(6, WIRE_LEN, inner);
|
|
70
76
|
}
|
|
71
77
|
return field(1, WIRE_LEN, str(text));
|
|
72
78
|
}
|
|
@@ -260,8 +266,12 @@ function encodeMessage(type, payload) {
|
|
|
260
266
|
case 'sticker': return field(26, WIRE_LEN, payload);
|
|
261
267
|
case 'reaction': return field(46, WIRE_LEN, payload);
|
|
262
268
|
case 'protocol': return field(12, WIRE_LEN, payload);
|
|
263
|
-
|
|
264
|
-
|
|
269
|
+
// extendedTextMessage is field 6. It used to be written to field 2, which is
|
|
270
|
+
// senderKeyDistributionMessage — so an edited message went out claiming to be
|
|
271
|
+
// a SenderKey distribution, and field 2 was not free for the real one.
|
|
272
|
+
case 'extendedText': return field(6, WIRE_LEN, payload);
|
|
273
|
+
// pollCreationMessage is field 49; field 85 is eventCoverImage.
|
|
274
|
+
case 'poll': return field(49, WIRE_LEN, payload);
|
|
265
275
|
case 'location': return field(5, WIRE_LEN, payload);
|
|
266
276
|
case 'contact': return field(4, WIRE_LEN, payload);
|
|
267
277
|
default: return payload;
|
|
@@ -380,12 +390,18 @@ function decodeMessageContainer(buf) {
|
|
|
380
390
|
if (dsm[2]) return decodeMessageContainer(dsm[2]);
|
|
381
391
|
}
|
|
382
392
|
|
|
383
|
-
// Field
|
|
393
|
+
// Field 2: senderKeyDistributionMessage — used to set up sender keys.
|
|
384
394
|
// In WhatsApp Multi-Device, SKDM is bundled WITH the first real message in a new
|
|
385
395
|
// session (even for 1-on-1 DMs). We extract it and continue decoding the real message.
|
|
396
|
+
//
|
|
397
|
+
// This read field 35, which is messageContextInfo. Two things followed: a real
|
|
398
|
+
// distribution from another member (always at field 2) was never seen, so their
|
|
399
|
+
// group messages stayed undecryptable; and the messageContextInfo that ordinary
|
|
400
|
+
// messages carry was fed to processSKDM as if it were a distribution, which
|
|
401
|
+
// writes junk states into the sender-key record.
|
|
386
402
|
let skdmInfo = null;
|
|
387
|
-
if (f[
|
|
388
|
-
const skdm = _decodeFields(f[
|
|
403
|
+
if (f[2]) {
|
|
404
|
+
const skdm = _decodeFields(f[2]);
|
|
389
405
|
// field 1 = groupId (string), field 2 = axolotlSenderKeyDistributionMessage (bytes)
|
|
390
406
|
skdmInfo = {
|
|
391
407
|
groupId: _str(skdm[1]),
|
|
@@ -403,10 +419,10 @@ function decodeMessageContainer(buf) {
|
|
|
403
419
|
if (text) msgResult = { type: 'text', text };
|
|
404
420
|
}
|
|
405
421
|
|
|
406
|
-
// Field
|
|
407
|
-
if (!msgResult && f[
|
|
422
|
+
// Field 6: extendedTextMessage — field 1 = text
|
|
423
|
+
if (!msgResult && f[6] && Buffer.isBuffer(f[6])) {
|
|
408
424
|
try {
|
|
409
|
-
const ext = _decodeFields(f[
|
|
425
|
+
const ext = _decodeFields(f[6]);
|
|
410
426
|
const text = _str(ext[1]);
|
|
411
427
|
if (text) msgResult = { type: 'text', text };
|
|
412
428
|
} catch (_) {}
|
|
@@ -552,11 +568,11 @@ function decodeMessageContainer(buf) {
|
|
|
552
568
|
} catch (_) { msgResult = { type: 'reaction', emoji: '' }; }
|
|
553
569
|
}
|
|
554
570
|
|
|
555
|
-
// Field
|
|
571
|
+
// Field 49: pollCreationMessage
|
|
556
572
|
// encKey=1, name=2 (question), options=3 (repeated PollOption{name=1}), selectableOptionsCount=4
|
|
557
|
-
if (!msgResult && f[
|
|
573
|
+
if (!msgResult && f[49] && Buffer.isBuffer(f[49])) {
|
|
558
574
|
try {
|
|
559
|
-
const poll = _decodeFields(f[
|
|
575
|
+
const poll = _decodeFields(f[49]);
|
|
560
576
|
const question = _str(poll[2]);
|
|
561
577
|
const rawOpts = poll[3];
|
|
562
578
|
const options = [];
|
|
@@ -574,8 +590,8 @@ function decodeMessageContainer(buf) {
|
|
|
574
590
|
} catch (_) { msgResult = { type: 'poll', question: '', options: [] }; }
|
|
575
591
|
}
|
|
576
592
|
|
|
577
|
-
// Field
|
|
578
|
-
if (!msgResult && f[
|
|
593
|
+
// Field 50: pollUpdateMessage (a vote on an existing poll)
|
|
594
|
+
if (!msgResult && f[50] && Buffer.isBuffer(f[50])) {
|
|
579
595
|
msgResult = { type: 'pollVote' };
|
|
580
596
|
}
|
|
581
597
|
|
|
@@ -595,12 +611,20 @@ function decodeMessageContainer(buf) {
|
|
|
595
611
|
}
|
|
596
612
|
}
|
|
597
613
|
|
|
614
|
+
// ─── SenderKeyDistributionMessage (field 2 of Message) ───────────────────────
|
|
615
|
+
// SenderKeyDistributionMessage { groupId=1, axolotlSenderKeyDistributionMessage=2 }
|
|
616
|
+
//
|
|
617
|
+
// This was written to field 35, which in Message is messageContextInfo — an
|
|
618
|
+
// entirely different type. Every group SenderKey we distributed was therefore
|
|
619
|
+
// labelled as context metadata, so no recipient ever saw a distribution, no
|
|
620
|
+
// recipient could open the sender-key session, and every skmsg we sent showed up
|
|
621
|
+
// as "Waiting for this message" on their side. It is field 2 in WAProto.
|
|
598
622
|
function encodeSenderKeyDistributionMessage(groupId, axolotlBytes) {
|
|
599
623
|
const inner = Buffer.concat([
|
|
600
624
|
field(1, WIRE_LEN, str(groupId)),
|
|
601
625
|
field(2, WIRE_LEN, bytes(axolotlBytes))
|
|
602
626
|
]);
|
|
603
|
-
return field(
|
|
627
|
+
return field(2, WIRE_LEN, inner);
|
|
604
628
|
}
|
|
605
629
|
|
|
606
630
|
module.exports = {
|
package/lib/signal/SenderKey.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const { dbg: _whaDbg } = require('../logger');
|
|
4
|
+
|
|
3
5
|
const crypto = require('crypto');
|
|
4
6
|
const fs = require('fs');
|
|
5
7
|
const path = require('path');
|
|
@@ -74,6 +76,16 @@ function deriveMessageKeys(messageKey) {
|
|
|
74
76
|
|
|
75
77
|
const _SK_DEBOUNCE_MS = 300;
|
|
76
78
|
|
|
79
|
+
// Bumped whenever a change means the SenderKey distributions we already sent
|
|
80
|
+
// were never usable by their recipients. The stored "who already has our key"
|
|
81
|
+
// map is then dropped once on load, so the next send to each group distributes
|
|
82
|
+
// again instead of trusting a record of deliveries that never landed.
|
|
83
|
+
//
|
|
84
|
+
// v2: the distribution was written to Message field 35 (messageContextInfo)
|
|
85
|
+
// instead of field 2, so no recipient ever received a readable one.
|
|
86
|
+
const SKDM_DISTRIBUTION_FORMAT = 2;
|
|
87
|
+
const SKDM_FORMAT_KEY = '__skdmFormat__';
|
|
88
|
+
|
|
77
89
|
class SenderKeyStore {
|
|
78
90
|
constructor() {
|
|
79
91
|
this._data = {};
|
|
@@ -96,6 +108,27 @@ class SenderKeyStore {
|
|
|
96
108
|
this._data = {};
|
|
97
109
|
}
|
|
98
110
|
}
|
|
111
|
+
this._migrateSKDMDistribution();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Drop the "already distributed" bookkeeping once when the format changes.
|
|
115
|
+
// The sender keys themselves are kept — only the record of who has them is
|
|
116
|
+
// reset, so the next send to a group re-distributes to every member.
|
|
117
|
+
_migrateSKDMDistribution() {
|
|
118
|
+
if (this._data[SKDM_FORMAT_KEY] === SKDM_DISTRIBUTION_FORMAT) return;
|
|
119
|
+
let dropped = 0;
|
|
120
|
+
for (const key of Object.keys(this._data)) {
|
|
121
|
+
if (key.startsWith('__skdm__')) {
|
|
122
|
+
delete this._data[key];
|
|
123
|
+
dropped++;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
this._data[SKDM_FORMAT_KEY] = SKDM_DISTRIBUTION_FORMAT;
|
|
127
|
+
this._save();
|
|
128
|
+
if (dropped > 0) {
|
|
129
|
+
_whaDbg('[DBG] SKDM_FORMAT_MIGRATION dropped ' + dropped +
|
|
130
|
+
' stale distribution maps — every group re-distributes on next send');
|
|
131
|
+
}
|
|
99
132
|
}
|
|
100
133
|
|
|
101
134
|
_flushSync() {
|