whalibmob 5.33.28 → 5.33.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/Registration.js +20 -10
- package/lib/Store.js +7 -2
- package/lib/messages/MessageSender.js +19 -3
- package/lib/messages/ReportingToken.js +50 -9
- package/package.json +1 -1
package/lib/Registration.js
CHANGED
|
@@ -1593,6 +1593,16 @@ function toBase64Url(buf) {
|
|
|
1593
1593
|
return buf.toString('base64').replace(/\+/g, '-').replace(/\//g, '_');
|
|
1594
1594
|
}
|
|
1595
1595
|
|
|
1596
|
+
// The native client encodes the Signal-key and expid wire parameters with
|
|
1597
|
+
// Base64.encodeToString(bytes, 11) = URL_SAFE | NO_WRAP | NO_PADDING: authkey
|
|
1598
|
+
// (43 chars), e_ident/e_skey_val (43), e_skey_sig (86), e_regid (6), e_keytype,
|
|
1599
|
+
// e_skey_id (4), expid (22) — never a trailing '='. The padded form still
|
|
1600
|
+
// decodes server-side, but the bare '=' is a wire tell no real device emits.
|
|
1601
|
+
// The ENC envelope and the vname certificate keep their own encoder above.
|
|
1602
|
+
function toBase64UrlNoPad(buf) {
|
|
1603
|
+
return toBase64Url(buf).replace(/=+$/, '');
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1596
1606
|
// ---------- vname: the Business verified-name certificate ----------
|
|
1597
1607
|
//
|
|
1598
1608
|
// A Business registration carries one extra form field, `vname`, and a consumer
|
|
@@ -1712,18 +1722,18 @@ async function buildPayload(store, waVersion, useToken, extraPairs, path) {
|
|
|
1712
1722
|
'rc', String(RELEASE_CHANNEL),
|
|
1713
1723
|
'lg', meta.lg,
|
|
1714
1724
|
'lc', meta.lc,
|
|
1715
|
-
'authkey',
|
|
1725
|
+
'authkey', toBase64UrlNoPad(stripKeyPrefix(store.noiseKeyPair.public)),
|
|
1716
1726
|
// Business only; buildForm drops a null, so a consumer registration sends
|
|
1717
1727
|
// the same body it always has.
|
|
1718
1728
|
'vname', buildVerifiedNameCertificate(store, device),
|
|
1719
|
-
'e_regid',
|
|
1720
|
-
'e_keytype',
|
|
1721
|
-
'e_ident',
|
|
1722
|
-
'e_skey_id',
|
|
1723
|
-
'e_skey_val',
|
|
1724
|
-
'e_skey_sig',
|
|
1729
|
+
'e_regid', toBase64UrlNoPad(intToBytes(store.registrationId, 4)),
|
|
1730
|
+
'e_keytype', toBase64UrlNoPad(Buffer.from([SIGNAL_KEY_TYPE])),
|
|
1731
|
+
'e_ident', toBase64UrlNoPad(stripKeyPrefix(store.identityKeyPair.public)),
|
|
1732
|
+
'e_skey_id', toBase64UrlNoPad(intToBytes(store.signedPreKey.id, 3)),
|
|
1733
|
+
'e_skey_val', toBase64UrlNoPad(stripKeyPrefix(store.signedPreKey.public)),
|
|
1734
|
+
'e_skey_sig', toBase64UrlNoPad(store.signedPreKey.signature),
|
|
1725
1735
|
'fdid', fdid,
|
|
1726
|
-
'expid',
|
|
1736
|
+
'expid', toBase64UrlNoPad(store.deviceId),
|
|
1727
1737
|
'id', toUrlHex(store.identityId),
|
|
1728
1738
|
'access_session_id', getAccessSessionId(store),
|
|
1729
1739
|
'token', token,
|
|
@@ -1920,7 +1930,7 @@ async function sendFunnelLog(store, waVersion, currentScreen, actionTaken, event
|
|
|
1920
1930
|
'in', national,
|
|
1921
1931
|
'lg', 'en',
|
|
1922
1932
|
'lc', 'US',
|
|
1923
|
-
'expid',
|
|
1933
|
+
'expid', toBase64UrlNoPad(store.deviceId),
|
|
1924
1934
|
'fdid', fdid,
|
|
1925
1935
|
'id', toUrlHex(store.identityId),
|
|
1926
1936
|
'current_screen', currentScreen,
|
|
@@ -1952,7 +1962,7 @@ async function sendPrePnFunnelLog(store, waVersion, actionTaken, eventName) {
|
|
|
1952
1962
|
const body = buildForm([
|
|
1953
1963
|
'lg', 'en',
|
|
1954
1964
|
'lc', 'US',
|
|
1955
|
-
'expid',
|
|
1965
|
+
'expid', toBase64UrlNoPad(store.deviceId),
|
|
1956
1966
|
'fdid', fdid,
|
|
1957
1967
|
'id', toUrlHex(store.identityId),
|
|
1958
1968
|
'current_screen', 'enter_number',
|
package/lib/Store.js
CHANGED
|
@@ -82,7 +82,11 @@ function createNewStore(phoneNumber, opts) {
|
|
|
82
82
|
|
|
83
83
|
const registrationId = (crypto.randomBytes(2).readUInt16BE(0) & 0x3fff) + 1;
|
|
84
84
|
const fdid = uuidv4();
|
|
85
|
-
|
|
85
|
+
// expid goes on the wire as a UUID and WhatsApp reads its version/variant
|
|
86
|
+
// nibbles (the native client builds it with UUID.nameUUIDFromBytes/randomUUID).
|
|
87
|
+
// 16 fully random bytes land on a valid UUID shape only ~1 time in 16, so the
|
|
88
|
+
// server sees a malformed UUID; take the 16 bytes of a real v4 UUID instead.
|
|
89
|
+
const deviceId = Buffer.from(uuidv4().replace(/-/g, ''), 'hex');
|
|
86
90
|
const identityId = crypto.randomBytes(16);
|
|
87
91
|
// Device fingerprint fields the native client sends on /code (see
|
|
88
92
|
// Registration.js): advertising id (UUID) and the 20-byte backup token.
|
|
@@ -358,7 +362,8 @@ function fromSixParts(sixParts) {
|
|
|
358
362
|
},
|
|
359
363
|
registrationId: (crypto.randomBytes(2).readUInt16BE(0) & 0x3fff) + 1,
|
|
360
364
|
fdid: uuidv4(),
|
|
361
|
-
|
|
365
|
+
// expid must be a valid UUID on the wire — see createNewStore above.
|
|
366
|
+
deviceId: Buffer.from(uuidv4().replace(/-/g, ''), 'hex'),
|
|
362
367
|
identityId,
|
|
363
368
|
advertisingId: uuidv4(),
|
|
364
369
|
backupToken: crypto.randomBytes(20),
|
|
@@ -27,7 +27,7 @@ const { uploadMedia } = require('../MediaService');
|
|
|
27
27
|
const { imageThumbnail, videoThumbnail, probeImageSize } = require('../MediaThumbnail');
|
|
28
28
|
const { tcTokenExpired, shouldSendNewTcToken, normalizeJidForTcToken,
|
|
29
29
|
isRegularTcTokenUser } = require('./TcTokenStore');
|
|
30
|
-
const { buildReportingNode } = require('./ReportingToken');
|
|
30
|
+
const { buildReportingNode, ensureMessageSecret } = require('./ReportingToken');
|
|
31
31
|
|
|
32
32
|
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
33
33
|
|
|
@@ -937,6 +937,13 @@ class MessageSender {
|
|
|
937
937
|
const recipientPhone = phoneFromJid(toJid);
|
|
938
938
|
const ownPhone = String(this._store.phoneNumber);
|
|
939
939
|
const ownMainJid = `${ownPhone}@s.whatsapp.net`;
|
|
940
|
+
|
|
941
|
+
// Stamp the message secret the native client puts on every reportable
|
|
942
|
+
// message, before the plaintext is wrapped in a DeviceSentMessage, cached
|
|
943
|
+
// for retries, or encrypted. A no-op for messages that carry no reporting
|
|
944
|
+
// token or already hold a secret (a poll), so it only ever adds the field
|
|
945
|
+
// that was missing from plain text and media. See ensureMessageSecret.
|
|
946
|
+
plaintext = ensureMessageSecret(plaintext);
|
|
940
947
|
const mediaSubtype = options._mediaSubtype || null;
|
|
941
948
|
const decryptFail = resolveDecryptFail(mediaType, options);
|
|
942
949
|
|
|
@@ -1080,8 +1087,11 @@ class MessageSender {
|
|
|
1080
1087
|
// it and have the server verify the reported text is what was actually
|
|
1081
1088
|
// sent. Sending the secret without the token is a combination no real
|
|
1082
1089
|
// client produces.
|
|
1090
|
+
// Our own identity in the same address family as the destination — the
|
|
1091
|
+
// original sender half of the token's key derivation.
|
|
1092
|
+
const ownReportingJid = isLidTarget ? `${ownLidUser}@lid` : ownMainJid;
|
|
1083
1093
|
const reportingNode = buildReportingNode({
|
|
1084
|
-
plaintext, msgId, remoteJid: routingToJid, BinaryNode
|
|
1094
|
+
plaintext, msgId, remoteJid: routingToJid, senderJid: ownReportingJid, BinaryNode
|
|
1085
1095
|
});
|
|
1086
1096
|
if (reportingNode) {
|
|
1087
1097
|
msgContent.push(reportingNode);
|
|
@@ -1233,6 +1243,12 @@ class MessageSender {
|
|
|
1233
1243
|
const mediaSubtype = options._mediaSubtype || null;
|
|
1234
1244
|
const decryptFail = resolveDecryptFail(mediaType, options);
|
|
1235
1245
|
|
|
1246
|
+
// Same message secret the native client stamps on reportable group
|
|
1247
|
+
// messages, before the plaintext is cached or encrypted. A no-op for a poll
|
|
1248
|
+
// (already carries one) or a reaction (carries no token). See
|
|
1249
|
+
// ensureMessageSecret.
|
|
1250
|
+
plaintext = ensureMessageSecret(plaintext);
|
|
1251
|
+
|
|
1236
1252
|
const isStatus = isStatusBroadcast(groupJid);
|
|
1237
1253
|
|
|
1238
1254
|
// A Status has no group metadata; its recipients come from the account's
|
|
@@ -1495,7 +1511,7 @@ class MessageSender {
|
|
|
1495
1511
|
// is the common case: it has a message secret, so it needs the token that
|
|
1496
1512
|
// lets a report on it be verified.
|
|
1497
1513
|
const groupReportingNode = buildReportingNode({
|
|
1498
|
-
plaintext, msgId, remoteJid: groupJid, BinaryNode
|
|
1514
|
+
plaintext, msgId, remoteJid: groupJid, senderJid: ownJid, BinaryNode
|
|
1499
1515
|
});
|
|
1500
1516
|
if (groupReportingNode) {
|
|
1501
1517
|
msgContent.push(groupReportingNode);
|
|
@@ -16,13 +16,17 @@
|
|
|
16
16
|
// not change the token. REPORTING_FIELDS below is that subset, and it is the
|
|
17
17
|
// part that has to match byte for byte or the token is worthless.
|
|
18
18
|
//
|
|
19
|
-
// The key is derived
|
|
20
|
-
//
|
|
21
|
-
//
|
|
19
|
+
// The key is derived from the message secret, keyed by the two identities of
|
|
20
|
+
// the exchange: our own JID as the original sender and the destination JID as
|
|
21
|
+
// the party the modification is addressed to — the same binding the native
|
|
22
|
+
// client uses (origMsgSender = sender, modificationSender = recipient). The
|
|
23
|
+
// server recomputes with those same two identities when a report is filed, so
|
|
24
|
+
// getting them wrong leaves a token that no report could ever verify.
|
|
22
25
|
|
|
23
26
|
const crypto = require('crypto');
|
|
24
27
|
const { hkdfSha256 } = require('../hkdf');
|
|
25
28
|
const { sha256 } = require('@noble/hashes/sha256');
|
|
29
|
+
const { encodeMessageContextInfo } = require('../proto/MessageProto');
|
|
26
30
|
|
|
27
31
|
// Protobuf field numbers on Message that the token is computed over.
|
|
28
32
|
// f field number
|
|
@@ -315,6 +319,38 @@ function shouldIncludeReportingToken(present) {
|
|
|
315
319
|
!present.has(F_POLL_UPDATE);
|
|
316
320
|
}
|
|
317
321
|
|
|
322
|
+
/**
|
|
323
|
+
* Give an outgoing message the message secret the native client puts on
|
|
324
|
+
* everything reportable, so the reporting token below can actually be built.
|
|
325
|
+
*
|
|
326
|
+
* The native client mints a random 32-byte secret into
|
|
327
|
+
* MessageContextInfo.messageSecret for every message that carries a reporting
|
|
328
|
+
* token, and reuses an existing one when the message already has it (a poll
|
|
329
|
+
* writes its own secret there). Without this step ordinary text and media go
|
|
330
|
+
* out with no secret and therefore no <reporting> node at all — a shape no real
|
|
331
|
+
* client produces, and the gap this function closes.
|
|
332
|
+
*
|
|
333
|
+
* Returns the plaintext unchanged when the message is one that carries no token
|
|
334
|
+
* (reactions, poll votes) or already has a secret; otherwise returns a new
|
|
335
|
+
* buffer with field 35 appended. Field 35 is not part of REPORTING_FIELDS, so
|
|
336
|
+
* appending it never changes the token's content hash. On malformed input the
|
|
337
|
+
* plaintext is returned untouched.
|
|
338
|
+
*
|
|
339
|
+
* @param {Buffer} plaintext encoded Message protobuf, before encryption
|
|
340
|
+
* @returns {Buffer}
|
|
341
|
+
*/
|
|
342
|
+
function ensureMessageSecret(plaintext) {
|
|
343
|
+
if (!Buffer.isBuffer(plaintext)) return plaintext;
|
|
344
|
+
const info = inspectMessage(plaintext);
|
|
345
|
+
if (!info.ok) return plaintext;
|
|
346
|
+
if (!shouldIncludeReportingToken(info.present)) return plaintext;
|
|
347
|
+
if (info.messageSecret && info.messageSecret.length) return plaintext;
|
|
348
|
+
|
|
349
|
+
const ctx = encodeMessageContextInfo({ messageSecret: crypto.randomBytes(32) });
|
|
350
|
+
if (!ctx || ctx.length === 0) return plaintext;
|
|
351
|
+
return Buffer.concat([plaintext, ctx]);
|
|
352
|
+
}
|
|
353
|
+
|
|
318
354
|
/**
|
|
319
355
|
* Build the <reporting> node for an outgoing message, or null when there is
|
|
320
356
|
* nothing to attach.
|
|
@@ -322,21 +358,25 @@ function shouldIncludeReportingToken(present) {
|
|
|
322
358
|
* @param {object} opts
|
|
323
359
|
* plaintext encoded Message protobuf, exactly as it will be encrypted
|
|
324
360
|
* msgId the stanza id
|
|
325
|
-
* remoteJid the destination JID
|
|
326
|
-
*
|
|
361
|
+
* remoteJid the destination JID (recipient for a DM, group JID for a group)
|
|
362
|
+
* senderJid our own JID — the original sender in the key derivation;
|
|
363
|
+
* falls back to remoteJid when unset
|
|
327
364
|
* BinaryNode the node constructor
|
|
328
365
|
*/
|
|
329
366
|
function buildReportingNode(opts) {
|
|
330
|
-
const { plaintext, msgId, remoteJid,
|
|
367
|
+
const { plaintext, msgId, remoteJid, senderJid, BinaryNode } = opts;
|
|
331
368
|
if (!Buffer.isBuffer(plaintext) || !msgId || !remoteJid) return null;
|
|
332
369
|
|
|
333
370
|
const info = inspectMessage(plaintext);
|
|
334
371
|
if (!info.ok || !info.messageSecret || !info.messageSecret.length) return null;
|
|
335
372
|
if (!shouldIncludeReportingToken(info.present)) return null;
|
|
336
373
|
|
|
337
|
-
// fromMe is always true here — we only ever build this for our own sends
|
|
338
|
-
|
|
339
|
-
|
|
374
|
+
// fromMe is always true here — we only ever build this for our own sends, so
|
|
375
|
+
// the original sender is us and the modification is addressed to the
|
|
376
|
+
// destination. Keyed the other way (both halves the destination), the token
|
|
377
|
+
// was present but no filed report could verify it.
|
|
378
|
+
const from = String(senderJid || remoteJid); // origMsgSender = us
|
|
379
|
+
const to = String(remoteJid); // modificationSender = destination
|
|
340
380
|
|
|
341
381
|
const reportingSecret = generateMsgSecretKey(
|
|
342
382
|
ENC_SECRET_REPORT_TOKEN, msgId, from, to, info.messageSecret
|
|
@@ -355,6 +395,7 @@ function buildReportingNode(opts) {
|
|
|
355
395
|
|
|
356
396
|
module.exports = {
|
|
357
397
|
buildReportingNode,
|
|
398
|
+
ensureMessageSecret,
|
|
358
399
|
shouldIncludeReportingToken,
|
|
359
400
|
extractReportingTokenContent,
|
|
360
401
|
generateMsgSecretKey,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "whalibmob",
|
|
3
|
-
"version": "5.33.
|
|
3
|
+
"version": "5.33.30",
|
|
4
4
|
"description": "Node.js library for WhatsApp — register a number over SMS, or link as a companion by QR. Signal E2E encryption, media, groups, channels.",
|
|
5
5
|
"author": "Kunboruto20",
|
|
6
6
|
"main": "index.js",
|