whalibmob 5.33.0 → 5.33.1
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 +2 -2
- package/lib/apns-courier.js +76 -26
- package/lib/apns.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2819,9 +2819,9 @@ Apple's flow is not Google's, but it lands in the same place:
|
|
|
2819
2819
|
| 2 | `init-p01st.push.apple.com/bag` | which courier hosts to dial |
|
|
2820
2820
|
| 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
2821
|
|
|
2822
|
-
Step 1 runs once per number and its result is cached on the session: the certificate is good for
|
|
2822
|
+
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
2823
|
|
|
2824
|
-
The courier connection is a TLS stream
|
|
2824
|
+
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
2825
|
|
|
2826
2826
|
Turn it off with `WA_APNS_PUSH=0`.
|
|
2827
2827
|
|
package/lib/apns-courier.js
CHANGED
|
@@ -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
|
-
*
|
|
219
|
-
*
|
|
220
|
-
*
|
|
221
|
-
*
|
|
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
|
-
|
|
227
|
-
|
|
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
|
|
350
|
-
//
|
|
351
|
-
//
|
|
352
|
-
//
|
|
353
|
-
//
|
|
354
|
-
//
|
|
355
|
-
//
|
|
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
|
-
//
|
|
364
|
-
//
|
|
365
|
-
//
|
|
366
|
-
//
|
|
367
|
-
// that
|
|
368
|
-
//
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
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
|
|
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.
|
|
3
|
+
"version": "5.33.1",
|
|
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",
|