whalibmob 5.33.0 → 5.33.2

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
@@ -29,6 +29,8 @@ code, and bring the account into being. Both transports, one API.
29
29
 
30
30
  [![Device attestation](https://img.shields.io/badge/Device_attestation-Play_Integrity_%2B_App_Attest-8E44AD?style=for-the-badge)](#device-attestation--play-integrity-and-app-attest)
31
31
 
32
+ [![Why registration can be blocked](https://img.shields.io/badge/Why_registration_can_be_blocked-anti--abuse_%26_unofficial--client_checks-C0392B?style=for-the-badge)](#why-registration-can-be-blocked--the-anti-abuse-landscape)
33
+
32
34
  </div>
33
35
 
34
36
  ##
@@ -2775,6 +2777,50 @@ If that is refused too, the number has to go through the real app once, on a pho
2775
2777
  > [!NOTE]
2776
2778
  > The `login` field in that reply is worth reading. Brazilian mobiles gained a ninth digit that WhatsApp never adopted, so `+5571976034186` is filed as `+557176034186`. whalibmob adopts the server's form automatically on a successful registration and saves the session under it — the digit difference is not itself the failure.
2777
2779
 
2780
+ ## Why Registration Can Be Blocked — The Anti-Abuse Landscape
2781
+
2782
+ If registration works on some numbers and fails on others with the same code, the failure is almost never a bug in whalibmob. It is one of two **separate** defences WhatsApp runs, built at different times, for different reasons. Telling them apart is the whole of debugging a block, so this section explains what each one is, where it came from, and what actually moves the needle.
2783
+
2784
+ ### Two different walls
2785
+
2786
+ **Wall 1 — "are you the real app?" (unofficial-client detection).**
2787
+ This is the wall built against **modified WhatsApp clients** — GB WhatsApp, FM WhatsApp, YoWhatsApp, WhatsApp Plus and the rest of that family, mods that repackage the official APK to add themes, dual accounts and privacy toggles. WhatsApp fought them for years and, from around 2024, banned tens of millions of the accounts using them in a single wave. The detection that came out of that fight checks three things on every connection: the **APK signature** (is it signed by WhatsApp Inc. or by a mod author?), the **integrity attestation** (does Play Integrity / App Attest vouch that this is the genuine app on a real device?), and **behaviour**.
2788
+
2789
+ whalibmob is not a mod — but from the server's point of view a whalibmob request with **empty attestation** produces the same answer to those questions as a mod does: *"I can't prove I'm the official app."* WhatsApp does not read your project's name; it reads signals, and on that signal whalibmob and a mod look alike. That is why the same screens a mod triggers can appear here:
2790
+
2791
+ - **`"reason":"blocked"` with a `custom_block_screen`** — *"For security reasons, we can't connect you right now."* The request is refused before the code is even routed.
2792
+ - **"We could not confirm you are using the official WhatsApp app,"** often with a link to download it.
2793
+ - On a **linked/companion** session, **"Use the official WhatsApp Web to continue"** — the same idea, one layer over, aimed at web-class linked devices.
2794
+
2795
+ These tend to appear on numbers that have been **flagged before** (previously banned, or previously seen on an unofficial client). A clean number is usually given the benefit of the doubt and passes with empty attestation; a flagged number has spent that benefit and is asked to *prove* it is the official app — which empty attestation cannot.
2796
+
2797
+ **Wall 2 — "are you spamming?" (anti-abuse / IP reputation).**
2798
+ This wall is **older than the mods** and unrelated to them. WhatsApp scores the **IP address** the request comes from, and if it belongs to a datacenter, a known VPN range or an address already on a spam list, it is refused regardless of how genuine the client looks. This is what fails **many numbers at once from the same server**: the common factor is not the numbers, it is the IP. It is also what a `no_routes` reply (with every `*_wait` at `3600`) usually means — a routing/rate refusal; cool down for an hour and slow down.
2799
+
2800
+ The distinction matters because the fixes are different, and because **Wall 2 would exist even if the mods never had**. Even in a world with no GB WhatsApp, WhatsApp would still stop mass account creation — that is a spam problem it has fought since before any mod existed. There is no configuration that turns a headless server into "unlimited numbers": that is precisely the outcome the anti-abuse system is designed to prevent.
2801
+
2802
+ ### Reading the reply
2803
+
2804
+ | What the server sends back | Which wall | What it means |
2805
+ |---|---|---|
2806
+ | `"reason":"no_routes"`, every `*_wait: 3600` | Anti-abuse | Can't route the code now; usually the IP or rate. Wait out the cooldown, use a cleaner IP. |
2807
+ | `"reason":"blocked"` + `custom_block_screen` | Unofficial-client / reputation | Refused for "security"; the IP and/or the number are distrusted. |
2808
+ | "not using the official app" / download link | Unofficial-client | The number is flagged and is being asked to prove it is the genuine app. |
2809
+ | `"reason":"consent"`, `"pending":"app_store_age"` | Consent (separate) | See [When Registration Is Refused for Consent](#when-registration-is-refused-for-consent). |
2810
+
2811
+ ### What actually helps — and what does not
2812
+
2813
+ Honest, in order of effect:
2814
+
2815
+ - **A clean IP does the most.** A residential or mobile IP, one that is not on a blocklist, clears Wall 2 more than anything else. Datacenter/VPS IPs are the single most common cause of a `blocked` reply across many numbers. Route through a good residential/mobile proxy (see [Routing Traffic Through a Proxy](#routing-traffic-through-a-proxy)), and do not push dozens of numbers through one address.
2816
+ - **Clean, unused numbers.** A number that has never been flagged passes with empty attestation. A number that already carries the "unofficial app" screen has been marked, and is usually not worth fighting — a fresh clean number is the better use of time.
2817
+ - **The Android profile** (`WA_OS=android`) carries more of the fields the server wants than the iOS one, and is the first thing to try when a number is refused.
2818
+ - **Real attestation** answers Wall 1 directly. The Frida scripts in [`frida/`](https://github.com/Kunboruto20/whalibmob/tree/main/frida) mint a genuine Play Integrity / App Attest token **from a real handset with the Play-Store app** and fold it in — this is the honest way to answer "prove you're the official app," because it *is* a real device proving it. It needs a rooted Android or jailbroken iOS phone (see [Device Attestation with Frida](#device-attestation-with-frida-optional)), so it is not always practical; clean numbers do not need it, and no software-only trick substitutes for it.
2819
+
2820
+ ### The honest bottom line
2821
+
2822
+ This is a moving target, not a solved problem. WhatsApp changes these checks continually — the mods went from surviving *months* to surviving *hours* under the same pressure — and whalibmob is kept in step release by release, but nothing here is permanent and there is no magic bypass. Registration on the mobile protocol is a real capability that works, most reliably on **clean numbers from clean IPs**; the further you get from that, the harder the anti-abuse system pushes back, by design.
2823
+
2778
2824
  ## The Push Token
2779
2825
 
2780
2826
  Every WhatsApp on a real phone holds a push token. It is the address the push network uses to wake the app, and no install exists without one — so a registration that ships no `push_token` describes a WhatsApp that cannot be notified, which is a device that does not exist.
@@ -2819,9 +2865,9 @@ Apple's flow is not Google's, but it lands in the same place:
2819
2865
  | 2 | `init-p01st.push.apple.com/bag` | which courier hosts to dial |
2820
2866
  | 3 | `<n>-courier.push.apple.com:443` | the device token, then `GET_TOKEN` for `net.whatsapp.WhatsApp` — the per-topic token that *is* `push_token` |
2821
2867
 
2822
- Step 1 runs once per number and its result is cached on the session: the certificate is good for about three years, and the identity behind it is what makes the same push token come back on the next run. A token WhatsApp has already recorded stops being deliverable if that identity is thrown away, which is the same reason the Firebase android id is kept.
2868
+ Step 1 runs once per number and its result is cached on the session: the certificate Apple issues is good for a year, and the identity behind it is what makes the same push token come back on the next run. A token WhatsApp has already recorded stops being deliverable if that identity is thrown away, which is the same reason the Firebase android id is kept.
2823
2869
 
2824
- The courier connection is a TLS stream with ALPN `apns-security-v3` — the device proves itself with a signature over a fresh nonce in the first frame rather than with a TLS client certificate. A network that terminates TLS in the middle cannot carry it, and the failure says so by name instead of looking like a dropped connection.
2870
+ The courier connection is a TLS stream that offers ALPN `apns-security-v3` — the device proves itself with a signature over a fresh nonce in the first frame rather than with a TLS client certificate. Apple accepts the offer without echoing it back, so a missing echo is normal and is not treated as a failure. A network that terminates TLS in the middle cannot carry the stream; what gives it away is the certificate, since a middlebox has to present one its own CA signed rather than Apple's. The failure names that issuer, instead of looking like a dropped connection.
2825
2871
 
2826
2872
  Turn it off with `WA_APNS_PUSH=0`.
2827
2873
 
@@ -215,17 +215,22 @@ function parseBag(body) {
215
215
  /**
216
216
  * Where to ask for the bag, in the order to try.
217
217
  *
218
- * HTTPS first because that is the one a proxied session can route: the SOCKS
219
- * agent is an https.Agent, so a plain-HTTP attempt would go out direct and
220
- * around the proxy the user asked for. When one is configured the fallback is
221
- * dropped rather than leaked — the default pool is the better answer.
218
+ * Plain HTTP is the canonical URL and the one that actually answers: the bag is
219
+ * signed rather than encrypted, and the host does not serve a certificate for
220
+ * its own name — it presents one for images.apple.com, so HTTPS fails the
221
+ * hostname check every time. It is tried second only in case that ever changes.
222
+ *
223
+ * With a proxy configured the order inverts and HTTP is dropped entirely: the
224
+ * SOCKS agent is an https.Agent, so a plain-HTTP attempt would go out direct,
225
+ * around the proxy the user set precisely so that nothing does. Losing the bag
226
+ * costs nothing — the default pool below is the same answer it would have
227
+ * given.
222
228
  *
223
229
  * @returns {string[]}
224
230
  */
225
231
  function bagUrls() {
226
- const urls = ['https://' + BAG_HOST + BAG_PATH];
227
- if (!socksProxyUrl()) urls.push('http://' + BAG_HOST + BAG_PATH);
228
- return urls;
232
+ if (socksProxyUrl()) return ['https://' + BAG_HOST + BAG_PATH];
233
+ return ['http://' + BAG_HOST + BAG_PATH, 'https://' + BAG_HOST + BAG_PATH];
229
234
  }
230
235
 
231
236
  async function fetchBag() {
@@ -278,6 +283,34 @@ function certificateDer(certificate) {
278
283
  return new crypto.X509Certificate(certificate).raw;
279
284
  }
280
285
 
286
+ // A certificate field can repeat, and Node hands a repeated one back as an
287
+ // array, so every read goes through this rather than assuming a string.
288
+ function issuerValues(issuer, field) {
289
+ if (!issuer || issuer[field] == null) return [];
290
+ return [].concat(issuer[field]).map(String);
291
+ }
292
+
293
+ /**
294
+ * Whether the courier's certificate was issued by Apple.
295
+ *
296
+ * A middlebox that terminates TLS has to present its own certificate, signed
297
+ * by its own CA, because it cannot hold Apple's key. So the issuer's
298
+ * organisation is the one field that tells the real courier from something
299
+ * standing in front of it.
300
+ *
301
+ * @param {object|null} issuer the issuer from getPeerCertificate()
302
+ * @returns {boolean}
303
+ */
304
+ function isApplePeer(issuer) {
305
+ return issuerValues(issuer, 'O').includes('Apple Inc.');
306
+ }
307
+
308
+ /** A readable name for an issuer, for a log line or an error message. */
309
+ function describeIssuer(issuer) {
310
+ const name = issuerValues(issuer, 'CN')[0] || issuerValues(issuer, 'O')[0];
311
+ return name ? '"' + name + '"' : 'an unnamed issuer';
312
+ }
313
+
281
314
  // ─── The connection ───────────────────────────────────────────────────────────
282
315
 
283
316
  class ApnsCourierConnection {
@@ -296,6 +329,10 @@ class ApnsCourierConnection {
296
329
  this.onLost = typeof opts.onLost === 'function' ? opts.onLost : null;
297
330
 
298
331
  this.socket = null;
332
+ // Who signed the certificate the courier presented, set by the TLS
333
+ // handshake. Null until a socket exists, so a failure that happens before
334
+ // one does carries no note about interception.
335
+ this.peerIssuer = null;
299
336
  this.deviceToken = session.deviceToken || null;
300
337
  // Set by the handshake when Apple hands back a device token other than the
301
338
  // one presented. False until then, including before any connection.
@@ -329,6 +366,15 @@ class ApnsCourierConnection {
329
366
  // from. Callers do close on failure, but the connection should not depend
330
367
  // on them remembering to.
331
368
  this.close();
369
+ // A failure on a connection whose certificate Apple did not sign is the
370
+ // signature of TLS being terminated in the middle: the courier never saw
371
+ // the frame, something else did and hung up. Worth naming, because the
372
+ // error on its own reads like Apple refused the credentials.
373
+ if (this.peerIssuer && !isApplePeer(this.peerIssuer)) {
374
+ err.message += ' (the courier presented a certificate from ' +
375
+ describeIssuer(this.peerIssuer) + ', not Apple — TLS is being' +
376
+ ' intercepted on this network)';
377
+ }
332
378
  throw err;
333
379
  }
334
380
  return this;
@@ -345,14 +391,17 @@ class ApnsCourierConnection {
345
391
  host,
346
392
  port: COURIER_PORT,
347
393
  servername: host,
394
+ // Offered as every client offers it. Apple accepts it without echoing
395
+ // it back — see the issuer check below for what that means.
348
396
  ALPNProtocols: [COURIER_ALPN],
349
- // The courier answers with a certificate from an Apple-internal issuer
350
- // that is in no public trust store, so chain validation cannot succeed
351
- // here and is not what protects this connection: the device proves
352
- // itself with a signature over a fresh nonce, and every push that
353
- // matters is a WhatsApp payload the registration server signs for
354
- // separately. Verification stays on for every other socket in the
355
- // library — this is the one endpoint that cannot use it.
397
+ // The courier answers with a certificate from Apple Server
398
+ // Authentication CA, which chains to an Apple root that is not in
399
+ // Node's bundled trust store, so chain validation cannot succeed here
400
+ // and is not what protects this connection: the device proves itself
401
+ // with a signature over a fresh nonce, and every push that matters is
402
+ // a WhatsApp payload the registration server signs for separately.
403
+ // Verification stays on for every other socket in the library — this
404
+ // is the one endpoint that cannot use it.
356
405
  rejectUnauthorized: false
357
406
  };
358
407
  if (base) options.socket = base;
@@ -360,18 +409,17 @@ class ApnsCourierConnection {
360
409
  const socket = tls.connect(options, () => {
361
410
  socket.setTimeout(0);
362
411
  socket.setNoDelay(true);
363
- // The courier speaks its own protocol over the TLS session, and says so
364
- // through ALPN. A middlebox that terminates TLS — a corporate gateway,
365
- // an inspecting proxy — negotiates no protocol at all and then drops
366
- // the connection the moment a frame that is not HTTP goes out. Saying
367
- // that here turns an unexplained disconnect into the one line that
368
- // names the cause.
369
- if (socket.alpnProtocol !== COURIER_ALPN) {
370
- socket.destroy();
371
- reject(new Error('courier did not negotiate ' + COURIER_ALPN +
372
- ' (TLS is being intercepted on this network)'));
373
- return;
374
- }
412
+ // Who signed the certificate on the other end. This, not ALPN, is what
413
+ // tells a genuine courier from a middlebox: Apple's courier accepts the
414
+ // apns-security-v3 offer without echoing it back, so a missing echo is
415
+ // what the real thing looks like too. A first version refused any
416
+ // connection that did not echo it, and so refused Apple itself.
417
+ //
418
+ // The issuer is recorded, not enforced. The handshake decides; this
419
+ // only explains a failure afterwards.
420
+ const certificate = socket.getPeerCertificate() || {};
421
+ this.peerIssuer = certificate.issuer || null;
422
+ _whaDbg('[DBG] APNs courier certificate issued by ' + describeIssuer(this.peerIssuer));
375
423
  resolve(socket);
376
424
  });
377
425
  socket.setTimeout(CONNECT_TIMEOUT_MS, () => socket.destroy(new Error('courier connect timed out')));
@@ -617,6 +665,8 @@ module.exports = {
617
665
  topicHash,
618
666
  createNonce,
619
667
  certificateDer,
668
+ isApplePeer,
669
+ describeIssuer,
620
670
  TAG,
621
671
  DEFAULT_BAG,
622
672
  F_NOTIFICATION_TOPIC,
package/lib/apns.js CHANGED
@@ -16,7 +16,7 @@
16
16
  // we generate, given a FairPlay-signed request. This is what
17
17
  // a Mac or an iPhone does on first boot, and what every
18
18
  // desktop iMessage/APNs client has done since. The
19
- // certificate is good for about three years, so it is
19
+ // certificate Apple signs is good for a year, so it is
20
20
  // persisted with the account and the handshake runs once.
21
21
  // 2. courier one TLS connection to Apple carrying the device token, and
22
22
  // over it GET_TOKEN for net.whatsapp.WhatsApp — the per-topic
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whalibmob",
3
- "version": "5.33.0",
3
+ "version": "5.33.2",
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",