whalibmob 5.33.19 → 5.33.21

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,105 @@
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
+ // ─── _ge: environment security flags ─────────────────────────────────────────
59
+ // sb=false (no security bypass), sv=false (not security-verified by TEE).
60
+ // Always false on a headless client — matching a device that passed basic
61
+ // checks but has no hardware-backed verification.
62
+ function generateGeToken() {
63
+ return encodeURIComponent('{"sb":false,"sv":false}');
64
+ }
65
+
66
+ // ─── _ga: app analytics envelope ─────────────────────────────────────────────
67
+ // bi = SHA-512 of the session nonce (deterministic per session, unique per
68
+ // device); ap/ai/ae are numeric constants observed across Android builds;
69
+ // mp/mu are multi-profile/multi-user booleans (false on a single-user install).
70
+ function generateGaToken(nonceB64) {
71
+ const nonceBuf = Buffer.from(
72
+ (nonceB64 || '').replace(/-/g, '+').replace(/_/g, '/'), 'base64'
73
+ );
74
+ const bi = crypto.createHash('sha512').update(nonceBuf).digest('base64');
75
+ const json = '{"bi":"' + bi + '","ap":247444,"ai":145847,"mp":false,"ae":128383,"mu":false}';
76
+ return encodeURIComponent(json);
77
+ }
78
+
79
+ function generateGsToken() {
80
+ const nowMs = Date.now();
81
+ const sec = Math.floor(nowMs / 1000);
82
+ const usec = (nowMs % 1000) * 1000;
83
+
84
+ // base = ((sec ^ usec) & 0xefff) + 0x1000, range [0x1000, 0xFFFF]
85
+ const base = ((sec ^ usec) & 0xefff) + 0x1000;
86
+
87
+ // Stat fields: index 2 (/system/build.prop) succeeds; all others fail (+1000).
88
+ const statFields = GS_BASE_CODES.map((c, i) => {
89
+ const code = (i === 2) ? c : (c + 1000);
90
+ return (code ^ (base + i * 7)) | 0; // signed int32
91
+ });
92
+
93
+ const randRaw = (crypto.randomBytes(4).readUInt32BE(0) >>> 1);
94
+ const rand = (randRaw ^ (base + 91)) | 0;
95
+
96
+ const millis = BigInt(nowMs);
97
+ const millisLo = Number(BigInt.asUintN(32, millis));
98
+ const millisHi = Number(millis >> 32n);
99
+ const millisLoField = (millisLo ^ ((base + 98) | 0)) | 0;
100
+ const millisHiField = (millisHi ^ ((base + 105) | 0)) | 0;
101
+ const preparedField = (4242 ^ ((base + 112) | 0)) | 0; // WASafeSignalsPrepare called
102
+
103
+ const csv = [1, ...statFields, base >>> 0, rand, millisLoField, millisHiField, preparedField].join(',');
104
+ const plaintext = Buffer.from(csv, 'utf8');
105
+
106
+ const ephSeed = crypto.randomBytes(32);
107
+ const ephKp = curve.generateKeyPair(ephSeed);
108
+ const ephPub = Buffer.from(ephKp.public);
109
+ const shared = Buffer.from(curve.sharedKey(ephKp.private, GS_SERVER_KEY));
110
+
111
+ const iv = Buffer.alloc(12);
112
+ const cipher = crypto.createCipheriv('aes-256-gcm', shared, iv);
113
+ const ct = Buffer.concat([cipher.update(plaintext), cipher.final()]);
114
+ const tag = cipher.getAuthTag();
115
+
116
+ const envelope = Buffer.concat([ephPub, ct, tag]);
117
+ const em = envelope.toString('base64')
118
+ .replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
119
+
120
+ return encodeURIComponent('{"em":"' + em + '"}');
121
+ }
122
+
29
123
  // ─── Empty (NONE) fallbacks ───────────────────────────────────────────────────
30
124
 
31
125
  const EMPTY_ANDROID = { gpia: '', gg: '', gi: '', gp: '', ge: '', ga: '' };
@@ -97,16 +191,12 @@ function fridaGet(pathname, query, device) {
97
191
 
98
192
  // ─── Android: Play Integrity legs (gpia + _gg _gi _gp _ge _ga) ────────────────
99
193
  //
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
194
  // nonceB64 is the URL-safe base64 request nonce (see attestationFieldsAndroid).
106
195
  async function androidIntegrity(nonceB64, device) {
107
196
  if (!isEnabled()) return { ...EMPTY_ANDROID };
108
197
  const res = await fridaGet('/integrity', { authKey: nonceB64 }, device);
109
198
  if (!res || !res.token) return { ...EMPTY_ANDROID };
199
+ _ok('gpia field SET from Frida (' + res.token.length + ' chars)');
110
200
  return { gpia: res.token, gg: '', gi: '', gp: '', ge: '', ga: '' };
111
201
  }
112
202
 
@@ -175,14 +265,19 @@ async function attestationFields(device, nonceB64, opts) {
175
265
  const pushToken = orNull(opts.pushToken);
176
266
 
177
267
  if (device && device.os === 'android') {
178
- const a = await androidIntegrity(nonceB64 || '', device);
268
+ const a = await androidIntegrity(nonceB64 || '', device);
269
+ const gs = generateGsToken();
270
+ const ge = generateGeToken();
271
+ const ga = generateGaToken(nonceB64 || '');
272
+ _ok('_gs field generated (WASafe envelope, ' + gs.length + ' encoded chars)');
179
273
  return [
180
274
  'gpia', orNull(a.gpia),
181
275
  '_gg', orNull(a.gg),
182
276
  '_gi', orNull(a.gi),
183
277
  '_gp', orNull(a.gp),
184
- '_ge', orNull(a.ge),
185
- '_ga', orNull(a.ga),
278
+ '_ge', ge,
279
+ '_ga', ga,
280
+ '_gs', gs,
186
281
  'push_token', pushToken
187
282
  ];
188
283
  }
@@ -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', 'success_get_client_capabilities',
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.19",
3
+ "version": "5.33.21",
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",