whalibmob 5.5.56 → 5.5.59

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.
Files changed (49) hide show
  1. package/README.md +526 -58
  2. package/cli.js +43 -20
  3. package/index.js +36 -6
  4. package/lib/Attestation.js +212 -0
  5. package/lib/Client.js +842 -330
  6. package/lib/DeviceManager.js +700 -182
  7. package/lib/HistorySyncHandler.js +759 -0
  8. package/lib/MediaService.js +62 -45
  9. package/lib/Registration.js +202 -184
  10. package/lib/Store.js +76 -19
  11. package/lib/WAUSync/Protocols/USyncContactProtocol.js +55 -0
  12. package/lib/WAUSync/Protocols/USyncDeviceProtocol.js +76 -0
  13. package/lib/WAUSync/Protocols/USyncDisappearingModeProtocol.js +29 -0
  14. package/lib/WAUSync/Protocols/USyncLIDProtocol.js +34 -0
  15. package/lib/WAUSync/Protocols/USyncStatusProtocol.js +40 -0
  16. package/lib/WAUSync/Protocols/USyncUsernameProtocol.js +27 -0
  17. package/lib/WAUSync/Protocols/index.js +17 -0
  18. package/lib/WAUSync/USyncQuery.js +113 -0
  19. package/lib/WAUSync/USyncUser.js +40 -0
  20. package/lib/WAUSync/index.js +11 -0
  21. package/lib/auth-utils.js +693 -0
  22. package/lib/logger.js +36 -0
  23. package/lib/messages/MessageSender.js +386 -113
  24. package/lib/messages/TcTokenStore.js +116 -0
  25. package/lib/noise.js +43 -8
  26. package/lib/proto/MessageProto.js +28 -2
  27. package/lib/signal/SenderKey.js +12 -0
  28. package/lib/signal/SignalProtocol.js +70 -46
  29. package/lib/signal/SignalStore.js +20 -2
  30. package/lib/signal/WaSignalGroup/ciphertext-message.js +15 -0
  31. package/lib/signal/WaSignalGroup/group-session-builder.js +45 -0
  32. package/lib/signal/WaSignalGroup/group_cipher.js +92 -0
  33. package/lib/signal/WaSignalGroup/index.js +27 -0
  34. package/lib/signal/WaSignalGroup/keyhelper.js +24 -0
  35. package/lib/signal/WaSignalGroup/sender-chain-key.js +35 -0
  36. package/lib/signal/WaSignalGroup/sender-key-distribution-message.js +136 -0
  37. package/lib/signal/WaSignalGroup/sender-key-message.js +139 -0
  38. package/lib/signal/WaSignalGroup/sender-key-name.js +59 -0
  39. package/lib/signal/WaSignalGroup/sender-key-record.js +63 -0
  40. package/lib/signal/WaSignalGroup/sender-key-state.js +101 -0
  41. package/lib/signal/WaSignalGroup/sender-message-key.js +34 -0
  42. package/lib/signal/libsignal/curve.js +3 -1
  43. package/lib/signal/libsignal/queue_job.js +3 -1
  44. package/lib/signal/libsignal/session_builder.js +3 -1
  45. package/lib/signal/libsignal/session_cipher.js +3 -1
  46. package/lib/signal/libsignal/session_record.js +9 -9
  47. package/package.json +5 -8
  48. package/.env.example +0 -62
  49. package/lib/AttestationClient.js +0 -259
package/cli.js CHANGED
@@ -35,7 +35,9 @@ const {
35
35
  loadStore
36
36
  } = require('./lib/Client');
37
37
 
38
- const VERSION = '5.1.15';
38
+ const { assertMeId, initAuthCreds } = require('./lib/auth-utils');
39
+
40
+ const VERSION = '5.5.59';
39
41
 
40
42
  // ─── output helpers ───────────────────────────────────────────────────────────
41
43
 
@@ -194,9 +196,10 @@ const HELP = `
194
196
  /biz <phone|jid> query business profile
195
197
 
196
198
  Registration
197
- /reg check <phone> check if number has WhatsApp
198
- /reg code <phone> [sms|voice|wa_old] request verification code
199
- /reg confirm <phone> <code> complete registration
199
+ /reg check <phone> check if number has WhatsApp
200
+ /reg code <phone> [sms|voice|wa_old] request verification code
201
+ /reg code <phone> email <address> request code via email
202
+ /reg confirm <phone> <code> complete registration
200
203
 
201
204
  Connection
202
205
  /connect <phone> connect to WhatsApp
@@ -435,14 +438,18 @@ async function handleLine(line) {
435
438
  if (_client) { try { _client.disconnect(); } catch (_) {} }
436
439
  process.exit(0);
437
440
 
438
- case '/session':
441
+ case '/session': {
439
442
  if (!_client || !_client.store) { fail('not connected'); break; }
443
+ let _meJid = '—';
444
+ try { _meJid = assertMeId(_client.store); } catch (_) {}
440
445
  hr();
441
446
  kv('phone', _client.store.phoneNumber);
447
+ kv('jid', _meJid);
442
448
  kv('name', _client.store.pushName || _client.store.name || '—');
443
449
  kv('session', _sessDir);
444
450
  hr();
445
451
  break;
452
+ }
446
453
 
447
454
  case '/connect': {
448
455
  const ph = p[1];
@@ -1147,13 +1154,19 @@ async function handleLine(line) {
1147
1154
  }
1148
1155
  else if (sub === 'code') {
1149
1156
  const ph = normalizePhone(p[2]);
1150
- const method = p[3] || 'sms';
1151
- if (!ph) { fail('usage: /reg code <phone> [sms|voice|wa_old]'); break; }
1157
+ const method = (p[3] || 'sms').toLowerCase();
1158
+ // email method: /reg code <phone> email <address>
1159
+ const emailAddr = method === 'email' ? (p[4] || '') : '';
1160
+ if (!ph) { fail('usage: /reg code <phone> [sms|voice|wa_old|email <address>]'); break; }
1161
+ if (method === 'email' && !emailAddr) {
1162
+ fail('email method requires an address — usage: /reg code <phone> email <address>');
1163
+ break;
1164
+ }
1152
1165
  if (!fs.existsSync(_sessDir)) fs.mkdirSync(_sessDir, { recursive: true });
1153
1166
  const sessFile = path.join(_sessDir, `${ph}.json`);
1154
1167
  let store = loadStore(sessFile);
1155
1168
  if (!store) {
1156
- store = createNewStore(ph);
1169
+ store = initAuthCreds(ph);
1157
1170
  saveStore(store, sessFile);
1158
1171
  } else if (!store.codePending && !store.registered) {
1159
1172
  // Only check /exist when keys were never used to request a code.
@@ -1162,13 +1175,15 @@ async function handleLine(line) {
1162
1175
  const fresh = await assertRegistrationKeys(store, waVersion);
1163
1176
  if (!fresh) {
1164
1177
  out(' device keys already registered — generating new keys...');
1165
- store = createNewStore(ph);
1178
+ store = initAuthCreds(ph);
1166
1179
  saveStore(store, sessFile);
1167
1180
  out(' new keys saved — proceed with code below');
1168
1181
  }
1169
1182
  }
1170
- out('requesting ' + method + ' code for +' + ph + '...');
1171
- const r = await requestSmsCode(store, method);
1183
+ const methodLabel = method === 'email' ? ('email ' + emailAddr) : method;
1184
+ out('requesting ' + methodLabel + ' code for +' + ph + '...');
1185
+ const codeOpts = method === 'email' ? { email: emailAddr } : {};
1186
+ const r = await requestSmsCode(store, method, codeOpts);
1172
1187
  store.codePending = true;
1173
1188
  saveStore(store, sessFile);
1174
1189
  out(' status ' + (r && r.status));
@@ -1180,7 +1195,7 @@ async function handleLine(line) {
1180
1195
  const code = p[3];
1181
1196
  if (!ph || !code) { fail('usage: /reg confirm <phone> <code>'); break; }
1182
1197
  const file = path.join(_sessDir, `${ph}.json`);
1183
- const store = loadStore(file) || createNewStore(ph);
1198
+ const store = loadStore(file) || initAuthCreds(ph);
1184
1199
  out('verifying...');
1185
1200
  const r = await verifyCode(store, code);
1186
1201
  if (r && (r.status === 'ok' || r.status === 'sent' || r.status === 'verified')) {
@@ -1421,7 +1436,8 @@ usage:
1421
1436
 
1422
1437
  options:
1423
1438
  --session <dir> session directory (default: ~/.waSession)
1424
- --method sms | voice | wa_old (default: sms)
1439
+ --method sms | voice | wa_old | email (default: sms)
1440
+ --email <address> email address (required when --method email)
1425
1441
 
1426
1442
  after connecting, type /help for all available commands.
1427
1443
  `.trim();
@@ -1469,15 +1485,20 @@ async function main() {
1469
1485
  }
1470
1486
 
1471
1487
  if (flags['request-code'] !== undefined) {
1472
- const ph = phone || normalizePhone(pos[0] || '');
1473
- const method = flags.method || 'sms';
1488
+ const ph = phone || normalizePhone(pos[0] || '');
1489
+ const method = (flags.method || 'sms').toLowerCase();
1490
+ const emailAddr = flags.email || '';
1474
1491
  if (!ph) { fail('phone number required'); process.exit(1); }
1492
+ if (method === 'email' && !emailAddr) {
1493
+ fail('--method email requires --email <address>');
1494
+ process.exit(1);
1495
+ }
1475
1496
  if (!fs.existsSync(_sessDir)) fs.mkdirSync(_sessDir, { recursive: true });
1476
1497
  const sessFile = path.join(_sessDir, `${ph}.json`);
1477
1498
  let store = loadStore(sessFile);
1478
1499
  if (!store) {
1479
1500
  // Brand new — generate fresh keys, save immediately, no need to check /exist
1480
- store = createNewStore(ph);
1501
+ store = initAuthCreds(ph);
1481
1502
  saveStore(store, sessFile);
1482
1503
  } else if (!store.codePending && !store.registered) {
1483
1504
  // Existing store but code was never sent and not registered — check if
@@ -1488,15 +1509,17 @@ async function main() {
1488
1509
  const fresh = await assertRegistrationKeys(store, waVersion);
1489
1510
  if (!fresh) {
1490
1511
  out(' device keys already registered — generating new keys...');
1491
- store = createNewStore(ph);
1512
+ store = initAuthCreds(ph);
1492
1513
  saveStore(store, sessFile);
1493
1514
  }
1494
1515
  }
1495
1516
  // If store.codePending === true, keys were already accepted by WhatsApp in a
1496
1517
  // prior /code request — reuse the exact same store without any /exist call.
1497
- out('requesting ' + method + ' code for +' + ph + '...');
1518
+ const methodLabel = method === 'email' ? ('email ' + emailAddr) : method;
1519
+ out('requesting ' + methodLabel + ' code for +' + ph + '...');
1498
1520
  try {
1499
- const r = await requestSmsCode(store, method);
1521
+ const codeOpts = method === 'email' ? { email: emailAddr } : {};
1522
+ const r = await requestSmsCode(store, method, codeOpts);
1500
1523
  store.codePending = true;
1501
1524
  saveStore(store, sessFile);
1502
1525
  out(' status ' + (r && r.status));
@@ -1518,7 +1541,7 @@ async function main() {
1518
1541
  if (!ph) { fail('phone number required'); process.exit(1); }
1519
1542
  if (!code) { fail('--code is required'); process.exit(1); }
1520
1543
  const file = path.join(_sessDir, `${ph}.json`);
1521
- const store = loadStore(file) || createNewStore(ph);
1544
+ const store = loadStore(file) || initAuthCreds(ph);
1522
1545
  out('verifying code for +' + ph + '...');
1523
1546
  try {
1524
1547
  const r = await verifyCode(store, code);
package/index.js CHANGED
@@ -11,7 +11,25 @@ const { SenderKeyStore, SenderKeyCrypto } = require('./lib/signal/SenderKey');
11
11
  const { DeviceManager } = require('./lib/DeviceManager');
12
12
  const { encryptMedia, decryptMedia, uploadMedia, downloadMedia } = require('./lib/MediaService');
13
13
  const { MessageSender, makeJid, generateMessageId } = require('./lib/messages/MessageSender');
14
- const { FridaAttestor, getAttestorFromEnv } = require('./lib/AttestationClient');
14
+ const {
15
+ makeCacheableSignalKeyStore,
16
+ addTransactionCapability,
17
+ assertMeId,
18
+ initAuthCreds
19
+ } = require('./lib/auth-utils');
20
+ const {
21
+ processHistorySyncNotification,
22
+ decodeHistorySyncNotification,
23
+ decodeHistorySync,
24
+ decodeAppStateSyncKeyShare,
25
+ saveAppStateSyncKeys,
26
+ loadAppStateSyncKeys,
27
+ mergeHistoryToStore,
28
+ decryptHistoryBlob,
29
+ deriveHistoryKeys,
30
+ HistorySyncType,
31
+ HistorySyncTypeName
32
+ } = require('./lib/HistorySyncHandler');
15
33
 
16
34
  module.exports = {
17
35
  WhalibmobClient,
@@ -50,9 +68,21 @@ module.exports = {
50
68
  MessageSender,
51
69
  makeJid,
52
70
  generateMessageId,
53
- // Device attestation (Frida middleware in ./frida) — see frida/README.md.
54
- // Enable with WA_FRIDA_ATTESTATION=1 to attach Play Integrity / App Attest
55
- // tokens to registration requests, matching a genuine device.
56
- FridaAttestor,
57
- getAttestorFromEnv
71
+ // Auth utilities (mirrors Baileys' auth-utils)
72
+ makeCacheableSignalKeyStore,
73
+ addTransactionCapability,
74
+ assertMeId,
75
+ initAuthCreds,
76
+ // History sync
77
+ processHistorySyncNotification,
78
+ decodeHistorySyncNotification,
79
+ decodeHistorySync,
80
+ decodeAppStateSyncKeyShare,
81
+ saveAppStateSyncKeys,
82
+ loadAppStateSyncKeys,
83
+ mergeHistoryToStore,
84
+ decryptHistoryBlob,
85
+ deriveHistoryKeys,
86
+ HistorySyncType,
87
+ HistorySyncTypeName
58
88
  };
@@ -0,0 +1,212 @@
1
+ 'use strict';
2
+
3
+ // ─── Device attestation client ────────────────────────────────────────────────
4
+ //
5
+ // Talks to the on-device Frida attestation server shipped in ./frida (Android
6
+ // Play Integrity / Keystore attestation, iOS App Attest) and folds its output
7
+ // into the registration request, mirroring how Auties00/cobalt consumes the
8
+ // same scripts.
9
+ //
10
+ // Design (matches Cobalt's LinkedWhatsAppClientDeviceAttestor):
11
+ // • Two data shapes are produced —
12
+ // Android: { gpia, gg, gi, gp, ge, ga } (Play Integrity legs)
13
+ // iOS: { attestation, assertion, keyId } (App Attest)
14
+ // • A NONE fallback returns empty strings for every field. The WhatsApp
15
+ // registration server tolerates empty attestation as a low-trust signal,
16
+ // so registration still works with no rooted/jailbroken device attached.
17
+ //
18
+ // Activation: set WA_FRIDA_HOST (e.g. 127.0.0.1) — and optionally WA_FRIDA_PORT
19
+ // (default 1119 for WhatsApp consumer, 1120 for Business) — to make the client
20
+ // query the local Frida server. When WA_FRIDA_HOST is unset every method
21
+ // returns the empty NONE fallback and performs no network I/O.
22
+
23
+ const crypto = require('crypto');
24
+ const http = require('http');
25
+
26
+ const DEFAULT_PORT = 1119; // WhatsApp consumer; Business build listens on 1120
27
+ const REQUEST_TIMEMS = 15000;
28
+
29
+ // ─── Empty (NONE) fallbacks ───────────────────────────────────────────────────
30
+
31
+ const EMPTY_ANDROID = { gpia: '', gg: '', gi: '', gp: '', ge: '', ga: '' };
32
+ const EMPTY_IOS = { attestation: '', assertion: '', keyId: '' };
33
+ const EMPTY_BODY = { bodyAttestation: null, authorizationHeader: null };
34
+
35
+ function fridaHost() {
36
+ return process.env.WA_FRIDA_HOST || null;
37
+ }
38
+
39
+ function fridaPort() {
40
+ return parseInt(process.env.WA_FRIDA_PORT, 10) || DEFAULT_PORT;
41
+ }
42
+
43
+ function isEnabled() {
44
+ return !!fridaHost();
45
+ }
46
+
47
+ // ─── base64 / hex helpers ─────────────────────────────────────────────────────
48
+
49
+ function toBase64Url(buf) {
50
+ return buf.toString('base64').replace(/\+/g, '-').replace(/\//g, '_');
51
+ }
52
+
53
+ function sha256(buf) {
54
+ return crypto.createHash('sha256').update(buf).digest();
55
+ }
56
+
57
+ function toHex(buf) {
58
+ return Buffer.from(buf).toString('hex');
59
+ }
60
+
61
+ // ─── Local Frida HTTP GET (best-effort; never throws) ─────────────────────────
62
+ // Returns the parsed JSON object on success, or null on any failure/timeout so
63
+ // the caller can fall back to the empty NONE attestation.
64
+
65
+ function fridaGet(pathname, query) {
66
+ return new Promise((resolve) => {
67
+ const host = fridaHost();
68
+ if (!host) return resolve(null);
69
+
70
+ const qs = query
71
+ ? '?' + Object.keys(query)
72
+ .filter(k => query[k] != null)
73
+ .map(k => k + '=' + encodeURIComponent(query[k]))
74
+ .join('&')
75
+ : '';
76
+
77
+ const req = http.request(
78
+ { host, port: fridaPort(), path: pathname + qs, method: 'GET', timeout: REQUEST_TIMEMS },
79
+ (res) => {
80
+ const chunks = [];
81
+ res.on('data', d => chunks.push(d));
82
+ res.on('end', () => {
83
+ try { resolve(JSON.parse(Buffer.concat(chunks).toString('utf8'))); }
84
+ catch (_) { resolve(null); }
85
+ });
86
+ }
87
+ );
88
+ req.on('error', () => resolve(null));
89
+ req.on('timeout', () => { req.destroy(); resolve(null); });
90
+ req.end();
91
+ });
92
+ }
93
+
94
+ // ─── Android: Play Integrity legs (gpia + _gg _gi _gp _ge _ga) ────────────────
95
+ //
96
+ // Queries /integrity?authKey=<nonce>. The Frida server returns { token }, which
97
+ // becomes the Play Integrity verdict token (gpia). The _gg/_gi/_gp/_ge/_ga GMS
98
+ // legs are only produced by the full native attestor, so a Frida-only setup
99
+ // leaves them empty — the server accepts a gpia-only attestation.
100
+ //
101
+ // nonceB64 is the URL-safe base64 request nonce (see attestationFieldsAndroid).
102
+ async function androidIntegrity(nonceB64) {
103
+ if (!isEnabled()) return { ...EMPTY_ANDROID };
104
+ const res = await fridaGet('/integrity', { authKey: nonceB64 });
105
+ if (!res || !res.token) return { ...EMPTY_ANDROID };
106
+ return { gpia: res.token, gg: '', gi: '', gp: '', ge: '', ga: '' };
107
+ }
108
+
109
+ // ─── Android: Keystore attestation over the ENC body (&H= + Authorization) ────
110
+ //
111
+ // Queries /cert?authKey=<authKeyB64>&enc=<encB64>. Both parameters are decoded
112
+ // with the STANDARD base64 decoder on the device, so they must not be URL-safe:
113
+ // authKey — the 32-byte raw auth key (the Noise public key, no 0x05 prefix);
114
+ // the server embeds it in the Keystore attestation challenge and
115
+ // locates it in the certificate by its 64-hex-char representation.
116
+ // enc — the UTF-8 bytes of the ENC payload string, base64-encoded; the
117
+ // server signs exactly those bytes.
118
+ // The server returns { signature, certificate }: signature → the &H= body
119
+ // suffix, certificate → the Authorization cert-chain header.
120
+ async function androidAttestBody(encB64, authKeyB64) {
121
+ if (!isEnabled()) return { ...EMPTY_BODY };
122
+ const encParam = Buffer.from(encB64, 'utf8').toString('base64');
123
+ const res = await fridaGet('/cert', { authKey: authKeyB64, enc: encParam });
124
+ if (!res || !res.signature || !res.certificate) return { ...EMPTY_BODY };
125
+ return { bodyAttestation: res.signature, authorizationHeader: res.certificate };
126
+ }
127
+
128
+ // ─── Android: device /info (apk hashes / signature / secret key) ──────────────
129
+ async function androidInfo() {
130
+ if (!isEnabled()) return null;
131
+ return fridaGet('/info');
132
+ }
133
+
134
+ // ─── iOS: App Attest (attestation + assertion + keyId) ────────────────────────
135
+ //
136
+ // Queries /integrity?authKey=<clientDataHashB64>. The Frida server returns
137
+ // { attestation, assertion }; the keyId is generated per call on-device and
138
+ // echoed by the server when available. clientDataHash is the base64 of the
139
+ // Noise public key (the server SHA-256-hashes it internally).
140
+ async function iosAppAttest(noisePubB64) {
141
+ if (!isEnabled()) return { ...EMPTY_IOS };
142
+ const res = await fridaGet('/integrity', { authKey: noisePubB64 });
143
+ if (!res || !res.attestation || !res.assertion) return { ...EMPTY_IOS };
144
+ return {
145
+ attestation: res.attestation,
146
+ assertion: res.assertion,
147
+ keyId: res.keyId || ''
148
+ };
149
+ }
150
+
151
+ // ─── High-level: attestation fields shipped on every attested endpoint ────────
152
+ //
153
+ // Returns the alternating name/value pairs Cobalt ships via attestationFields():
154
+ // Android → gpia,_gg,_gi,_gp,_ge,_ga,push_token
155
+ // iOS → push_token
156
+ //
157
+ // These fields are part of the plaintext that gets AES-GCM-sealed into the
158
+ // ENC envelope, so the Play Integrity nonce is derived from the store (a stable
159
+ // per-session value), NOT from the ENC body. pushToken comes from the optional
160
+ // push client — empty by default.
161
+ async function attestationFields(device, nonceB64, opts) {
162
+ opts = opts || {};
163
+ const pushToken = opts.pushToken || '';
164
+
165
+ if (device && device.os === 'android') {
166
+ const a = await androidIntegrity(nonceB64 || '');
167
+ return [
168
+ 'gpia', a.gpia,
169
+ '_gg', a.gg,
170
+ '_gi', a.gi,
171
+ '_gp', a.gp,
172
+ '_ge', a.ge,
173
+ '_ga', a.ga,
174
+ 'push_token', pushToken
175
+ ];
176
+ }
177
+
178
+ // iOS ships only push_token in the form body; App Attest rides in &H= + header.
179
+ return ['push_token', pushToken];
180
+ }
181
+
182
+ // ─── High-level: body attestation (&H= suffix + Authorization header) ─────────
183
+ //
184
+ // encB64 — the base64url ENC payload (without the "ENC=" prefix)
185
+ // device — { os }
186
+ // keys.noisePubB64 — standard base64 of the 32-byte Noise public key. Android
187
+ // uses it as the Keystore attestation challenge auth key;
188
+ // iOS uses it as the App Attest clientDataHash source.
189
+ async function attestBody(encB64, device, keys) {
190
+ keys = keys || {};
191
+ if (device && device.os === 'android') {
192
+ return androidAttestBody(encB64, keys.noisePubB64 || '');
193
+ }
194
+ const data = await iosAppAttest(keys.noisePubB64 || '');
195
+ if (!data.attestation || !data.assertion || !data.keyId) return { ...EMPTY_BODY };
196
+ return {
197
+ bodyAttestation: '{"assertion":"' + data.assertion + '"}',
198
+ authorizationHeader: data.attestation + '|' + data.keyId
199
+ };
200
+ }
201
+
202
+ module.exports = {
203
+ isEnabled,
204
+ attestationFields,
205
+ attestBody,
206
+ androidInfo,
207
+ toHex,
208
+ toBase64Url,
209
+ EMPTY_ANDROID,
210
+ EMPTY_IOS,
211
+ EMPTY_BODY
212
+ };