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.
Files changed (2) hide show
  1. package/lib/Registration.js +119 -23
  2. package/package.json +1 -1
@@ -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': `WhatsApp/${waVersion} ${_uaPlatformName(device)}/${device.osVersion} Device/${device.manufacturer}-${device.modelId}`,
45
- 'Content-Type': 'application/x-www-form-urlencoded',
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': uuidv4()
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 /v2${path} HTTP/1.1`,
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
- // Asked for, but not relied on: the server answers and then keeps the
99
- // connection open anyway. The response is finished by its own length, not by
100
- // the socket closing.
101
- reqLines.push(`Connection: close`, '', body);
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
- return buildForm([
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
- ], extraPairs);
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 text = Buffer.concat(chunks).toString('utf8');
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
- const result = await httpPost(path, body, waVersion, bodyAtt.authorizationHeader, device);
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.26",
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",