whalibmob 5.24.2 → 5.25.0
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 +8 -0
- package/index.d.ts +2 -0
- package/lib/Client.js +82 -0
- package/lib/WebStore.js +4 -1
- package/lib/appstate/Mutations.js +2 -0
- package/lib/appstate/SyncdProto.js +6 -1
- package/lib/messages/MessageSender.js +15 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3575,6 +3575,14 @@ whalibmob implements the full lifecycle to prevent this:
|
|
|
3575
3575
|
|
|
3576
3576
|
Token storage uses the **LID JID** of the contact (e.g. `112345678901234@lid`) as the key — never the phone-number JID — matching WhatsApp's internal convention.
|
|
3577
3577
|
|
|
3578
|
+
**cstoken — the companion fallback.** When there is no `<tctoken>` for a recipient, the genuine client attaches a `<cstoken>` instead. Unlike a tctoken it is not granted per contact; it is computed locally:
|
|
3579
|
+
|
|
3580
|
+
```
|
|
3581
|
+
cstoken = HMAC-SHA256(nctSalt, recipient-LID)
|
|
3582
|
+
```
|
|
3583
|
+
|
|
3584
|
+
The `nctSalt` is an account-wide secret the server distributes over app state, so only a **companion (QR / pairing-code) session** ever holds one — a primary (SMS) session has none and sends no cstoken, exactly as the real primary does until it has minted its own. When a companion session receives the salt it fires a `nct_salt` event and persists it; from then on, any first DM without a tctoken carries a `<cstoken>` derived for that recipient's LID. Like the rest of this section it is fully automatic — there is nothing to call.
|
|
3585
|
+
|
|
3578
3586
|
<a id="account-restriction"></a>
|
|
3579
3587
|
|
|
3580
3588
|
### When 463 Means the Account Is Restricted
|
package/index.d.ts
CHANGED
|
@@ -688,6 +688,8 @@ export interface WhalibmobEvents {
|
|
|
688
688
|
/** The server refused the client itself — `405` means the announced version is not accepted. */
|
|
689
689
|
client_rejected: (r: { reason: string; location?: string; message?: string }) => void;
|
|
690
690
|
version_update: (v: { from: string; to: string; source: string }) => void;
|
|
691
|
+
/** A companion session received the NCT salt (over app state) that cstoken is derived from. */
|
|
692
|
+
nct_salt: (s: { bytes: number }) => void;
|
|
691
693
|
apk_material_stale: (m: { materialVersion: string; liveVersion: string; hint?: string }) => void;
|
|
692
694
|
number_corrected: (n: { from: string; to: string }) => void;
|
|
693
695
|
|
package/lib/Client.js
CHANGED
|
@@ -2229,6 +2229,27 @@ class WhalibmobClient extends EventEmitter {
|
|
|
2229
2229
|
case 'delete':
|
|
2230
2230
|
this.emit('chat_removed', { jid, kind: what.kind, remote: true });
|
|
2231
2231
|
break;
|
|
2232
|
+
case 'nct_salt_sync': {
|
|
2233
|
+
// The salt the cstoken fallback is derived from. The server
|
|
2234
|
+
// distributes it into app state; a SET carries the bytes, a REMOVE
|
|
2235
|
+
// withdraws it. Kept on the store so it survives a reconnect. Only a
|
|
2236
|
+
// companion ever gets one, which is why cstoken is a companion-only
|
|
2237
|
+
// thing — a primary has no salt on file and sends none, exactly as
|
|
2238
|
+
// the real primary does until it has minted its own.
|
|
2239
|
+
if (removed) {
|
|
2240
|
+
if (this._store) { this._store.nctSalt = null; this._persistStore(); }
|
|
2241
|
+
_whaDbg('[DBG] NCT_SALT cleared');
|
|
2242
|
+
} else {
|
|
2243
|
+
const salt = a.nctSaltSyncAction && a.nctSaltSyncAction.salt;
|
|
2244
|
+
if (salt && salt.length && this._store) {
|
|
2245
|
+
this._store.nctSalt = Buffer.from(salt).toString('base64');
|
|
2246
|
+
this._persistStore();
|
|
2247
|
+
_whaDbg('[DBG] NCT_SALT stored (' + salt.length + ' bytes)');
|
|
2248
|
+
this.emit('nct_salt', { bytes: salt.length });
|
|
2249
|
+
}
|
|
2250
|
+
}
|
|
2251
|
+
break;
|
|
2252
|
+
}
|
|
2232
2253
|
default:
|
|
2233
2254
|
// Something the server tracks that this library does not model yet.
|
|
2234
2255
|
// Reported rather than dropped, so it is visible that it happened.
|
|
@@ -2707,6 +2728,67 @@ class WhalibmobClient extends EventEmitter {
|
|
|
2707
2728
|
return lid ? lid + device + '@lid' : null;
|
|
2708
2729
|
}
|
|
2709
2730
|
|
|
2731
|
+
// ─── cstoken ────────────────────────────────────────────────────────────────
|
|
2732
|
+
//
|
|
2733
|
+
// The anti-abuse token the genuine client attaches to a message when it has no
|
|
2734
|
+
// trusted-contact token for the recipient. Unlike a tctoken it is not granted
|
|
2735
|
+
// by the server per contact — it is computed locally:
|
|
2736
|
+
//
|
|
2737
|
+
// cstoken = HMAC-SHA256(nctSalt, recipient LID)
|
|
2738
|
+
//
|
|
2739
|
+
// The salt is account-wide and arrives over app state, so only a companion
|
|
2740
|
+
// ever holds one. A primary (an SMS session) has no salt on file and sends no
|
|
2741
|
+
// cstoken, which is what the real primary does until it has minted its own.
|
|
2742
|
+
//
|
|
2743
|
+
// This mirrors whatsmeow's generateCsToken line for line: a regular user, not
|
|
2744
|
+
// the PSA account and not a bot; the recipient resolved to a LID; the HMAC
|
|
2745
|
+
// taken over that LID's bare `user@lid` string.
|
|
2746
|
+
|
|
2747
|
+
/** The stored NCT salt as bytes, or null when this session has none. */
|
|
2748
|
+
_nctSalt() {
|
|
2749
|
+
const b64 = this._store && this._store.nctSalt;
|
|
2750
|
+
if (!b64) return null;
|
|
2751
|
+
try { const b = Buffer.from(b64, 'base64'); return b.length ? b : null; }
|
|
2752
|
+
catch (_) { return null; }
|
|
2753
|
+
}
|
|
2754
|
+
|
|
2755
|
+
/**
|
|
2756
|
+
* The recipient's bare LID (`user@lid`), or null when there is no mapping.
|
|
2757
|
+
*
|
|
2758
|
+
* A JID already on the LID server is used as-is; a phone JID is resolved
|
|
2759
|
+
* through the same PN→LID map every send uses. Device and agent parts are
|
|
2760
|
+
* dropped — the token is taken over the non-AD address, as whatsmeow does.
|
|
2761
|
+
*/
|
|
2762
|
+
_bareLidFor(jid) {
|
|
2763
|
+
if (!jid) return null;
|
|
2764
|
+
const str = String(jid);
|
|
2765
|
+
const user = str.split('@')[0].split(':')[0];
|
|
2766
|
+
if (!user) return null;
|
|
2767
|
+
if (str.endsWith('@lid')) return user + '@lid';
|
|
2768
|
+
const lid = this._pnToLid && this._pnToLid.get(user);
|
|
2769
|
+
return lid ? lid + '@lid' : null;
|
|
2770
|
+
}
|
|
2771
|
+
|
|
2772
|
+
/**
|
|
2773
|
+
* The cstoken bytes for a recipient, or null when none should be sent.
|
|
2774
|
+
*
|
|
2775
|
+
* Returns null — so no `<cstoken>` is attached — unless a salt is on file,
|
|
2776
|
+
* the recipient is a regular user, and a LID resolves for them. Every one of
|
|
2777
|
+
* those is a reason the genuine client also sends nothing.
|
|
2778
|
+
*/
|
|
2779
|
+
_csTokenFor(jid) {
|
|
2780
|
+
const salt = this._nctSalt();
|
|
2781
|
+
if (!salt) return null;
|
|
2782
|
+
|
|
2783
|
+
const { isRegularTcTokenUser } = require('./messages/TcTokenStore');
|
|
2784
|
+
if (!isRegularTcTokenUser(jid)) return null;
|
|
2785
|
+
|
|
2786
|
+
const lidJid = this._bareLidFor(jid);
|
|
2787
|
+
if (!lidJid) return null;
|
|
2788
|
+
|
|
2789
|
+
return crypto.createHmac('sha256', salt).update(lidJid, 'utf8').digest();
|
|
2790
|
+
}
|
|
2791
|
+
|
|
2710
2792
|
// Every address this message's session might be filed under, best first.
|
|
2711
2793
|
_signalJidCandidates(from, participant, senderPn) {
|
|
2712
2794
|
const out = [];
|
package/lib/WebStore.js
CHANGED
|
@@ -63,7 +63,9 @@ function webStoreToJson(store) {
|
|
|
63
63
|
platform: store.platform || null,
|
|
64
64
|
webVersion: store.webVersion || WEB_VERSION_FALLBACK.slice(),
|
|
65
65
|
browser: store.browser || WEB_BROWSER.slice(),
|
|
66
|
-
syncFullHistory: store.syncFullHistory !== false
|
|
66
|
+
syncFullHistory: store.syncFullHistory !== false,
|
|
67
|
+
// The NCT salt (base64) a cstoken is derived from, received over app state.
|
|
68
|
+
nctSalt: store.nctSalt || null
|
|
67
69
|
});
|
|
68
70
|
}
|
|
69
71
|
|
|
@@ -87,6 +89,7 @@ function webStoreFromJson(obj) {
|
|
|
87
89
|
store.webVersion = obj.webVersion || WEB_VERSION_FALLBACK.slice();
|
|
88
90
|
store.browser = obj.browser || WEB_BROWSER.slice();
|
|
89
91
|
store.syncFullHistory = obj.syncFullHistory !== false;
|
|
92
|
+
store.nctSalt = obj.nctSalt || null;
|
|
90
93
|
|
|
91
94
|
return store;
|
|
92
95
|
}
|
|
@@ -96,6 +96,8 @@ function describeIndex(index) {
|
|
|
96
96
|
case 'setting_pushName':return { kind: 'pushName' };
|
|
97
97
|
case 'clearChat': return { kind: 'clear', jid };
|
|
98
98
|
case 'deleteChat': return { kind: 'delete', jid };
|
|
99
|
+
// Account-wide, no jid: the salt the cstoken fallback is derived from.
|
|
100
|
+
case 'nct_salt_sync': return { kind: 'nct_salt_sync' };
|
|
99
101
|
default: return { kind, jid };
|
|
100
102
|
}
|
|
101
103
|
}
|
|
@@ -125,7 +125,10 @@ const ACTION_FIELDS = {
|
|
|
125
125
|
17: 'archiveChatAction',
|
|
126
126
|
20: 'markChatAsReadAction',
|
|
127
127
|
21: 'clearChatAction',
|
|
128
|
-
22: 'deleteChatAction'
|
|
128
|
+
22: 'deleteChatAction',
|
|
129
|
+
// NctSaltSyncAction { salt = 1 }. The salt a cstoken is derived from; the
|
|
130
|
+
// server distributes it into app state and a companion reads it from here.
|
|
131
|
+
80: 'nctSaltSyncAction'
|
|
129
132
|
};
|
|
130
133
|
|
|
131
134
|
function decodeSyncActionValue(buf) {
|
|
@@ -156,6 +159,8 @@ function decodeSyncActionValue(buf) {
|
|
|
156
159
|
out.clearChatAction = {}; break;
|
|
157
160
|
case 'deleteChatAction':
|
|
158
161
|
out.deleteChatAction = {}; break;
|
|
162
|
+
case 'nctSaltSyncAction':
|
|
163
|
+
out.nctSaltSyncAction = { salt: Buffer.isBuffer(a[1]) ? a[1] : null }; break;
|
|
159
164
|
case 'pushNameSetting':
|
|
160
165
|
out.pushNameSetting = { name: _str(a[1]) }; break;
|
|
161
166
|
case 'contactAction':
|
|
@@ -1119,11 +1119,13 @@ class MessageSender {
|
|
|
1119
1119
|
await this._client.ensureTcTokenBeforeSend(tcJid, routingToJid);
|
|
1120
1120
|
}
|
|
1121
1121
|
|
|
1122
|
+
let tcTokenAttached = false;
|
|
1122
1123
|
if (tcStore) {
|
|
1123
1124
|
const tcEntry = tcStore.get(tcJid);
|
|
1124
1125
|
if (tcEntry && tcEntry.token && tcEntry.token.length) {
|
|
1125
1126
|
if (!tcTokenExpired(tcEntry.timestamp)) {
|
|
1126
1127
|
msgContent.push(new BinaryNode('tctoken', {}, tcEntry.token));
|
|
1128
|
+
tcTokenAttached = true;
|
|
1127
1129
|
_whaDbg('[DBG] TCTOKEN_ATTACH jid=' + tcJid);
|
|
1128
1130
|
} else {
|
|
1129
1131
|
// Expired token — clear it, keep senderTimestamp for dedupe
|
|
@@ -1132,6 +1134,19 @@ class MessageSender {
|
|
|
1132
1134
|
}
|
|
1133
1135
|
}
|
|
1134
1136
|
}
|
|
1137
|
+
|
|
1138
|
+
// cstoken — the fallback the genuine client attaches when it has no
|
|
1139
|
+
// trusted-contact token. Locally computed HMAC over the recipient's LID; a
|
|
1140
|
+
// companion holds the salt, a primary does not, so this is a no-op on an SMS
|
|
1141
|
+
// session. Never on a protocol message, and never alongside a tctoken —
|
|
1142
|
+
// exactly the `if tctoken … else if cstoken …` the real client does.
|
|
1143
|
+
if (!tcTokenAttached && !isProtocolMsg) {
|
|
1144
|
+
const cs = this._client._csTokenFor(routingToJid);
|
|
1145
|
+
if (cs && cs.length) {
|
|
1146
|
+
msgContent.push(new BinaryNode('cstoken', {}, cs));
|
|
1147
|
+
_whaDbg('[DBG] CSTOKEN_ATTACH jid=' + tcJid);
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1135
1150
|
// ─────────────────────────────────────────────────────────────────────────
|
|
1136
1151
|
|
|
1137
1152
|
// Extra children a caller needs on the stanza — the <meta appdata> a
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "whalibmob",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.25.0",
|
|
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",
|