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.
- package/lib/Attestation.js +78 -6
- package/lib/Registration.js +22 -5
- package/package.json +1 -1
package/lib/Attestation.js
CHANGED
|
@@ -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
|
|
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
|
}
|
package/lib/Registration.js
CHANGED
|
@@ -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', '
|
|
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 ? '
|
|
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', '
|
|
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, [
|
|
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.
|
|
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",
|