whalibmob 5.33.18 → 5.33.20

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.
@@ -21,11 +21,84 @@
21
21
 
22
22
  const crypto = require('crypto');
23
23
  const http = require('http');
24
+ const curve = require('./curve');
25
+
26
+ // ─── Logging helpers (always visible, not gated behind WA_DEBUG) ─────────────
27
+
28
+ function _log(msg) { process.stderr.write('[INTEGRITY] ' + msg + '\n'); }
29
+ function _ok(msg) { process.stderr.write('[INTEGRITY] ✓ ' + msg + '\n'); }
30
+ function _fail(msg) { process.stderr.write('[INTEGRITY] ✗ ' + msg + '\n'); }
24
31
 
25
32
  const DEFAULT_PORT = 1119; // WhatsApp consumer
26
33
  const BUSINESS_PORT = 1120; // the Business build's own attestation server
27
34
  const REQUEST_TIMEMS = 15000;
28
35
 
36
+ // ─── _gs: WASafe environment security signals ─────────────────────────────────
37
+ //
38
+ // WhatsApp sends a fresh WASafe envelope (_gs) on every registration request
39
+ // (/v2/exist, /v2/code, /v2/register). The envelope is a 167-byte blob
40
+ // (ephX25519pub-32 ‖ AES-256-GCM-ct ‖ tag-16) base64url-encoded and wrapped
41
+ // in JSON {"em":"<base64url>"}, then percent-encoded for the form body.
42
+ //
43
+ // The plaintext is a CSV of 19 fields that report an Android environment health
44
+ // profile (root/emulator/tamper checks) plus a timestamp and a random.
45
+ // We generate a "clean reference" profile: only /system/build.prop succeeds
46
+ // (index 2 of 13 stat checks), all others fail — matching a stock Android.
47
+ // The cipher key is the raw X25519 shared secret (no KDF), the server key is
48
+ // the same 32-byte key used to encrypt the registration ENC envelope.
49
+
50
+ const GS_SERVER_KEY = Buffer.from(
51
+ '8e8c0f74c3ebc5d7a6865c6c3c843856b06121cce8ea774d22fb6f122512302d',
52
+ 'hex'
53
+ );
54
+
55
+ // Base stat codes for the 13 path checks; +1000 when stat() fails.
56
+ const GS_BASE_CODES = [10, 11, 0, 100, 101, 102, 103, 104, 1, 105, 106, 107, 108];
57
+
58
+ function generateGsToken() {
59
+ const nowMs = Date.now();
60
+ const sec = Math.floor(nowMs / 1000);
61
+ const usec = (nowMs % 1000) * 1000;
62
+
63
+ // base = ((sec ^ usec) & 0xefff) + 0x1000, range [0x1000, 0xFFFF]
64
+ const base = ((sec ^ usec) & 0xefff) + 0x1000;
65
+
66
+ // Stat fields: index 2 (/system/build.prop) succeeds; all others fail (+1000).
67
+ const statFields = GS_BASE_CODES.map((c, i) => {
68
+ const code = (i === 2) ? c : (c + 1000);
69
+ return (code ^ (base + i * 7)) | 0; // signed int32
70
+ });
71
+
72
+ const randRaw = (crypto.randomBytes(4).readUInt32BE(0) >>> 1);
73
+ const rand = (randRaw ^ (base + 91)) | 0;
74
+
75
+ const millis = BigInt(nowMs);
76
+ const millisLo = Number(BigInt.asUintN(32, millis));
77
+ const millisHi = Number(millis >> 32n);
78
+ const millisLoField = (millisLo ^ ((base + 98) | 0)) | 0;
79
+ const millisHiField = (millisHi ^ ((base + 105) | 0)) | 0;
80
+ const preparedField = (4242 ^ ((base + 112) | 0)) | 0; // WASafeSignalsPrepare called
81
+
82
+ const csv = [1, ...statFields, base >>> 0, rand, millisLoField, millisHiField, preparedField].join(',');
83
+ const plaintext = Buffer.from(csv, 'utf8');
84
+
85
+ const ephSeed = crypto.randomBytes(32);
86
+ const ephKp = curve.generateKeyPair(ephSeed);
87
+ const ephPub = Buffer.from(ephKp.public);
88
+ const shared = Buffer.from(curve.sharedKey(ephKp.private, GS_SERVER_KEY));
89
+
90
+ const iv = Buffer.alloc(12);
91
+ const cipher = crypto.createCipheriv('aes-256-gcm', shared, iv);
92
+ const ct = Buffer.concat([cipher.update(plaintext), cipher.final()]);
93
+ const tag = cipher.getAuthTag();
94
+
95
+ const envelope = Buffer.concat([ephPub, ct, tag]);
96
+ const em = envelope.toString('base64')
97
+ .replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
98
+
99
+ return encodeURIComponent('{"em":"' + em + '"}');
100
+ }
101
+
29
102
  // ─── Empty (NONE) fallbacks ───────────────────────────────────────────────────
30
103
 
31
104
  const EMPTY_ANDROID = { gpia: '', gg: '', gi: '', gp: '', ge: '', ga: '' };
@@ -97,16 +170,12 @@ function fridaGet(pathname, query, device) {
97
170
 
98
171
  // ─── Android: Play Integrity legs (gpia + _gg _gi _gp _ge _ga) ────────────────
99
172
  //
100
- // Queries /integrity?authKey=<nonce>. The Frida server returns { token }, which
101
- // becomes the Play Integrity verdict token (gpia). The _gg/_gi/_gp/_ge/_ga GMS
102
- // legs are only produced by the full native attestor, so a Frida-only setup
103
- // leaves them empty — the server accepts a gpia-only attestation.
104
- //
105
173
  // nonceB64 is the URL-safe base64 request nonce (see attestationFieldsAndroid).
106
174
  async function androidIntegrity(nonceB64, device) {
107
175
  if (!isEnabled()) return { ...EMPTY_ANDROID };
108
176
  const res = await fridaGet('/integrity', { authKey: nonceB64 }, device);
109
177
  if (!res || !res.token) return { ...EMPTY_ANDROID };
178
+ _ok('gpia field SET from Frida (' + res.token.length + ' chars)');
110
179
  return { gpia: res.token, gg: '', gi: '', gp: '', ge: '', ga: '' };
111
180
  }
112
181
 
@@ -175,7 +244,9 @@ async function attestationFields(device, nonceB64, opts) {
175
244
  const pushToken = orNull(opts.pushToken);
176
245
 
177
246
  if (device && device.os === 'android') {
178
- const a = await androidIntegrity(nonceB64 || '', device);
247
+ const a = await androidIntegrity(nonceB64 || '', device);
248
+ const gs = generateGsToken();
249
+ _ok('_gs field generated (WASafe envelope, ' + gs.length + ' encoded chars)');
179
250
  return [
180
251
  'gpia', orNull(a.gpia),
181
252
  '_gg', orNull(a.gg),
@@ -183,6 +254,7 @@ async function attestationFields(device, nonceB64, opts) {
183
254
  '_gp', orNull(a.gp),
184
255
  '_ge', orNull(a.ge),
185
256
  '_ga', orNull(a.ga),
257
+ '_gs', gs,
186
258
  'push_token', pushToken
187
259
  ];
188
260
  }
@@ -1367,6 +1367,13 @@ function buildClientMetrics(attempt, meta) {
1367
1367
  // validates the shape of what it receives and answers a blank push_code with
1368
1368
  // bad_param/bad_format naming that field, while a field that is not there is
1369
1369
  // not validated at all. buildForm drops nulls, which is exactly what we want.
1370
+ function deviceAid(store) {
1371
+ const seed = (store.identityKeyPair && store.identityKeyPair.public)
1372
+ ? Buffer.from(store.identityKeyPair.public)
1373
+ : Buffer.from(String(store.phoneNumber || ''), 'utf8');
1374
+ return crypto.createHash('sha256').update(seed).update('aid').digest('base64');
1375
+ }
1376
+
1370
1377
  function getRequestVerificationCodeParameters(store, method, meta, device, attempt) {
1371
1378
  if (device && device.os === 'android') {
1372
1379
  // Flash call is the one method whose delivery the server only routes when
@@ -1382,12 +1389,12 @@ function getRequestVerificationCodeParameters(store, method, meta, device, attem
1382
1389
  'reason', '',
1383
1390
  'mcc', meta.mcc,
1384
1391
  'mnc', meta.mnc,
1385
- 'feo2_query_status', 'error_security_exception',
1392
+ 'feo2_query_status', 'client_capabilities_cached',
1386
1393
  'db', '1',
1387
1394
  'sim_type', '1',
1388
1395
  'recaptcha', '%7B%22stage%22%3A%22ABPROP_DISABLED%22%7D',
1389
1396
  'network_radio_type', '1',
1390
- 'prefer_sms_over_flash', wantsFlash ? 'false' : 'true',
1397
+ 'prefer_sms_over_flash', wantsFlash ? 'true' : 'false',
1391
1398
  'simnum', '0',
1392
1399
  'airplane_mode_type', '0',
1393
1400
  'client_metrics', buildClientMetrics(attempt, meta),
@@ -1396,7 +1403,7 @@ function getRequestVerificationCodeParameters(store, method, meta, device, attem
1396
1403
  'hasinrc', '1',
1397
1404
  'roaming_type', '0',
1398
1405
  'device_ram', deviceRam(device),
1399
- 'education_screen_displayed', 'true',
1406
+ 'education_screen_displayed', 'false',
1400
1407
  'pid', getAppPid(store),
1401
1408
  'cellular_strength', '5',
1402
1409
  'backup_token', store.backupToken ? toUrlHex(store.backupToken) : '',
@@ -1404,7 +1411,9 @@ function getRequestVerificationCodeParameters(store, method, meta, device, attem
1404
1411
  'call_log_permission', wantsFlash ? 'true' : 'false',
1405
1412
  'manage_call_permission', wantsFlash ? 'true' : 'false',
1406
1413
  'clicked_education_link', 'false',
1407
- 'aid', '',
1414
+ 'aid', deviceAid(store),
1415
+ 'hasav', '2',
1416
+ 'entrypoint', 'suma',
1408
1417
  // Always absent — see the note above the function.
1409
1418
  'push_code', null
1410
1419
  ];
@@ -2391,7 +2400,15 @@ async function verifyCode(store, code, opts) {
2391
2400
  const screen = currentVerifyScreen(store);
2392
2401
  await sendFunnelLog(store, waVersion, screen, 'submit_code', 'submit_code_attempt');
2393
2402
 
2394
- const result = await sendRequest('/register', store, waVersion, true, ['code', normalized]);
2403
+ const result = await sendRequest('/register', store, waVersion, true, [
2404
+ 'code', normalized,
2405
+ 'sim_operator_name', '',
2406
+ 'passkey_login_status', 'not_attempted',
2407
+ 'network_operator_name', '',
2408
+ 'entered', '1',
2409
+ 'has_play_store', 'true',
2410
+ 'entrypoint', 'suma'
2411
+ ]);
2395
2412
 
2396
2413
  const status = result.status;
2397
2414
  if (status === 'ok' || status === 'sent' || status === 'verified') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whalibmob",
3
- "version": "5.33.18",
3
+ "version": "5.33.20",
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",