whalibmob 5.33.22 → 5.33.26

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 CHANGED
@@ -45,7 +45,7 @@ code, and bring the account into being. Both transports, one API.
45
45
  > wa registration --request-code <number> --method wa_old
46
46
  > wa registration --register <number> --code <code>
47
47
  > ```
48
- >
48
+ >
49
49
  > **This is optional.** Most numbers register fine with `--method sms` or `--method voice` — try those first. Use the `wa_old` route **only if** you want a guaranteed, 100% success rate on numbers where SMS/voice are blocked.
50
50
 
51
51
  ##
@@ -6171,4 +6171,5 @@ The **Sponsor** button at the top of the repository leads back here.
6171
6171
 
6172
6172
  ## License
6173
6173
 
6174
- MIT
6174
+ MIT
6175
+ .
package/lib/AndroidApk.js CHANGED
@@ -215,12 +215,14 @@ function _readStringPool(buf, pos) {
215
215
  }
216
216
 
217
217
  /**
218
- * { packageName, versionName, versionCode } out of a binary AndroidManifest.xml.
219
- * Any field the manifest does not carry comes back null.
218
+ * { packageName, versionName, versionCode, permissions } out of a binary
219
+ * AndroidManifest.xml. Any field the manifest does not carry comes back null,
220
+ * and permissions is the list of every <uses-permission> /
221
+ * <uses-permission-sdk-23> name, in manifest order (the _gp hash sorts them).
220
222
  */
221
223
  function parseManifest(buf) {
222
- const empty = { packageName: null, versionName: null, versionCode: null };
223
- if (!buf || buf.length < 8) return empty;
224
+ const out = { packageName: null, versionName: null, versionCode: null, permissions: [] };
225
+ if (!buf || buf.length < 8) return out;
224
226
 
225
227
  let strings = null;
226
228
  let pos = 8; // past the file's own chunk header
@@ -238,32 +240,37 @@ function parseManifest(buf) {
238
240
  const attributeStart = buf.readUInt16LE(ext + 8);
239
241
  const attributeSize = buf.readUInt16LE(ext + 10);
240
242
  const attributeCount = buf.readUInt16LE(ext + 12);
241
- if (strings[nameIndex] !== 'manifest') { pos += chunkSize; continue; }
242
-
243
- const out = Object.assign({}, empty);
244
- for (let i = 0; i < attributeCount; i++) {
245
- const a = ext + attributeStart + i * attributeSize;
246
- const attrName = strings[buf.readUInt32LE(a + 4)];
247
- const rawValue = buf.readUInt32LE(a + 8);
248
- const dataType = buf[a + 15];
249
- const data = buf.readUInt32LE(a + 16);
250
- // A string attribute names its value in the pool twice — the raw source
251
- // text and the typed value. Either will do; the raw one is absent when
252
- // the manifest was written without it.
253
- const asString = () =>
254
- (rawValue !== 0xffffffff && strings[rawValue] !== undefined) ? strings[rawValue]
255
- : (dataType === _TYPE_STRING ? strings[data] : String(data));
256
-
257
- if (attrName === 'package') out.packageName = asString();
258
- else if (attrName === 'versionName') out.versionName = asString();
259
- else if (attrName === 'versionCode') out.versionCode = dataType === _TYPE_STRING
260
- ? parseInt(strings[data], 10) : data;
243
+ const elementName = strings[nameIndex];
244
+
245
+ const attr = (wantName) => {
246
+ for (let i = 0; i < attributeCount; i++) {
247
+ const a = ext + attributeStart + i * attributeSize;
248
+ if (strings[buf.readUInt32LE(a + 4)] !== wantName) continue;
249
+ const rawValue = buf.readUInt32LE(a + 8);
250
+ const dataType = buf[a + 15];
251
+ const data = buf.readUInt32LE(a + 16);
252
+ // A string attribute names its value in the pool twice — the raw
253
+ // source text and the typed value. Either will do; the raw one is
254
+ // absent when the manifest was written without it.
255
+ return (rawValue !== 0xffffffff && strings[rawValue] !== undefined) ? strings[rawValue]
256
+ : (dataType === _TYPE_STRING ? strings[data] : String(data));
257
+ }
258
+ return null;
259
+ };
260
+
261
+ if (elementName === 'manifest') {
262
+ out.packageName = attr('package');
263
+ out.versionName = attr('versionName');
264
+ const vc = attr('versionCode');
265
+ out.versionCode = vc == null ? null : (/^\d+$/.test(vc) ? parseInt(vc, 10) : vc);
266
+ } else if (elementName === 'uses-permission' || elementName === 'uses-permission-sdk-23') {
267
+ const name = attr('name');
268
+ if (name) out.permissions.push(name);
261
269
  }
262
- return out;
263
270
  }
264
271
  pos += chunkSize;
265
272
  }
266
- return empty;
273
+ return out;
267
274
  }
268
275
 
269
276
  // A split's own name, as Android files it: "config.xxhdpi" out of
@@ -339,8 +346,17 @@ function extractMaterial(base, splits) {
339
346
 
340
347
  return {
341
348
  packageName,
342
- secretKey: deriveSecretKey(packageName, aboutLogo),
343
- classesDexMd5: crypto.createHash('md5').update(classesDex).digest(),
349
+ secretKey: deriveSecretKey(packageName, aboutLogo),
350
+ classesDexMd5: crypto.createHash('md5').update(classesDex).digest(),
351
+ // What _gi reports about the installed base.apk: the dex, the whole file,
352
+ // its first 10 MiB and its size.
353
+ classesDexSha256: crypto.createHash('sha256').update(classesDex).digest(),
354
+ apkSize: base.length,
355
+ apkSha256Full: crypto.createHash('sha256').update(base).digest(),
356
+ apkSha256First10M: crypto.createHash('sha256').update(base.subarray(0, 0xA00000)).digest(),
357
+ // _gp: the permission-list hash, computed from this APK's own manifest so
358
+ // it tracks the version rather than a pinned constant.
359
+ gpHash: permissionsHash(manifest.permissions),
344
360
  certificates,
345
361
  // Which drawable the key came from — worth showing, since a bundle carries
346
362
  // one per density and only one of them makes the token the server expects.
@@ -352,6 +368,25 @@ function extractMaterial(base, splits) {
352
368
  };
353
369
  }
354
370
 
371
+ // The _gp value: base64 SHA-256 of the sorted, separator-less concatenation of
372
+ // the requested permission names, as PackageManager.getRequestedPermissions()
373
+ // hands them to the app. On Android 12+ the platform drops the legacy
374
+ // android.permission.BLUETOOTH when BLUETOOTH_CONNECT is present, so the hash
375
+ // matches the device only once that one name is removed. Returns null when the
376
+ // manifest carried no permissions (nothing to hash).
377
+ const _PERM_BLUETOOTH = 'android.permission.BLUETOOTH';
378
+ const _PERM_BLUETOOTH_CONNECT = 'android.permission.BLUETOOTH_CONNECT';
379
+
380
+ function permissionsHash(permissions) {
381
+ if (!Array.isArray(permissions) || !permissions.length) return null;
382
+ let names = permissions.slice();
383
+ if (names.includes(_PERM_BLUETOOTH_CONNECT)) {
384
+ names = names.filter(n => n !== _PERM_BLUETOOTH);
385
+ }
386
+ names.sort();
387
+ return crypto.createHash('sha256').update(Buffer.from(names.join(''), 'utf8')).digest('base64');
388
+ }
389
+
355
390
  // PBKDF2-HMAC-SHA1 over the package name followed by the raw PNG bytes.
356
391
  //
357
392
  // A JCA factory rejects a binary password, so a hand-rolled loop is the usual
@@ -417,12 +452,17 @@ function looksLikeWhatsAppCertificate(description) {
417
452
 
418
453
  function materialToJson(material) {
419
454
  return {
420
- packageName: material.packageName,
421
- secretKey: material.secretKey.toString('base64'),
422
- classesDexMd5: material.classesDexMd5.toString('base64'),
423
- certificates: material.certificates.map(c => c.toString('base64')),
424
- apkVersion: material.apkVersion || null,
425
- apkVersionCode: material.apkVersionCode || null
455
+ packageName: material.packageName,
456
+ secretKey: material.secretKey.toString('base64'),
457
+ classesDexMd5: material.classesDexMd5.toString('base64'),
458
+ classesDexSha256: material.classesDexSha256 ? material.classesDexSha256.toString('base64') : null,
459
+ apkSize: material.apkSize != null ? material.apkSize : null,
460
+ apkSha256Full: material.apkSha256Full ? material.apkSha256Full.toString('base64') : null,
461
+ apkSha256First10M: material.apkSha256First10M ? material.apkSha256First10M.toString('base64') : null,
462
+ gpHash: material.gpHash || null,
463
+ certificates: material.certificates.map(c => c.toString('base64')),
464
+ apkVersion: material.apkVersion || null,
465
+ apkVersionCode: material.apkVersionCode || null
426
466
  };
427
467
  }
428
468
 
@@ -431,12 +471,17 @@ function materialFromJson(json) {
431
471
  throw new Error('android APK material file is incomplete');
432
472
  }
433
473
  return {
434
- packageName: json.packageName || PERSONAL_PACKAGE,
435
- secretKey: Buffer.from(json.secretKey, 'base64'),
436
- classesDexMd5: Buffer.from(json.classesDexMd5, 'base64'),
437
- certificates: json.certificates.map(c => Buffer.from(c, 'base64')),
438
- apkVersion: json.apkVersion || null,
439
- apkVersionCode: json.apkVersionCode || null
474
+ packageName: json.packageName || PERSONAL_PACKAGE,
475
+ secretKey: Buffer.from(json.secretKey, 'base64'),
476
+ classesDexMd5: Buffer.from(json.classesDexMd5, 'base64'),
477
+ classesDexSha256: json.classesDexSha256 ? Buffer.from(json.classesDexSha256, 'base64') : null,
478
+ apkSize: json.apkSize != null ? json.apkSize : null,
479
+ apkSha256Full: json.apkSha256Full ? Buffer.from(json.apkSha256Full, 'base64') : null,
480
+ apkSha256First10M: json.apkSha256First10M ? Buffer.from(json.apkSha256First10M, 'base64') : null,
481
+ gpHash: json.gpHash || null,
482
+ certificates: json.certificates.map(c => Buffer.from(c, 'base64')),
483
+ apkVersion: json.apkVersion || null,
484
+ apkVersionCode: json.apkVersionCode || null
440
485
  };
441
486
  }
442
487
 
@@ -446,6 +491,7 @@ module.exports = {
446
491
  describeCertificate,
447
492
  looksLikeWhatsAppCertificate,
448
493
  deriveSecretKey,
494
+ permissionsHash,
449
495
  computeToken,
450
496
  materialToJson,
451
497
  materialFromJson,
@@ -1,5 +1,5 @@
1
1
  'use strict';
2
-
2
+ //
3
3
  // ─── Device attestation client ────────────────────────────────────────────────
4
4
  //
5
5
  // Talks to the on-device Frida attestation server shipped in ./frida (Android
@@ -64,15 +64,29 @@ function generateGeToken() {
64
64
  }
65
65
 
66
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}';
67
+ // bi AES-256-CBC (key = SHA256(standard base64 of the auth key), random IV
68
+ // prepended) over the base64 of 32 random bytes: 88 base64 characters
69
+ // ap age of base.apk in seconds
70
+ // ai age of the oldest file in the data directory
71
+ // ae age of the oldest file in the external files directory, a little
72
+ // younger than ai
73
+ // mp/mu monkey-test flags, false on a device a person is using
74
+ //
75
+ // The ages model a fresh install, drawn per request: fixed numbers would be
76
+ // the same on every registration this library makes, which no phone does.
77
+ function generateGaToken(authKeyRaw) {
78
+ const aesKey = sha256(Buffer.from(authKeyRaw.toString('base64'), 'utf8'));
79
+ const plaintext = Buffer.from(crypto.randomBytes(32).toString('base64'), 'utf8');
80
+ const iv = crypto.randomBytes(16);
81
+ const cipher = crypto.createCipheriv('aes-256-cbc', aesKey, iv);
82
+ const ct = Buffer.concat([cipher.update(plaintext), cipher.final()]);
83
+ const bi = Buffer.concat([iv, ct]).toString('base64').replace(/\//g, '\\/');
84
+
85
+ const ai = 120 + crypto.randomInt(7081); // 2 min .. 2 h
86
+ const ap = Math.max(1, ai + crypto.randomInt(601) - 300); // ai ± 5 min
87
+ const ae = ai - (30 + crypto.randomInt(91)); // ai − 30..120 s
88
+
89
+ const json = '{"bi":"' + bi + '","ap":' + ap + ',"ai":' + ai + ',"mp":false,"ae":' + ae + ',"mu":false}';
76
90
  return encodeURIComponent(json);
77
91
  }
78
92
 
@@ -120,6 +134,92 @@ function generateGsToken() {
120
134
  return encodeURIComponent('{"em":"' + em + '"}');
121
135
  }
122
136
 
137
+ // ─── Integrity envelope shared by _gg and _gi ────────────────────────────────
138
+ // AES-256-CBC, key = SHA256(standard base64 of the 32-byte auth key), a random
139
+ // 16-byte IV prepended to the ciphertext, then standard base64.
140
+ function integrityEnvelope(authKeyRaw, json) {
141
+ const aesKey = sha256(Buffer.from(authKeyRaw.toString('base64'), 'utf8'));
142
+ const iv = crypto.randomBytes(16);
143
+ const cipher = crypto.createCipheriv('aes-256-cbc', aesKey, iv);
144
+ const ct = Buffer.concat([cipher.update(Buffer.from(json, 'utf8')), cipher.final()]);
145
+ return encodeURIComponent(Buffer.concat([iv, ct]).toString('base64'));
146
+ }
147
+
148
+ // ─── _gg: Play Integrity status ──────────────────────────────────────────────
149
+ // {"_ic": N, "_it": "<token>"}
150
+ // _ic — 0 on success, 1003 when the provider is not prepared (no GMS)
151
+ // _it — the Play Integrity token, or the literal "undefined" on error
152
+ function generateGgToken(authKeyRaw, icCode, itToken) {
153
+ const json = '{"_ic":' + icCode + ',"_it":"' + String(itToken).replace(/\//g, '\\/') + '"}';
154
+ return integrityEnvelope(authKeyRaw, json);
155
+ }
156
+
157
+ // ─── _gp: SHA-256 of the manifest permission list ────────────────────────────
158
+ // Base64 SHA-256 of the sorted, separator-less permission names, percent-encoded
159
+ // on the wire. Computed from the installed APK's own manifest (material.gpHash)
160
+ // so it tracks the version. The constant is the known value for 2.26.35.75+ on
161
+ // Android 12+ (85 permissions) and is used when no material hash is available —
162
+ // an older material file, or a build read without the manifest permissions.
163
+ const GP_ANDROID12 = 'XpccAx8EKQswlTL2MtpCnGIMXTCaUbAf95ElLECc6jk=';
164
+
165
+ function generateGpToken(material) {
166
+ const hash = (material && material.gpHash) || GP_ANDROID12;
167
+ return encodeURIComponent(hash);
168
+ }
169
+
170
+ // ─── _gi: installation integrity ─────────────────────────────────────────────
171
+ // Ten keys, in this order:
172
+ // _dh SHA256(classes.dex of base.apk)
173
+ // _iln SHA256 of the sorted .so names the app unpacks under its data
174
+ // directory (files/decompressed/libs.spo); fixed per package, below
175
+ // _isb size of base.apk in bytes, as a string
176
+ // _ip package name
177
+ // did Build.ID + "." + Build.VERSION.INCREMENTAL
178
+ // _p ApplicationInfo.sourceDir, the installed base.apk path
179
+ // _ln SHA256 of the names in nativeLibraryDir; that directory is empty on
180
+ // current builds, so the app hashes its fallback string "signal_error"
181
+ // _ist SHA256(first 10 MiB of base.apk)
182
+ // _icr SHA1(DER signing certificate)
183
+ // _is SHA256(base.apk)
184
+ // Values are standard base64 with '/' written as '\/'. Returns null when the
185
+ // material predates these hashes, so the field is omitted instead of faked.
186
+ const GI_ILN = {
187
+ 'com.whatsapp': 'i7ls4vAnG2Bvmsp8NDKh7uREGcY4q8Hi4JtdCLix+Cc=',
188
+ 'com.whatsapp.w4b': 'Mw3rbAdtB9wUHE6w9vpff27e6I9/ruCa555vkJXh+N4='
189
+ };
190
+ const GI_LN = crypto.createHash('sha256').update('signal_error').digest('base64');
191
+
192
+ // install: { did, sourceDir } — the firmware string and the base.apk path of
193
+ // the install this session claims to be.
194
+ function generateGiToken(authKeyRaw, material, install) {
195
+ if (!material || !material.classesDexSha256 || material.apkSize == null ||
196
+ !material.apkSha256Full || !material.apkSha256First10M ||
197
+ !material.certificates || !material.certificates.length ||
198
+ !install || !install.did || !install.sourceDir) {
199
+ return null;
200
+ }
201
+ const pkg = material.packageName || 'com.whatsapp';
202
+ const iln = GI_ILN[pkg];
203
+ if (!iln) return null;
204
+
205
+ const esc = (s) => String(s).replace(/\//g, '\\/');
206
+ const b64 = (buf) => esc(buf.toString('base64'));
207
+
208
+ const json = '{' +
209
+ '"_dh":"' + b64(material.classesDexSha256) + '",' +
210
+ '"_iln":"' + esc(iln) + '",' +
211
+ '"_isb":"' + String(material.apkSize) + '",' +
212
+ '"_ip":"' + esc(pkg) + '",' +
213
+ '"did":"' + esc(install.did) + '",' +
214
+ '"_p":"' + esc(install.sourceDir) + '",' +
215
+ '"_ln":"' + esc(GI_LN) + '",' +
216
+ '"_ist":"' + b64(material.apkSha256First10M) + '",' +
217
+ '"_icr":"' + b64(crypto.createHash('sha1').update(material.certificates[0]).digest()) + '",' +
218
+ '"_is":"' + b64(material.apkSha256Full) + '"' +
219
+ '}';
220
+ return integrityEnvelope(authKeyRaw, json);
221
+ }
222
+
123
223
  // ─── Empty (NONE) fallbacks ───────────────────────────────────────────────────
124
224
 
125
225
  const EMPTY_ANDROID = { gpia: '', gg: '', gi: '', gp: '', ge: '', ga: '' };
@@ -268,13 +368,23 @@ async function attestationFields(device, nonceB64, opts) {
268
368
  const a = await androidIntegrity(nonceB64 || '', device);
269
369
  const gs = generateGsToken();
270
370
  const ge = generateGeToken();
271
- const ga = generateGaToken(nonceB64 || '');
371
+ // The envelope key comes from the Noise static public key; a random one
372
+ // still yields a well-formed field when the caller has no key to give.
373
+ const authKeyRaw = (Buffer.isBuffer(opts.authKeyRaw) && opts.authKeyRaw.length === 32)
374
+ ? opts.authKeyRaw
375
+ : crypto.randomBytes(32);
376
+ const ga = opts.ga || generateGaToken(authKeyRaw);
377
+ const gg = a.gpia
378
+ ? generateGgToken(authKeyRaw, 0, a.gpia)
379
+ : generateGgToken(authKeyRaw, 1003, 'undefined');
380
+ const gp = generateGpToken(opts.apkMaterial || null);
381
+ const gi = generateGiToken(authKeyRaw, opts.apkMaterial || null, opts.install || null);
272
382
  _ok('_gs field generated (WASafe envelope, ' + gs.length + ' encoded chars)');
273
383
  return [
274
384
  'gpia', orNull(a.gpia),
275
- '_gg', orNull(a.gg),
276
- '_gi', orNull(a.gi),
277
- '_gp', orNull(a.gp),
385
+ '_gg', gg,
386
+ '_gi', gi,
387
+ '_gp', gp,
278
388
  '_ge', ge,
279
389
  '_ga', ga,
280
390
  '_gs', gs,
@@ -311,6 +421,7 @@ module.exports = {
311
421
  attestationFields,
312
422
  attestBody,
313
423
  androidInfo,
424
+ generateGaToken,
314
425
  toHex,
315
426
  toBase64Url,
316
427
  EMPTY_ANDROID,
@@ -1,5 +1,5 @@
1
1
  'use strict';
2
- //registration tool
2
+ //registration tool//
3
3
  const crypto = require('crypto');
4
4
  const https = require('https');
5
5
  const tls = require('tls');
@@ -1339,15 +1339,59 @@ function getAppPid(store) {
1339
1339
  return store._appPid;
1340
1340
  }
1341
1341
 
1342
- // is_sim_absent used to be the constant false while the same request could send
1343
- // sim_mcc/sim_mnc as 000/000 — which is precisely what a handset reports when
1344
- // there is no SIM in it. One request then said both "a SIM is present" and "no
1345
- // operator", which no real handset ever says. Reading it off the operator the
1346
- // request actually declares keeps the two halves telling one story.
1347
- function buildClientMetrics(attempt, meta) {
1342
+ // ---------- the install _gi describes ----------
1343
+ //
1344
+ // sourceDir is where Android put base.apk: two random 16-byte tags, each
1345
+ // URL-safe base64 with padding, as PackageManager names them. did is the
1346
+ // firmware: Build.ID, a dot, Build.VERSION.INCREMENTAL. Samsung's incremental
1347
+ // is the model without "SM-", region XX, update type U, bootloader revision,
1348
+ // major release, year (X = 2024) and month letters and a build digit; other
1349
+ // vendors are given a numeric one. Both belong to one install, so they come
1350
+ // from the session identity like pid and stay put across /code and /register.
1351
+ function getInstallIdentity(store, device, packageName) {
1352
+ const seed = (store.identityId && Buffer.isBuffer(store.identityId))
1353
+ ? store.identityId
1354
+ : Buffer.from(String(store.phoneNumber || ''), 'utf8');
1355
+ const h = (label) => crypto.createHash('sha256').update(seed).update(label).digest();
1356
+ const tag = (label) => h(label).subarray(0, 16).toString('base64')
1357
+ .replace(/\+/g, '-').replace(/\//g, '_');
1358
+
1359
+ const sourceDir = '/data/app/~~' + tag('apkdir') + '/' +
1360
+ packageName + '-' + tag('pkgdir') + '/base.apk';
1361
+
1362
+ let incremental = device && device.osBuildIncremental;
1363
+ if (!incremental) {
1364
+ const f = h('firmware');
1365
+ const model = String((device && device.modelId) || '');
1366
+ if (/^SM-/i.test(model)) {
1367
+ incremental = model.replace(/^SM-/i, '').toUpperCase() + 'XXU' +
1368
+ String(1 + f[0] % 3) +
1369
+ 'ABC'[f[1] % 3] +
1370
+ 'X' +
1371
+ 'ABCDEFGHIJKL'[f[2] % 12] +
1372
+ String(1 + f[3] % 9);
1373
+ } else {
1374
+ incremental = String(10000000 + f.readUInt32BE(4) % 90000000);
1375
+ }
1376
+ }
1377
+ const buildId = (device && device.osBuildNumber) || 'UP1A.231005.007';
1378
+
1379
+ return { did: buildId + '.' + incremental, sourceDir };
1380
+ }
1381
+
1382
+ // Whether the request describes a phone with no SIM in it. The operator code
1383
+ // is the tell: a real mcc means a card is present, a missing or all-zero mcc
1384
+ // means none. Every SIM-shaped field (is_sim_absent here, sim_type/simnum and
1385
+ // the operator fields in getRequestVerificationCodeParameters) reads it off
1386
+ // this one test, so a request never says a SIM is both present and absent.
1387
+ function _simAbsent(meta) {
1348
1388
  const mcc = meta && meta.mcc != null ? String(meta.mcc).trim() : '';
1349
- const simAbsent = !/^\d+$/.test(mcc) || /^0+$/.test(mcc);
1350
- const json = '{"attempts":' + (attempt || 1)
1389
+ return !/^\d+$/.test(mcc) || /^0+$/.test(mcc);
1390
+ }
1391
+
1392
+ function buildClientMetrics(attempt, meta) {
1393
+ const simAbsent = _simAbsent(meta);
1394
+ const json = '{"attempts":' + (attempt == null ? 1 : attempt)
1351
1395
  + ',"app_campaign_download_source":"google-play|unknown"'
1352
1396
  + ',"is_sim_absent":' + (simAbsent ? 'true' : 'false') + '}';
1353
1397
  return encodeURIComponent(json);
@@ -1382,20 +1426,30 @@ function getRequestVerificationCodeParameters(store, method, meta, device, attem
1382
1426
  // otherwise gets the request answered with an SMS — or with no route at
1383
1427
  // all. They stay at their headless-safe values for every other method.
1384
1428
  const wantsFlash = method === 'flash';
1429
+ // One SIM story across every field. The operator codes decide it: a real
1430
+ // mcc means a SIM is in the phone, so sim_type and simnum say "present" and
1431
+ // the four operator fields carry the home network. A missing or all-zero
1432
+ // mcc is a phone with no SIM, which reports "000" in all four and both SIM
1433
+ // flags at 0 — the shape a device on Wi-Fi without a card actually sends.
1434
+ // buildClientMetrics reads is_sim_absent off the same test, so the request
1435
+ // never says a SIM is both there and not.
1436
+ const simAbsent = _simAbsent(meta);
1437
+ const simMcc = simAbsent ? '000' : meta.mcc;
1438
+ const simMnc = simAbsent ? '000' : meta.mnc;
1385
1439
  return [
1386
1440
  'method', method,
1387
- 'sim_mcc', meta.mcc,
1388
- 'sim_mnc', meta.mnc,
1441
+ 'sim_mcc', simMcc,
1442
+ 'sim_mnc', simMnc,
1389
1443
  'reason', '',
1390
- 'mcc', meta.mcc,
1391
- 'mnc', meta.mnc,
1444
+ 'mcc', simMcc,
1445
+ 'mnc', simMnc,
1392
1446
  'feo2_query_status', 'success_get_client_capabilities',
1393
1447
  'db', '1',
1394
- 'sim_type', '1',
1448
+ 'sim_type', simAbsent ? '0' : '1',
1395
1449
  'recaptcha', '%7B%22stage%22%3A%22ABPROP_DISABLED%22%7D',
1396
1450
  'network_radio_type', '1',
1397
1451
  'prefer_sms_over_flash', wantsFlash ? 'true' : 'false',
1398
- 'simnum', '0',
1452
+ 'simnum', simAbsent ? '0' : '1',
1399
1453
  'airplane_mode_type', '0',
1400
1454
  'client_metrics', buildClientMetrics(attempt, meta),
1401
1455
  'mistyped', '7',
@@ -1431,6 +1485,31 @@ function getRequestVerificationCodeParameters(store, method, meta, device, attem
1431
1485
  ];
1432
1486
  }
1433
1487
 
1488
+ // ---------- /register device parameters (Android) ----------
1489
+ //
1490
+ // The Android app sends its device description again on /register, with the
1491
+ // values it sent on /code, so they are taken from the /code list rather than
1492
+ // restated. client_metrics is rebuilt: its attempt counter restarts at 0 on
1493
+ // this screen.
1494
+ const REGISTER_DEVICE_FIELDS = new Set([
1495
+ 'backup_token', 'sim_mnc', 'device_ram', 'db', 'network_radio_type', 'pid',
1496
+ 'cellular_strength', 'hasinrc', 'aid', 'mcc', 'mnc', 'sim_mcc',
1497
+ 'roaming_type', 'mistyped', 'airplane_mode_type', 'sim_type', 'simnum',
1498
+ 'recaptcha'
1499
+ ]);
1500
+
1501
+ function getRegisterDeviceParameters(store, meta, device) {
1502
+ if (!device || device.os !== 'android') return [];
1503
+ const code = getRequestVerificationCodeParameters(
1504
+ store, store.codeMethod || 'sms', meta, device, 0);
1505
+ const out = [];
1506
+ for (let i = 0; i < code.length; i += 2) {
1507
+ if (REGISTER_DEVICE_FIELDS.has(code[i])) out.push(code[i], code[i + 1]);
1508
+ }
1509
+ out.push('client_metrics', buildClientMetrics(0, meta));
1510
+ return out;
1511
+ }
1512
+
1434
1513
  // ---------- Payload ----------
1435
1514
 
1436
1515
  // base64url WITH padding — matches Java's Base64.getUrlEncoder().encodeToString()
@@ -1537,7 +1616,19 @@ async function buildPayload(store, waVersion, useToken, extraPairs) {
1537
1616
  // were wired up.
1538
1617
  const pushToken = await pushClientFor(device).getPushToken(store, device);
1539
1618
 
1540
- const attestFields = await attestation.attestationFields(device, nonceB64, { pushToken });
1619
+ const authKeyRaw = stripKeyPrefix(store.noiseKeyPair.public);
1620
+ const apkMaterial = device.os === 'android' ? tryLoadAndroidMaterial(device) : null;
1621
+ // One _ga per session: the app sends the same one on /code and /register.
1622
+ if (device.os === 'android' && !store._gaToken) {
1623
+ store._gaToken = attestation.generateGaToken(authKeyRaw);
1624
+ }
1625
+ const attestFields = await attestation.attestationFields(device, nonceB64, {
1626
+ pushToken,
1627
+ authKeyRaw,
1628
+ apkMaterial,
1629
+ install: apkMaterial ? getInstallIdentity(store, device, apkMaterial.packageName) : null,
1630
+ ga: store._gaToken
1631
+ });
1541
1632
 
1542
1633
  return buildForm([
1543
1634
  'cc', cc,
@@ -2403,14 +2494,17 @@ async function verifyCode(store, code, opts) {
2403
2494
  const screen = currentVerifyScreen(store);
2404
2495
  await sendFunnelLog(store, waVersion, screen, 'submit_code', 'submit_code_attempt');
2405
2496
 
2406
- const result = await sendRequest('/register', store, waVersion, true, [
2497
+ // The Android app sends the setup token on /code only; iOS keeps it here.
2498
+ const registerMeta = getCountryMeta(parsePhone(store.phoneNumber).cc, store);
2499
+ const result = await sendRequest('/register', store, waVersion, _device.os !== 'android', [
2407
2500
  'code', normalized,
2408
2501
  'sim_operator_name', '',
2409
2502
  'passkey_login_status', 'not_attempted',
2410
2503
  'network_operator_name', '',
2411
2504
  'entered', '1',
2412
2505
  'has_play_store', 'true',
2413
- 'entrypoint', 'suma'
2506
+ 'entrypoint', 'suma',
2507
+ ...getRegisterDeviceParameters(store, registerMeta, _device)
2414
2508
  ]);
2415
2509
 
2416
2510
  const status = result.status;
@@ -2504,7 +2598,7 @@ module.exports._verify = {
2504
2598
  hasChallenge, is2FARequired, decodeOrNull, isSuccessful,
2505
2599
  normalizeCodeResult, currentVerifyScreen, adoptCanonicalNumber, funnelEnabled, waitHint,
2506
2600
  flashCodeFromCallerId, flashCodeLength, applyFlashFilter, codeForSubmission,
2507
- getRequestVerificationCodeParameters
2601
+ getRequestVerificationCodeParameters, getRegisterDeviceParameters, getInstallIdentity
2508
2602
  };
2509
2603
 
2510
2604
  // Pacing internals, exposed for tests. Not part of the public API.
package/lib/Store.js CHANGED
@@ -209,6 +209,11 @@ function storeToJson(store) {
209
209
  // call it has to know, since that code is read off the caller ID rather
210
210
  // than out of a message.
211
211
  codeMethod: store.codeMethod || null,
212
+ // /code and /register come from one running app, which sends the same
213
+ // access_session_id and the same _ga in both; the steps are usually
214
+ // separate commands, so both are written down rather than drawn again.
215
+ accessSessionId: store._accessSessionId || null,
216
+ gaToken: store._gaToken || null,
212
217
  // Firebase identity behind the push_token. Google issues an android id once
213
218
  // and expects it back; discarding it would mint a new phantom device on
214
219
  // every registration step, which is worse than carrying one.
@@ -266,6 +271,8 @@ function storeFromJson(obj) {
266
271
  simMnc: obj.simMnc || null,
267
272
  codePending: obj.codePending || false,
268
273
  codeMethod: obj.codeMethod || null,
274
+ _accessSessionId: obj.accessSessionId || undefined,
275
+ _gaToken: obj.gaToken || undefined,
269
276
  // Absent on stores written before push tokens existed; null simply means
270
277
  // the first registration step will fetch one.
271
278
  fcm: obj.fcm || null,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whalibmob",
3
- "version": "5.33.22",
3
+ "version": "5.33.26",
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",