whalibmob 5.33.26 → 5.33.28
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 +119 -23
- package/package.json +1 -1
package/lib/Registration.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const crypto = require('crypto');
|
|
4
4
|
const https = require('https');
|
|
5
5
|
const tls = require('tls');
|
|
6
|
+
const zlib = require('zlib');
|
|
6
7
|
const fs = require('fs');
|
|
7
8
|
const os = require('os');
|
|
8
9
|
const path = require('path');
|
|
@@ -40,13 +41,18 @@ function _uaPlatformName(device) {
|
|
|
40
41
|
|
|
41
42
|
function registrationHeaders(device, waVersion) {
|
|
42
43
|
if (device.os === 'android') {
|
|
44
|
+
// Header set and order mirror the native client's registration POST: no
|
|
45
|
+
// Accept, Connection kept alive, gzip advertised (the response is gunzipped
|
|
46
|
+
// on both the direct and SOCKS paths). request_token is an UPPERCASE UUID —
|
|
47
|
+
// the native client never mints a lowercase one.
|
|
43
48
|
return {
|
|
44
|
-
'User-Agent':
|
|
45
|
-
'
|
|
46
|
-
'Accept': 'text/json',
|
|
47
|
-
'WaMsysRequest': '1',
|
|
49
|
+
'User-Agent': `WhatsApp/${waVersion} ${_uaPlatformName(device)}/${device.osVersion} Device/${device.manufacturer}-${device.modelId}`,
|
|
50
|
+
'WaMsysRequest': '1',
|
|
48
51
|
// A fresh one per request, the way the native client mints it.
|
|
49
|
-
'request_token':
|
|
52
|
+
'request_token': uuidv4().toUpperCase(),
|
|
53
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
54
|
+
'Connection': 'Keep-Alive',
|
|
55
|
+
'Accept-Encoding': 'gzip'
|
|
50
56
|
};
|
|
51
57
|
}
|
|
52
58
|
return {
|
|
@@ -66,7 +72,7 @@ const {
|
|
|
66
72
|
parseSocksProxy, socksProxyUrl, socksConnect, proxyAgent
|
|
67
73
|
} = require('./socks');
|
|
68
74
|
|
|
69
|
-
async function httpPostViaSocks(path, body, waVersion, proxyUrl, authHeader, device) {
|
|
75
|
+
async function httpPostViaSocks(path, body, waVersion, proxyUrl, authHeader, device, query) {
|
|
70
76
|
const dHost = 'v.whatsapp.net';
|
|
71
77
|
const dPort = 443;
|
|
72
78
|
|
|
@@ -88,23 +94,27 @@ async function httpPostViaSocks(path, body, waVersion, proxyUrl, authHeader, dev
|
|
|
88
94
|
});
|
|
89
95
|
|
|
90
96
|
const headers = registrationHeaders(device || getDeviceConfig(), waVersion);
|
|
97
|
+
// The native client submits the parameters twice: once in the clear as the
|
|
98
|
+
// URL query, once encrypted inside ENC. The query string here is the exact
|
|
99
|
+
// plaintext that was encrypted, byte for byte.
|
|
100
|
+
const target = query ? `/v2${path}?${query}` : `/v2${path}`;
|
|
91
101
|
const reqLines = [
|
|
92
|
-
`POST
|
|
102
|
+
`POST ${target} HTTP/1.1`,
|
|
93
103
|
`Host: ${dHost}`,
|
|
94
104
|
...Object.keys(headers).map(k => `${k}: ${headers[k]}`),
|
|
95
105
|
`Content-Length: ${Buffer.byteLength(body)}`
|
|
96
106
|
];
|
|
97
107
|
if (authHeader) reqLines.push(`Authorization: ${authHeader}`);
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
reqLines.push(
|
|
108
|
+
// Connection: Keep-Alive rides in from registrationHeaders, matching the
|
|
109
|
+
// native client; the response is still delimited by its own length and the
|
|
110
|
+
// socket is destroyed once it is read.
|
|
111
|
+
reqLines.push('', body);
|
|
102
112
|
const req = reqLines.join('\r\n');
|
|
103
113
|
|
|
104
114
|
tlsSocket.write(req);
|
|
105
115
|
|
|
106
|
-
const { status, body: bodyBuf } = await readHttpResponse(tlsSocket, path);
|
|
107
|
-
const bodyStr = bodyBuf.toString('utf8');
|
|
116
|
+
const { status, headers: resHeaders, body: bodyBuf } = await readHttpResponse(tlsSocket, path);
|
|
117
|
+
const bodyStr = decodeBody(bodyBuf, resHeaders).toString('utf8');
|
|
108
118
|
|
|
109
119
|
if (status !== 200) throw new Error(`HTTP ${status} ${path}: ${bodyStr}`);
|
|
110
120
|
try { return JSON.parse(bodyStr); } catch (_) { return { raw: bodyStr }; }
|
|
@@ -172,6 +182,17 @@ function readHttpResponse(socket, label, timeoutMs) {
|
|
|
172
182
|
});
|
|
173
183
|
}
|
|
174
184
|
|
|
185
|
+
// The registration POST advertises `Accept-Encoding: gzip`, so the server may
|
|
186
|
+
// answer gzip-compressed. Decode by the response's own content-encoding; fall
|
|
187
|
+
// back to the raw bytes if the header lies or the stream will not inflate, so a
|
|
188
|
+
// plain reply still parses. `headers` is the raw header block (any case).
|
|
189
|
+
function decodeBody(bodyBuf, headers) {
|
|
190
|
+
if (/^content-encoding:[ \t]*gzip/im.test(headers || '')) {
|
|
191
|
+
try { return zlib.gunzipSync(bodyBuf); } catch (_) { /* fall through */ }
|
|
192
|
+
}
|
|
193
|
+
return bodyBuf;
|
|
194
|
+
}
|
|
195
|
+
|
|
175
196
|
/**
|
|
176
197
|
* Parse what has arrived so far.
|
|
177
198
|
*
|
|
@@ -1221,6 +1242,61 @@ function toUrlHex(buf) {
|
|
|
1221
1242
|
// IMPORTANT: do NOT use URLSearchParams here — it would double-encode the
|
|
1222
1243
|
// percent-hex sequences in the 'id' field (%AA → %25AA).
|
|
1223
1244
|
|
|
1245
|
+
// The native client inserts registration fields into a LinkedHashMap in a
|
|
1246
|
+
// fixed order; the wire sequence is that insertion order, not a sort. These are
|
|
1247
|
+
// the live orders captured from WhatsApp Android (WA_REVERSE_2026): 50 fields
|
|
1248
|
+
// for /v2/code, 49 for /v2/register. gpia/token/vname that this build omits
|
|
1249
|
+
// simply drop out — their slots collapse.
|
|
1250
|
+
const CODE_PARAM_ORDER = [
|
|
1251
|
+
'method', 'backup_token', '_gs', 'sim_mnc', 'id', 'mnc', '_gg',
|
|
1252
|
+
'network_radio_type', 'lg', 'rc', 'pid', '_ge', 'cellular_strength', 'gpia',
|
|
1253
|
+
'hasinrc', 'aid', 'mcc', '_gp', 'hasav', 'device_ram', 'db', 'sim_type',
|
|
1254
|
+
'prefer_sms_over_flash', 'access_session_id', 'sim_mcc', 'roaming_type',
|
|
1255
|
+
'mistyped', 'airplane_mode_type', 'e_ident', 'e_skey_sig', 'entrypoint',
|
|
1256
|
+
'token', 'in', 'expid', 'simnum', 'cc', 'education_screen_displayed',
|
|
1257
|
+
'e_skey_val', 'authkey', 'e_keytype', 'e_regid', 'client_metrics', '_gi',
|
|
1258
|
+
'e_skey_id', 'feo2_query_status', 'reason', 'fdid', 'recaptcha', '_ga', 'lc'
|
|
1259
|
+
];
|
|
1260
|
+
|
|
1261
|
+
const REGISTER_PARAM_ORDER = [
|
|
1262
|
+
'code', 'backup_token', 'sim_mnc', '_gs', 'id', 'device_ram', 'db', '_gg',
|
|
1263
|
+
'sim_operator_name', 'passkey_login_status', 'lg', 'rc', 'network_radio_type',
|
|
1264
|
+
'pid', '_ge', 'cellular_strength', 'gpia', 'hasinrc', 'aid',
|
|
1265
|
+
'network_operator_name', 'mcc', '_gp', 'mnc', 'entered', 'access_session_id',
|
|
1266
|
+
'sim_mcc', 'roaming_type', 'mistyped', 'airplane_mode_type', 'has_play_store',
|
|
1267
|
+
'sim_type', 'e_ident', 'e_skey_sig', 'entrypoint', 'in', 'expid', 'simnum',
|
|
1268
|
+
'cc', 'e_skey_val', 'authkey', 'e_keytype', 'e_regid', '_gi', 'client_metrics',
|
|
1269
|
+
'e_skey_id', 'fdid', 'recaptcha', '_ga', 'lc'
|
|
1270
|
+
];
|
|
1271
|
+
|
|
1272
|
+
// Reorder a flat [k, v, k, v, …] list to the native insertion order for the
|
|
1273
|
+
// endpoint. Keys outside the reference order (push_token and any build-specific
|
|
1274
|
+
// extras) keep their relative order and trail the known fields, so nothing is
|
|
1275
|
+
// dropped. Only /v2/code and /v2/register have a reference order; every other
|
|
1276
|
+
// endpoint (client_log, exist, …) and every non-Android device passes through
|
|
1277
|
+
// untouched.
|
|
1278
|
+
function reorderRegistrationPairs(pairs, path, device) {
|
|
1279
|
+
const order = path === '/code' ? CODE_PARAM_ORDER
|
|
1280
|
+
: path === '/register' ? REGISTER_PARAM_ORDER
|
|
1281
|
+
: null;
|
|
1282
|
+
if (!order || !device || device.os !== 'android') return pairs;
|
|
1283
|
+
|
|
1284
|
+
const orderSet = new Set(order);
|
|
1285
|
+
const seen = new Map();
|
|
1286
|
+
const extras = [];
|
|
1287
|
+
for (let i = 0; i < pairs.length; i += 2) {
|
|
1288
|
+
const k = pairs[i];
|
|
1289
|
+
if (seen.has(k)) continue;
|
|
1290
|
+
seen.set(k, pairs[i + 1]);
|
|
1291
|
+
if (!orderSet.has(k)) extras.push(k);
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
const out = [];
|
|
1295
|
+
for (const k of order) if (seen.has(k)) out.push(k, seen.get(k));
|
|
1296
|
+
for (const k of extras) out.push(k, seen.get(k));
|
|
1297
|
+
return out;
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1224
1300
|
function buildForm(pairs, extraPairs) {
|
|
1225
1301
|
const parts = [];
|
|
1226
1302
|
for (let i = 0; i < pairs.length; i += 2) {
|
|
@@ -1585,7 +1661,7 @@ function buildVerifiedNameCertificate(store, device) {
|
|
|
1585
1661
|
]));
|
|
1586
1662
|
}
|
|
1587
1663
|
|
|
1588
|
-
async function buildPayload(store, waVersion, useToken, extraPairs) {
|
|
1664
|
+
async function buildPayload(store, waVersion, useToken, extraPairs, path) {
|
|
1589
1665
|
const { cc, national } = parsePhone(store.phoneNumber);
|
|
1590
1666
|
const meta = getCountryMeta(cc, store);
|
|
1591
1667
|
const device = store.device || getDeviceConfig();
|
|
@@ -1630,7 +1706,7 @@ async function buildPayload(store, waVersion, useToken, extraPairs) {
|
|
|
1630
1706
|
ga: store._gaToken
|
|
1631
1707
|
});
|
|
1632
1708
|
|
|
1633
|
-
|
|
1709
|
+
const basePairs = [
|
|
1634
1710
|
'cc', cc,
|
|
1635
1711
|
'in', national,
|
|
1636
1712
|
'rc', String(RELEASE_CHANNEL),
|
|
@@ -1652,7 +1728,12 @@ async function buildPayload(store, waVersion, useToken, extraPairs) {
|
|
|
1652
1728
|
'access_session_id', getAccessSessionId(store),
|
|
1653
1729
|
'token', token,
|
|
1654
1730
|
...attestFields
|
|
1655
|
-
]
|
|
1731
|
+
];
|
|
1732
|
+
const allPairs = extraPairs ? basePairs.concat(extraPairs) : basePairs;
|
|
1733
|
+
// Emit the fields in the native client's fixed insertion order for /code and
|
|
1734
|
+
// /register. The order is part of the wire fingerprint and, with the query
|
|
1735
|
+
// now mirrored into the URL, is visible to the server in the clear.
|
|
1736
|
+
return buildForm(reorderRegistrationPairs(allPairs, path, device));
|
|
1656
1737
|
}
|
|
1657
1738
|
|
|
1658
1739
|
// ---------- Encryption (AES-256-GCM, ephemeral X25519) ----------
|
|
@@ -1672,9 +1753,9 @@ function encryptPayload(plaintext) {
|
|
|
1672
1753
|
|
|
1673
1754
|
// ---------- HTTP ----------
|
|
1674
1755
|
|
|
1675
|
-
function httpPost(path, body, waVersion, authHeader, device) {
|
|
1756
|
+
function httpPost(path, body, waVersion, authHeader, device, query) {
|
|
1676
1757
|
const proxy = socksProxyUrl();
|
|
1677
|
-
if (proxy) return httpPostViaSocks(path, body, waVersion, proxy, authHeader, device);
|
|
1758
|
+
if (proxy) return httpPostViaSocks(path, body, waVersion, proxy, authHeader, device, query);
|
|
1678
1759
|
|
|
1679
1760
|
return new Promise((resolve, reject) => {
|
|
1680
1761
|
const bodyBuf = Buffer.from(body, 'utf8');
|
|
@@ -1685,10 +1766,13 @@ function httpPost(path, body, waVersion, authHeader, device) {
|
|
|
1685
1766
|
// Attestation cert-chain / App Attest header (Android cert chain, iOS
|
|
1686
1767
|
// <attestation>|<keyId>). Only present when body attestation is available.
|
|
1687
1768
|
if (authHeader) headers['Authorization'] = authHeader;
|
|
1769
|
+
// The native client submits the parameters twice: once in the clear as the
|
|
1770
|
+
// URL query, once encrypted inside ENC. The query is the exact ENC
|
|
1771
|
+
// plaintext, byte for byte.
|
|
1688
1772
|
const opts = {
|
|
1689
1773
|
hostname: 'v.whatsapp.net',
|
|
1690
1774
|
port: 443,
|
|
1691
|
-
path: '/v2' + path,
|
|
1775
|
+
path: query ? '/v2' + path + '?' + query : '/v2' + path,
|
|
1692
1776
|
method: 'POST',
|
|
1693
1777
|
timeout: 20000,
|
|
1694
1778
|
headers
|
|
@@ -1697,7 +1781,12 @@ function httpPost(path, body, waVersion, authHeader, device) {
|
|
|
1697
1781
|
const chunks = [];
|
|
1698
1782
|
res.on('data', d => chunks.push(d));
|
|
1699
1783
|
res.on('end', () => {
|
|
1700
|
-
const
|
|
1784
|
+
const encoding = String(res.headers['content-encoding'] || '');
|
|
1785
|
+
let buf = Buffer.concat(chunks);
|
|
1786
|
+
if (/gzip/i.test(encoding)) {
|
|
1787
|
+
try { buf = zlib.gunzipSync(buf); } catch (_) { /* keep raw */ }
|
|
1788
|
+
}
|
|
1789
|
+
const text = buf.toString('utf8');
|
|
1701
1790
|
if (res.statusCode !== 200) {
|
|
1702
1791
|
return reject(new Error(`HTTP ${res.statusCode} ${path}: ${text}`));
|
|
1703
1792
|
}
|
|
@@ -1713,7 +1802,7 @@ function httpPost(path, body, waVersion, authHeader, device) {
|
|
|
1713
1802
|
}
|
|
1714
1803
|
|
|
1715
1804
|
async function sendRequest(path, store, waVersion, useToken, extraPairs) {
|
|
1716
|
-
const plaintext = await buildPayload(store, waVersion, useToken, extraPairs);
|
|
1805
|
+
const plaintext = await buildPayload(store, waVersion, useToken, extraPairs, path);
|
|
1717
1806
|
return sendEncrypted(path, plaintext, store, waVersion);
|
|
1718
1807
|
}
|
|
1719
1808
|
|
|
@@ -1741,7 +1830,14 @@ async function sendEncrypted(path, plaintext, store, waVersion) {
|
|
|
1741
1830
|
|
|
1742
1831
|
_whaDbg('[DBG] REG → ' + path + ' (' + body.length + ' bytes' +
|
|
1743
1832
|
(bodyAtt.bodyAttestation ? ', attested' : '') + ')');
|
|
1744
|
-
|
|
1833
|
+
// The ENC plaintext is also the URL query: the native client submits every
|
|
1834
|
+
// parameter twice, in the clear and encrypted, byte for byte identical. This
|
|
1835
|
+
// is documented and verified only for the Android /code and /register
|
|
1836
|
+
// requests, so the mirror is scoped to them; iOS and the funnel endpoints
|
|
1837
|
+
// keep posting to a bare path, exactly as before.
|
|
1838
|
+
const mirrorQuery = device.os === 'android' && (path === '/code' || path === '/register');
|
|
1839
|
+
const result = await httpPost(path, body, waVersion, bodyAtt.authorizationHeader, device,
|
|
1840
|
+
mirrorQuery ? plaintext : undefined);
|
|
1745
1841
|
_whaDbg('[DBG] REG ← ' + path + ' ' +
|
|
1746
1842
|
(result && result.status ? result.status : JSON.stringify(result).slice(0, 120)) +
|
|
1747
1843
|
(result && result.reason ? ' reason=' + result.reason : ''));
|
|
@@ -2588,7 +2684,7 @@ async function verifyCode(store, code, opts) {
|
|
|
2588
2684
|
module.exports = { checkIfRegistered, checkNumberStatus, requestSmsCode, verifyCode, fetchIosVersion, fetchAndroidVersion, fetchPlayVersion, fetchAndroidVersionLive, fetchWaVersion, currentVersionFor, refreshSessionVersion, clearVersionCache, tryLoadAndroidMaterial, compareVersions, parsePhone, getCountryMeta, assertRegistrationKeys, parseSocksProxy, socksProxyUrl };
|
|
2589
2685
|
|
|
2590
2686
|
// The HTTP reader, exposed for tests. Not part of the public API.
|
|
2591
|
-
module.exports._http = { readHttpResponse, parseHttpResponse, decodeChunkedBody };
|
|
2687
|
+
module.exports._http = { readHttpResponse, parseHttpResponse, decodeChunkedBody, decodeBody, reorderRegistrationPairs };
|
|
2592
2688
|
|
|
2593
2689
|
// Token computation, exposed for tests. Not part of the public API.
|
|
2594
2690
|
module.exports._token = { computeToken, androidMaterialPath, registrationHeaders, ensureAndroidMaterial, deviceForRegistration, buildVerifiedNameCertificate, buildPayload };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "whalibmob",
|
|
3
|
-
"version": "5.33.
|
|
3
|
+
"version": "5.33.28",
|
|
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",
|