whalibmob 5.5.68 → 5.5.71

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/cli.js CHANGED
@@ -174,6 +174,47 @@ function enableWireTrace() {
174
174
  }
175
175
  }
176
176
 
177
+ // The Noise handshake never goes through sendNode — sendNode refuses to run
178
+ // until the channel is secured, and the ClientHello/ClientFinish frames are
179
+ // written straight to the TCP socket. Wrap that socket as soon as connect()
180
+ // creates it so the handshake is visible too, then step aside once the
181
+ // channel is up and the stanza-level hooks take over.
182
+ const HS_OUT = ['ClientHello', 'ClientFinish'];
183
+ const HS_IN = ['ServerHello'];
184
+ const origConnect = NoiseSocket.prototype.connect;
185
+ NoiseSocket.prototype.connect = function (...args) {
186
+ const result = origConnect.apply(this, args);
187
+ const sock = this.socket;
188
+ if (sock && !sock.__waTraced) {
189
+ sock.__waTraced = true;
190
+ let outN = 0, inN = 0;
191
+ trace(`\n${C.out}${stamp()} ──▶ TCP CONNECT ${sock.remoteAddress || ''}${C.off}`);
192
+
193
+ const origWrite = sock.write.bind(sock);
194
+ sock.write = (chunk, ...rest) => {
195
+ if (!this.secured && Buffer.isBuffer(chunk)) {
196
+ const name = HS_OUT[outN++] || 'handshake frame';
197
+ trace(`\n${C.out}${stamp()} ──▶ NOISE ${name} (${chunk.length} bytes)${C.off}`);
198
+ trace(` ${C.dim}${chunk.toString('hex')}${C.off}`);
199
+ }
200
+ return origWrite(chunk, ...rest);
201
+ };
202
+
203
+ sock.on('data', (d) => {
204
+ if (this.secured) return;
205
+ // Label positionally, but say so when the reply is plainly not a Noise
206
+ // frame — a proxy or captive portal answering in ASCII is worth reading
207
+ // as text rather than staring at its hex.
208
+ const ascii = isPrintable(d);
209
+ const name = ascii ? 'unexpected plaintext reply' : (HS_IN[inN++] || 'handshake frame');
210
+ trace(`\n${C.in}${stamp()} ◀── NOISE ${name} (${d.length} bytes)${C.off}`);
211
+ trace(` ${C.dim}${d.toString('hex').slice(0, 1024)}${C.off}`);
212
+ if (ascii) trace(` ${C.dim}as text: ${JSON.stringify(d.toString('utf8').slice(0, 400))}${C.off}`);
213
+ });
214
+ }
215
+ return result;
216
+ };
217
+
177
218
  const origSendNode = NoiseSocket.prototype.sendNode;
178
219
  NoiseSocket.prototype.sendNode = function (node) {
179
220
  try { logStanza('OUT', node); } catch (_) {}
@@ -158,19 +158,27 @@ async function iosAppAttest(noisePubB64) {
158
158
  // ENC envelope, so the Play Integrity nonce is derived from the store (a stable
159
159
  // per-session value), NOT from the ENC body. pushToken comes from the optional
160
160
  // push client — empty by default.
161
+ // A field with no value is omitted from the body rather than sent empty.
162
+ // buildForm drops null and keeps '', and the registration server validates the
163
+ // shape of what it receives: an empty push_code is a malformed push_code, and
164
+ // it answers with bad_param/bad_format naming that field. An absent field has
165
+ // nothing to validate, which is what an unattested client should send.
166
+ // Real values are still sent whenever a Frida attestation server supplies them.
167
+ const orNull = (v) => (v === '' || v === undefined ? null : v);
168
+
161
169
  async function attestationFields(device, nonceB64, opts) {
162
170
  opts = opts || {};
163
- const pushToken = opts.pushToken || '';
171
+ const pushToken = orNull(opts.pushToken);
164
172
 
165
173
  if (device && device.os === 'android') {
166
174
  const a = await androidIntegrity(nonceB64 || '');
167
175
  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,
176
+ 'gpia', orNull(a.gpia),
177
+ '_gg', orNull(a.gg),
178
+ '_gi', orNull(a.gi),
179
+ '_gp', orNull(a.gp),
180
+ '_ge', orNull(a.ge),
181
+ '_ga', orNull(a.ga),
174
182
  'push_token', pushToken
175
183
  ];
176
184
  }
package/lib/Client.js CHANGED
@@ -877,9 +877,16 @@ class WhalibmobClient extends EventEmitter {
877
877
  return;
878
878
  }
879
879
 
880
- const fromStr = attrs.from ? String(attrs.from) : '';
881
- // Extract bare phone number: "40756469325@s.whatsapp.net" → "40756469325"
882
- const recipientPhone = fromStr.split('@')[0].split('.')[0];
880
+ const fromStr = attrs.from ? String(attrs.from) : '';
881
+ const isGroup = fromStr.endsWith('@g.us');
882
+
883
+ // On a group retry `from` is the group and `participant` is the device that
884
+ // failed to decrypt. Keying off `from` there would have targeted the group
885
+ // id as if it were a contact and cleared nothing.
886
+ const targetJid = isGroup && attrs.participant
887
+ ? String(attrs.participant)
888
+ : fromStr;
889
+ const recipientPhone = targetJid.split('@')[0].split(':')[0].split('.')[0];
883
890
 
884
891
  _whaDbg('[DBG] RETRY_RECV msgId=' + msgId + ' from=' + fromStr + ' — clearing session + resending\n');
885
892
 
@@ -900,7 +907,24 @@ class WhalibmobClient extends EventEmitter {
900
907
  this._devMgr.clearCache([recipientPhone]);
901
908
  }
902
909
 
903
- // Re-send the message — will create fresh pre-key (pkmsg) sessions
910
+ // Re-send the message — will create fresh pre-key (pkmsg) sessions.
911
+ // A group resend must go back through the group path: it needs a fresh
912
+ // SenderKey distribution, which only _sendGroupMessage builds. Drop the
913
+ // "already distributed" record first, or the resend would carry the same
914
+ // bare skmsg the recipient just told us it could not read.
915
+ const resendId = generateMessageId();
916
+ if (cached.isGroup) {
917
+ if (this._signal && this._signal.senderKeyStore) {
918
+ this._signal.senderKeyStore.invalidateSKDM(cached.toJid);
919
+ }
920
+ this._sender._sendGroupMessage(
921
+ cached.toJid, resendId, cached.plaintext, cached.mediaType, cached.options
922
+ )
923
+ .then(r => _whaDbg('[DBG] RETRY_RESEND group ok id=' + (r && r.id) + '\n'))
924
+ .catch(e => _whaDbg('[DBG] RETRY_RESEND_ERR: ' + e.message));
925
+ return;
926
+ }
927
+
904
928
  this._sender._sendDMMessage(
905
929
  cached.toJid, cached.msgId, cached.plaintext, cached.mediaType, cached.options
906
930
  )
@@ -305,30 +305,15 @@ class DeviceManager {
305
305
  async fetchBundles(jids) {
306
306
  if (!jids || jids.length === 0) return new Map();
307
307
 
308
- // Convert a string JID like "user@server" or "user:device@server" into
309
- // a proper JID object so the binary encoder uses JID_PAIR / AD_JID format.
310
- // Raw UTF-8 strings are NOT recognised by the WA server for @lid JIDs — the
311
- // server silently ignores unknown JIDs and returns an empty bundle list.
312
- const toJidObj = jidStr => {
313
- const str = typeof jidStr === 'string' ? jidStr : String(jidStr);
314
- const at = str.indexOf('@');
315
- const raw = at >= 0 ? str.slice(0, at) : str;
316
- const server = at >= 0 ? str.slice(at + 1) : 's.whatsapp.net';
317
- const colon = raw.indexOf(':');
318
- const user = colon >= 0 ? raw.slice(0, colon) : raw;
319
- const device = colon >= 0 ? (parseInt(raw.slice(colon + 1), 10) || 0) : 0;
320
- const self = str;
321
- // AD_JID binary format is for @s.whatsapp.net with device > 0.
322
- // JID_PAIR binary format is used for @lid and for device-0 @s.whatsapp.net.
323
- if (server === 's.whatsapp.net' && device > 0) {
324
- // AD_JID: agent=0, device=N
325
- return { user, agent: 0, device, server, toString() { return self; } };
326
- }
327
- // JID_PAIR: user@server — server may be 'lid' or 's.whatsapp.net'
328
- return { user, server, toString() { return self; } };
329
- };
330
-
331
- const userNodes = jids.map(jid => new BinaryNode('user', { jid: toJidObj(jid) }, null));
308
+ // Use the shared converter. The local copy this replaced dropped the device
309
+ // suffix on @lid JIDs: it split "112713111982325:49@lid" into user and
310
+ // device, then fell through to a JID_PAIR built from `user` alone, so every
311
+ // device of a contact encoded to the identical "112713111982325@lid". The
312
+ // server saw one repeated JID, returned a single device-0 bundle, and no
313
+ // session was ever built for the other devices — leaving them unable to
314
+ // decrypt anything we sent. jidStrToObj keeps the colon inside the user
315
+ // part for @lid, which is what the server expects.
316
+ const userNodes = jids.map(jid => new BinaryNode('user', { jid: jidStrToObj(jid) }, null));
332
317
  const iqId = this._client._genMsgId();
333
318
  const iqNode = new BinaryNode('iq',
334
319
  { id: iqId, xmlns: 'encrypt', type: 'get', to: 's.whatsapp.net' },
@@ -424,7 +424,9 @@ function getRequestVerificationCodeParameters(store, method, meta, device, attem
424
424
  'manage_call_permission', 'false',
425
425
  'clicked_education_link', 'false',
426
426
  'aid', '',
427
- 'push_code', store.pushCode || ''
427
+ // Omitted unless a push client supplies a real code — an empty value is
428
+ // rejected as bad_format, an absent field is not validated at all.
429
+ 'push_code', null
428
430
  ];
429
431
  }
430
432
 
@@ -434,7 +436,8 @@ function getRequestVerificationCodeParameters(store, method, meta, device, attem
434
436
  'sim_mcc', meta.mcc,
435
437
  'sim_mnc', meta.mnc,
436
438
  'jailbroken', '0',
437
- 'push_code', store.pushCode || '',
439
+ // Omitted unless a push client supplies a real code (see above).
440
+ 'push_code', null,
438
441
  'cellular_strength', '1'
439
442
  ];
440
443
  }
package/lib/Store.js CHANGED
@@ -64,11 +64,6 @@ function createNewStore(phoneNumber) {
64
64
  // Registration.js): advertising id (UUID) and the 20-byte backup token.
65
65
  const advertisingId = uuidv4();
66
66
  const backupToken = crypto.randomBytes(20);
67
- // Push-notification token sent as `push_code` on /code. The server validates
68
- // its shape and rejects an empty value with bad_param/bad_format, so it has
69
- // to be a real 32-hex-character token. Generated once and persisted, because
70
- // the value must stay the same across the /code and /register pair.
71
- const pushCode = crypto.randomBytes(16).toString('hex');
72
67
 
73
68
  const device = getDeviceConfig();
74
69
  const version = device.os === 'android' ? ANDROID_VERSION_FALLBACK : IOS_VERSION_FALLBACK;
@@ -91,7 +86,6 @@ function createNewStore(phoneNumber) {
91
86
  identityId,
92
87
  advertisingId,
93
88
  backupToken,
94
- pushCode,
95
89
  registered: false,
96
90
  codePending: false, // true after /code request, cleared on successful /register
97
91
  name: 'User',
@@ -146,7 +140,6 @@ function storeToJson(store) {
146
140
  identityId: store.identityId.toString('base64'),
147
141
  advertisingId: store.advertisingId || null,
148
142
  backupToken: store.backupToken ? store.backupToken.toString('base64') : null,
149
- pushCode: store.pushCode || null,
150
143
  registered: !!store.registered,
151
144
  codePending: store.codePending || false,
152
145
  name,
@@ -189,7 +182,6 @@ function storeFromJson(obj) {
189
182
  // enriched /code request always has stable values.
190
183
  advertisingId: obj.advertisingId || uuidv4(),
191
184
  backupToken: obj.backupToken ? Buffer.from(obj.backupToken, 'base64') : crypto.randomBytes(20),
192
- pushCode: obj.pushCode || crypto.randomBytes(16).toString('hex'),
193
185
  registered: !!obj.registered,
194
186
  codePending: obj.codePending || false,
195
187
  name,
@@ -973,6 +973,21 @@ class MessageSender {
973
973
  };
974
974
  if (options.edit) stanzaAttrs.edit = String(options.edit);
975
975
 
976
+ // Cache the plaintext here too, not just on the DM path. A group recipient
977
+ // that cannot decrypt sends a retry receipt exactly like a DM recipient
978
+ // does, and without this the client had nothing to re-send and dropped it
979
+ // with "no cached plaintext" — so the message was acked by the server and
980
+ // then silently never arrived.
981
+ if (this._client._sentMsgCache) {
982
+ this._client._sentMsgCache.set(msgId, {
983
+ plaintext, toJid: groupJid, msgId, mediaType, options, isGroup: true
984
+ });
985
+ if (this._client._sentMsgCache.size > 200) {
986
+ const firstKey = this._client._sentMsgCache.keys().next().value;
987
+ this._client._sentMsgCache.delete(firstKey);
988
+ }
989
+ }
990
+
976
991
  const msgNode = new BinaryNode('message', stanzaAttrs, msgContent);
977
992
  return this._dispatchAndAck(msgNode, msgId);
978
993
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whalibmob",
3
- "version": "5.5.68",
3
+ "version": "5.5.71",
4
4
  "description": "WhatsApp library for interaction with WhatsApp Mobile API no web",
5
5
  "author": "Kunboruto50",
6
6
  "main": "index.js",