whalibmob 5.0.0 → 5.0.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/cli.js CHANGED
@@ -11,6 +11,8 @@ const {
11
11
  checkNumberStatus,
12
12
  requestSmsCode,
13
13
  verifyCode,
14
+ assertRegistrationKeys,
15
+ fetchIosVersion,
14
16
  createNewStore,
15
17
  saveStore,
16
18
  loadStore
@@ -782,11 +784,32 @@ async function handleLine(line) {
782
784
  const method = p[3] || 'sms';
783
785
  if (!ph) { fail('usage: /reg code <phone> [sms|voice|wa_old]'); break; }
784
786
  if (!fs.existsSync(_sessDir)) fs.mkdirSync(_sessDir, { recursive: true });
785
- let store = loadStore(path.join(_sessDir, `${ph}.json`));
786
- if (!store) { store = createNewStore(ph); saveStore(store, path.join(_sessDir, `${ph}.json`)); }
787
+ const sessFile = path.join(_sessDir, `${ph}.json`);
788
+ let store = loadStore(sessFile);
789
+ if (!store) {
790
+ store = createNewStore(ph);
791
+ saveStore(store, sessFile);
792
+ } else {
793
+ // Check if these device keys are already registered in WhatsApp.
794
+ // If they are (not "incorrect"), generate fresh keys so the new
795
+ // code request and confirm use matching fresh keys — mirrors Cobalt's
796
+ // assertRegistrationKeys() behaviour.
797
+ out('checking device keys...');
798
+ const waVersion = await fetchIosVersion();
799
+ const fresh = await assertRegistrationKeys(store, waVersion);
800
+ if (!fresh) {
801
+ out(' device keys already registered — generating new keys...');
802
+ store = createNewStore(ph);
803
+ saveStore(store, sessFile);
804
+ out(' new keys saved — proceed with code below');
805
+ }
806
+ }
787
807
  out('requesting ' + method + ' code for +' + ph + '...');
788
808
  const r = await requestSmsCode(store, method);
809
+ // Always save the store after a code request to persist any state
810
+ saveStore(store, sessFile);
789
811
  out(' status ' + (r && r.status));
812
+ out(' important: enter the code within 10 minutes');
790
813
  out(' now run: /reg confirm ' + ph + ' <code>');
791
814
  }
792
815
  else if (sub === 'confirm') {
@@ -797,7 +820,7 @@ async function handleLine(line) {
797
820
  const store = loadStore(file) || createNewStore(ph);
798
821
  out('verifying...');
799
822
  const r = await verifyCode(store, code);
800
- if (r && (r.status === 'ok' || r.status === 'verified')) {
823
+ if (r && (r.status === 'ok' || r.status === 'sent' || r.status === 'verified')) {
801
824
  if (!fs.existsSync(_sessDir)) fs.mkdirSync(_sessDir, { recursive: true });
802
825
  saveStore(r.store || store, file);
803
826
  out('registered session saved to ' + file);
@@ -918,13 +941,30 @@ async function main() {
918
941
  const method = flags.method || 'sms';
919
942
  if (!ph) { fail('phone number required'); process.exit(1); }
920
943
  if (!fs.existsSync(_sessDir)) fs.mkdirSync(_sessDir, { recursive: true });
921
- let store = loadStore(path.join(_sessDir, `${ph}.json`));
922
- if (!store) { store = createNewStore(ph); saveStore(store, path.join(_sessDir, `${ph}.json`)); }
944
+ const sessFile = path.join(_sessDir, `${ph}.json`);
945
+ let store = loadStore(sessFile);
946
+ if (!store) {
947
+ store = createNewStore(ph);
948
+ saveStore(store, sessFile);
949
+ } else {
950
+ out('checking device keys...');
951
+ const waVersion = await fetchIosVersion();
952
+ const fresh = await assertRegistrationKeys(store, waVersion);
953
+ if (!fresh) {
954
+ out(' device keys already registered — generating new keys...');
955
+ store = createNewStore(ph);
956
+ saveStore(store, sessFile);
957
+ }
958
+ }
923
959
  out('requesting ' + method + ' code for +' + ph + '...');
924
960
  try {
925
961
  const r = await requestSmsCode(store, method);
962
+ saveStore(store, sessFile);
926
963
  out(' status ' + (r && r.status));
927
- if (r && r.status === 'sent') out(' code sent run: wa registration --register ' + ph + ' --code <code>');
964
+ if (r && (r.status === 'sent' || r.status === 'ok')) {
965
+ out(' important: enter the code within 10 minutes');
966
+ out(' run: wa registration --register ' + ph + ' --code <code>');
967
+ }
928
968
  } catch (e) {
929
969
  if (e.message && e.message.includes('too_recent')) out(' code already sent recently — check your phone');
930
970
  else fail(e.message);
@@ -944,7 +984,7 @@ async function main() {
944
984
  out('verifying code for +' + ph + '...');
945
985
  try {
946
986
  const r = await verifyCode(store, code);
947
- if (r && (r.status === 'ok' || r.status === 'verified')) {
987
+ if (r && (r.status === 'ok' || r.status === 'sent' || r.status === 'verified')) {
948
988
  if (!fs.existsSync(_sessDir)) fs.mkdirSync(_sessDir, { recursive: true });
949
989
  saveStore(r.store || store, file);
950
990
  out('registered session saved to ' + file);
package/lib/Client.js CHANGED
@@ -5,7 +5,7 @@ const path = require('path');
5
5
  const crypto = require('crypto');
6
6
  const { NoiseSocket } = require('./noise');
7
7
  const { MessageSender, generateMessageId, makeJid } = require('./messages/MessageSender');
8
- const { checkIfRegistered, checkNumberStatus, requestSmsCode, verifyCode } = require('./Registration');
8
+ const { checkIfRegistered, checkNumberStatus, requestSmsCode, verifyCode, assertRegistrationKeys, fetchIosVersion } = require('./Registration');
9
9
  const { createNewStore, saveStore, loadStore, toSixParts, fromSixParts } = require('./Store');
10
10
  const { BinaryNode } = require('./BinaryNode');
11
11
  const { SignalProtocol } = require('./signal/SignalProtocol');
@@ -1680,6 +1680,8 @@ module.exports = {
1680
1680
  checkNumberStatus,
1681
1681
  requestSmsCode,
1682
1682
  verifyCode,
1683
+ assertRegistrationKeys,
1684
+ fetchIosVersion,
1683
1685
  createNewStore,
1684
1686
  saveStore,
1685
1687
  loadStore,
@@ -234,12 +234,14 @@ class DeviceManager {
234
234
  }
235
235
 
236
236
  // ─── Build participants node ───────────────────────────────────────────────
237
- static buildParticipantsNode(encryptedList) {
238
- const toNodes = encryptedList.map(({ jid, type, ciphertext }) =>
239
- new BinaryNode('to', { jid },
240
- [new BinaryNode('enc', { type, v: '2' }, ciphertext)]
241
- )
242
- );
237
+ static buildParticipantsNode(encryptedList, mediaSubtype) {
238
+ const toNodes = encryptedList.map(({ jid, type, ciphertext }) => {
239
+ const encAttrs = { type, v: '2' };
240
+ if (mediaSubtype) encAttrs.mediatype = mediaSubtype;
241
+ return new BinaryNode('to', { jid },
242
+ [new BinaryNode('enc', encAttrs, ciphertext)]
243
+ );
244
+ });
243
245
  return new BinaryNode('participants', {}, toNodes);
244
246
  }
245
247
  }
@@ -189,9 +189,9 @@ function computeToken(waVersion, national) {
189
189
 
190
190
  // ---------- Byte helpers ----------
191
191
 
192
- // Strip 0x05 key-type prefix if present (32-byte raw X25519 key)
193
- function rawKey(buf) {
194
- return (buf.length === 33 && buf[0] === 0x05) ? buf.slice(1) : buf;
192
+ function stripKeyPrefix(buf) {
193
+ if (buf.length === 33 && buf[0] === 0x05) return buf.slice(1);
194
+ return buf;
195
195
  }
196
196
 
197
197
  function intToBytes(n, len) {
@@ -230,9 +230,13 @@ function buildForm(pairs, extraPairs) {
230
230
 
231
231
  // ---------- Payload ----------
232
232
 
233
+ // base64url WITH padding — matches Java's Base64.getUrlEncoder().encodeToString()
234
+ function toBase64Url(buf) {
235
+ return buf.toString('base64').replace(/\+/g, '-').replace(/\//g, '_');
236
+ }
237
+
233
238
  function buildPayload(store, waVersion, useToken, extraPairs) {
234
239
  const { cc, national } = parsePhone(store.phoneNumber);
235
- const meta = getCountryMeta(cc);
236
240
  const token = useToken ? computeToken(waVersion, national) : null;
237
241
  const fdid = store.fdid.toUpperCase();
238
242
 
@@ -240,17 +244,17 @@ function buildPayload(store, waVersion, useToken, extraPairs) {
240
244
  'cc', cc,
241
245
  'in', national,
242
246
  'rc', String(RELEASE_CHANNEL),
243
- 'lg', meta.lg,
244
- 'lc', meta.lc,
245
- 'authkey', rawKey(store.noiseKeyPair.public).toString('base64url'),
246
- 'e_regid', intToBytes(store.registrationId, 4).toString('base64url'),
247
- 'e_keytype', Buffer.from([SIGNAL_KEY_TYPE]).toString('base64url'),
248
- 'e_ident', rawKey(store.identityKeyPair.public).toString('base64url'),
249
- 'e_skey_id', intToBytes(store.signedPreKey.id, 3).toString('base64url'),
250
- 'e_skey_val', rawKey(store.signedPreKey.public).toString('base64url'),
251
- 'e_skey_sig', store.signedPreKey.signature.toString('base64url'),
247
+ 'lg', 'en',
248
+ 'lc', 'US',
249
+ 'authkey', toBase64Url(stripKeyPrefix(store.noiseKeyPair.public)),
250
+ 'e_regid', toBase64Url(intToBytes(store.registrationId, 4)),
251
+ 'e_keytype', toBase64Url(Buffer.from([SIGNAL_KEY_TYPE])),
252
+ 'e_ident', toBase64Url(stripKeyPrefix(store.identityKeyPair.public)),
253
+ 'e_skey_id', toBase64Url(intToBytes(store.signedPreKey.id, 3)),
254
+ 'e_skey_val', toBase64Url(stripKeyPrefix(store.signedPreKey.public)),
255
+ 'e_skey_sig', toBase64Url(store.signedPreKey.signature),
252
256
  'fdid', fdid,
253
- 'expid', store.deviceId.toString('base64url'),
257
+ 'expid', toBase64Url(store.deviceId),
254
258
  'id', toUrlHex(store.identityId),
255
259
  'token', token
256
260
  ], extraPairs);
@@ -261,14 +265,14 @@ function buildPayload(store, waVersion, useToken, extraPairs) {
261
265
  function encryptPayload(plaintext) {
262
266
  const seed = crypto.randomBytes(32);
263
267
  const ephKp = curveJs.generateKeyPair(seed);
264
- const ephemeralPub = Buffer.from(ephKp.public); // raw 32-byte X25519 key (no prefix)
268
+ const ephemeralPub = Buffer.from(ephKp.public);
265
269
  const sharedKey = Buffer.from(curveJs.sharedKey(ephKp.private, REGISTRATION_PUBLIC_KEY));
266
270
 
267
271
  const iv = Buffer.alloc(12);
268
272
  const cipher = crypto.createCipheriv('aes-256-gcm', sharedKey, iv);
269
273
  const enc = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
270
274
  const tag = cipher.getAuthTag();
271
- return Buffer.concat([ephemeralPub, enc, tag]).toString('base64url');
275
+ return toBase64Url(Buffer.concat([ephemeralPub, enc, tag]));
272
276
  }
273
277
 
274
278
  // ---------- HTTP ----------
@@ -283,8 +287,7 @@ function httpPost(path, body, waVersion) {
283
287
  method: 'POST',
284
288
  headers: {
285
289
  'User-Agent': userAgent,
286
- 'Content-Type': 'application/x-www-form-urlencoded',
287
- 'Accept': 'text/json'
290
+ 'Content-Type': 'application/x-www-form-urlencoded'
288
291
  }
289
292
  };
290
293
  const req = https.request(opts, (res) => {
@@ -423,21 +426,35 @@ async function checkNumberStatus(phoneNumber) {
423
426
  note: 'Could not determine status. Possible datacenter IP restriction. Try a residential proxy.' };
424
427
  }
425
428
 
429
+ // ---------- assertRegistrationKeys (mirrors Cobalt) ----------
430
+ // Calls /exist to ensure these device keys are NOT already registered.
431
+ // Returns true if keys are fresh (reason='incorrect'), false otherwise.
432
+ async function assertRegistrationKeys(store, waVersion) {
433
+ for (let attempt = 0; attempt < 2; attempt++) {
434
+ try {
435
+ const result = await sendRequest('/exist', store, waVersion, false, null);
436
+ if (result && result.reason === 'incorrect') return true;
437
+ } catch (_) {
438
+ return true;
439
+ }
440
+ }
441
+ return false;
442
+ }
443
+
426
444
  async function requestSmsCode(store, method) {
427
445
  method = method || 'sms';
428
446
  const waVersion = await fetchIosVersion();
447
+ store.version = waVersion;
429
448
 
430
- const { cc } = parsePhone(store.phoneNumber);
431
- const meta = getCountryMeta(cc);
432
-
449
+ // iOS-specific: sim_mcc and sim_mnc are always '000'/'000' (matches Cobalt)
433
450
  const methods = method === 'wa_old' ? ['wa_old'] : [method, 'wa_old'];
434
451
  let lastResult = null;
435
452
 
436
453
  for (const m of methods) {
437
454
  const extra = [
438
455
  'method', m,
439
- 'sim_mcc', meta.mcc,
440
- 'sim_mnc', meta.mnc,
456
+ 'sim_mcc', '000',
457
+ 'sim_mnc', '000',
441
458
  'reason', '',
442
459
  'cellular_strength', '1'
443
460
  ];
@@ -464,12 +481,27 @@ async function requestSmsCode(store, method) {
464
481
 
465
482
  async function verifyCode(store, code) {
466
483
  const waVersion = await fetchIosVersion();
467
- const normalized = code.replace(/\D/g, '');
484
+ store.version = waVersion;
485
+ const normalized = code.replace(/[\s\-]/g, '').replace(/\D/g, '');
468
486
  const result = await sendRequest('/register', store, waVersion, true, ['code', normalized]);
469
487
 
470
488
  const status = result.status;
471
- if (status === 'ok' || status === 'verified') return result;
472
- throw new Error(`Verification failed: ${result.reason || JSON.stringify(result)}`);
489
+ if (status === 'ok' || status === 'sent' || status === 'verified') return result;
490
+
491
+ const reason = result.reason || '';
492
+ if (reason === 'missing') {
493
+ throw new Error(
494
+ 'Verification failed: code expired or already used.\n' +
495
+ ' Run /reg code <phone> again to get a new code, then immediately confirm it.'
496
+ );
497
+ }
498
+ if (/bad_code|code_invalid|wrong/.test(reason)) {
499
+ throw new Error('Verification failed: wrong code entered. Check the SMS and try again.');
500
+ }
501
+ if (/too_many/.test(reason)) {
502
+ throw new Error('Verification failed: too many wrong attempts. Wait a few minutes then request a new code.');
503
+ }
504
+ throw new Error(`Verification failed: ${reason || JSON.stringify(result)}`);
473
505
  }
474
506
 
475
- module.exports = { checkIfRegistered, checkNumberStatus, requestSmsCode, verifyCode, fetchIosVersion, parsePhone };
507
+ module.exports = { checkIfRegistered, checkNumberStatus, requestSmsCode, verifyCode, fetchIosVersion, parsePhone, assertRegistrationKeys };
package/lib/Store.js CHANGED
@@ -51,13 +51,12 @@ function createNewStore(phoneNumber) {
51
51
  const signedPreKeyPair = generateKeyPair();
52
52
 
53
53
  const signedPreKeyId = (crypto.randomBytes(3).readUIntBE(0, 3) & 0xffffff) || 1;
54
- // Sign the 33-byte public key (with 0x05 prefix) using identity private key
55
54
  const signature = sign(identityKeyPair.private, signedPreKeyPair.public);
56
55
 
57
56
  const registrationId = (crypto.randomBytes(2).readUInt16BE(0) & 0x3fff) + 1;
58
57
  const fdid = uuidv4();
59
58
  const deviceId = crypto.randomBytes(16);
60
- const identityId = crypto.randomBytes(20);
59
+ const identityId = crypto.randomBytes(16);
61
60
 
62
61
  return {
63
62
  phoneNumber: String(phoneNumber).replace(/^\+/, ''),
@@ -123,12 +123,12 @@ function getContent(node) {
123
123
  return null;
124
124
  }
125
125
 
126
- // phash: SHA-256 of sorted JIDs joined, first 6 base64 chars, prefixed "2:"
127
- // Matches Baileys generateParticipantHashV2 — server uses it to validate participant list.
128
126
  function computePhash(jids) {
129
- const sorted = [...jids].sort().join('');
130
- const hash = crypto.createHash('sha256').update(sorted).digest('base64');
131
- return '2:' + hash.slice(0, 6);
127
+ const sorted = [...jids].sort();
128
+ const digest = crypto.createHash('sha256');
129
+ for (const jid of sorted) digest.update(jid, 'utf8');
130
+ const hash = digest.digest();
131
+ return '2:' + hash.slice(0, 6).toString('base64');
132
132
  }
133
133
 
134
134
  // ─── MessageSender ────────────────────────────────────────────────────────────
@@ -184,7 +184,7 @@ class MessageSender {
184
184
  jpegThumbnail: options.thumbnail || null,
185
185
  contextInfo: options.contextInfo
186
186
  }));
187
- return this._sendMessage(toJid, msgId, imgBuf, 'media', options);
187
+ return this._sendMessage(toJid, msgId, imgBuf, 'media', { ...options, _mediaSubtype: 'image' });
188
188
  }
189
189
 
190
190
  // ─── Video ────────────────────────────────────────────────────────────────
@@ -211,7 +211,7 @@ class MessageSender {
211
211
  gifPlayback: options.gifPlayback || false,
212
212
  contextInfo: options.contextInfo
213
213
  }));
214
- return this._sendMessage(toJid, msgId, vidBuf, 'media', options);
214
+ return this._sendMessage(toJid, msgId, vidBuf, 'media', { ...options, _mediaSubtype: 'video' });
215
215
  }
216
216
 
217
217
  // ─── Audio / PTT ──────────────────────────────────────────────────────────
@@ -235,7 +235,7 @@ class MessageSender {
235
235
  mediaKeyTimestamp: upload.mediaKeyTimestamp,
236
236
  contextInfo: options.contextInfo
237
237
  }));
238
- return this._sendMessage(toJid, msgId, audBuf, 'media', options);
238
+ return this._sendMessage(toJid, msgId, audBuf, 'media', { ...options, _mediaSubtype: isPtt ? 'ptt' : 'audio' });
239
239
  }
240
240
 
241
241
  // ─── Document ─────────────────────────────────────────────────────────────
@@ -259,7 +259,7 @@ class MessageSender {
259
259
  jpegThumbnail: options.thumbnail || null,
260
260
  contextInfo: options.contextInfo
261
261
  }));
262
- return this._sendMessage(toJid, msgId, docBuf, 'media', options);
262
+ return this._sendMessage(toJid, msgId, docBuf, 'media', { ...options, _mediaSubtype: 'document' });
263
263
  }
264
264
 
265
265
  // ─── Sticker ──────────────────────────────────────────────────────────────
@@ -283,7 +283,7 @@ class MessageSender {
283
283
  isAnimated: options.isAnimated || false,
284
284
  contextInfo: options.contextInfo
285
285
  }));
286
- return this._sendMessage(toJid, msgId, stkBuf, 'media', options);
286
+ return this._sendMessage(toJid, msgId, stkBuf, 'media', { ...options, _mediaSubtype: 'sticker' });
287
287
  }
288
288
 
289
289
  // ─── Reaction ─────────────────────────────────────────────────────────────
@@ -297,7 +297,7 @@ class MessageSender {
297
297
  text: emoji,
298
298
  senderTimestampMs: Date.now()
299
299
  }));
300
- return this._sendMessage(toJid, msgId, rxBuf, 'text', options);
300
+ return this._sendMessage(toJid, msgId, rxBuf, 'reaction', options);
301
301
  }
302
302
 
303
303
  // ─── Core dispatch ────────────────────────────────────────────────────────
@@ -339,30 +339,23 @@ class MessageSender {
339
339
  const recipientPhone = phoneFromJid(toJid);
340
340
  const ownPhone = String(this._store.phoneNumber);
341
341
  const ownMainJid = `${ownPhone}@s.whatsapp.net`;
342
+ const mediaSubtype = options._mediaSubtype || null;
342
343
 
343
- // Fetch device lists in parallel
344
344
  const [recipientDevices, ownDevices] = await Promise.all([
345
345
  this._devMgr.bulkEnsureSessions([recipientPhone], this._signal),
346
346
  this._devMgr.ensureOwnDeviceSessions(ownPhone, this._signal)
347
347
  ]);
348
348
 
349
- // Recipient devices fall back to main JID if usync returned nothing
350
- const otherJids = recipientDevices.length > 0 ? recipientDevices : [toJid];
351
-
352
- // Own linked devices (exclude primary — it is the sender)
349
+ const otherJids = recipientDevices.length > 0 ? recipientDevices : [toJid];
353
350
  const ownLinkedJids = ownDevices.filter(j => j !== ownMainJid);
354
351
 
355
- // phash — computed over all participant JIDs when multi-device
356
352
  const allParticipants = [...otherJids, ...ownLinkedJids];
357
353
  const phash = allParticipants.length > 1 ? computePhash(allParticipants) : null;
358
354
 
359
- // deviceSentMessage wrapper for own linked devices:
360
- // tells linked phones/tablets "this message was sent to toJid"
361
355
  const dsmBuf = ownLinkedJids.length > 0
362
356
  ? encodeDeviceSentMessage(toJid, plaintext, phash)
363
357
  : null;
364
358
 
365
- // Encrypt in parallel: recipients get original, own linked get DSM wrapper
366
359
  const [otherEncrypted, ownEncrypted] = await Promise.all([
367
360
  this._signal.bulkEncryptForDevices(otherJids, plaintext),
368
361
  ownLinkedJids.length > 0
@@ -376,10 +369,6 @@ class MessageSender {
376
369
  if (phash) stanzaAttrs.phash = phash;
377
370
  if (options.edit) stanzaAttrs.edit = String(options.edit);
378
371
 
379
- // ── Feature 2: device_identity for pkmsg ─────────────────────────────────
380
- // When any enc is type=pkmsg (new Signal session with a device), the server
381
- // requires a <device-identity> node carrying our ADVSignedDeviceIdentity
382
- // protobuf (field 2 / accountSignatureKey stripped for privacy).
383
372
  const hasPkmsg = encryptedList.some(e => e.type === 'pkmsg');
384
373
  const advRaw = this._client._store && this._client._store.advIdentity;
385
374
  const advBytes = hasPkmsg && advRaw ? stripAdvSignatureKey(advRaw) : null;
@@ -387,15 +376,16 @@ class MessageSender {
387
376
 
388
377
  let msgContent;
389
378
  if (encryptedList.length === 0) {
390
- // Fallback: unencrypted (should never happen in prod)
391
- msgContent = [...devIdNodes, new BinaryNode('enc', { v: '2', type: 'msg' }, plaintext)];
379
+ const encAttrs = { v: '2', type: 'msg' };
380
+ if (mediaSubtype) encAttrs.mediatype = mediaSubtype;
381
+ msgContent = [...devIdNodes, new BinaryNode('enc', encAttrs, plaintext)];
392
382
  } else if (encryptedList.length === 1 && !phash) {
393
- // Single device, no multi-device — simple enc node (backward compat)
394
383
  const { type, ciphertext } = encryptedList[0];
395
- msgContent = [...devIdNodes, new BinaryNode('enc', { v: '2', type }, ciphertext)];
384
+ const encAttrs = { v: '2', type };
385
+ if (mediaSubtype) encAttrs.mediatype = mediaSubtype;
386
+ msgContent = [...devIdNodes, new BinaryNode('enc', encAttrs, ciphertext)];
396
387
  } else {
397
- // Multi-device participants node with per-device enc
398
- const participantsNode = DeviceManager.buildParticipantsNode(encryptedList);
388
+ const participantsNode = DeviceManager.buildParticipantsNode(encryptedList, mediaSubtype);
399
389
  msgContent = [...devIdNodes, participantsNode];
400
390
  }
401
391
 
@@ -407,21 +397,17 @@ class MessageSender {
407
397
 
408
398
  async _sendGroupMessage(groupJid, msgId, plaintext, mediaType, options) {
409
399
  options = options || {};
410
- const ownPhone = String(this._store.phoneNumber);
411
- const ownJid = `${ownPhone}@s.whatsapp.net`;
400
+ const ownPhone = String(this._store.phoneNumber);
401
+ const ownJid = `${ownPhone}@s.whatsapp.net`;
402
+ const mediaSubtype = options._mediaSubtype || null;
412
403
 
413
- // Group members from cache
414
404
  const members = this._client._getGroupMembers(groupJid);
415
-
416
- // Build SKDM for this group (creates/loads SenderKey state)
417
405
  const skdmBytes = this._signal.buildSKDM(groupJid, ownJid);
418
406
 
419
- // Collect unique member phones (excluding self)
420
407
  const memberPhones = [...new Set(
421
408
  members.map(phoneFromJid).filter(p => p !== ownPhone)
422
409
  )];
423
410
 
424
- // Ensure sessions for all member devices + own linked devices in parallel
425
411
  const [memberDevices, ownDevices] = await Promise.all([
426
412
  memberPhones.length > 0
427
413
  ? this._devMgr.bulkEnsureSessions(memberPhones, this._signal)
@@ -431,15 +417,10 @@ class MessageSender {
431
417
 
432
418
  const allTargets = [...memberDevices, ...ownDevices];
433
419
 
434
- // phash MUST include the sender's own primary device.
435
- // Cobalt's calculateGroupPhash() explicitly adds senderDevice to the set.
436
- // ownDevices contains only linked devices (device != 0); ownJid is device 0.
437
420
  const phashTargets = allTargets.includes(ownJid)
438
421
  ? allTargets
439
422
  : [ownJid, ...allTargets];
440
423
 
441
- // senderKeyMap: only send SKDM to devices that haven't received it yet.
442
- // Persisted in .sk.json so we don't re-send on every group message.
443
424
  const skStore = this._signal.senderKeyStore;
444
425
  const skdmMap = skStore.getSKDMMap(groupJid);
445
426
  const skdmRecipients = allTargets.filter(jid => !skdmMap[jid]);
@@ -450,15 +431,9 @@ class MessageSender {
450
431
  skStore.markSKDMSent(groupJid, skdmRecipients);
451
432
  }
452
433
 
453
- // SenderKey encrypt the actual group message (one ciphertext for all)
454
434
  const skmsgCiphertext = this._signal.senderKeyEncrypt(groupJid, ownJid, plaintext);
455
-
456
- // phash over all group member devices + sender primary device
457
435
  const phash = phashTargets.length > 0 ? computePhash(phashTargets) : null;
458
436
 
459
- // ── Feature 2: device_identity for pkmsg in SKDM ─────────────────────────
460
- // SKDM messages sent to devices with no prior Signal session are pkmsg.
461
- // Include our ADVSignedDeviceIdentity (field 2 stripped) in the stanza.
462
437
  const skdmHasPkmsg = skdmEncrypted.some(e => e.type === 'pkmsg');
463
438
  const advRaw = this._client._store && this._client._store.advIdentity;
464
439
  const advBytes = skdmHasPkmsg && advRaw ? stripAdvSignatureKey(advRaw) : null;
@@ -468,14 +443,17 @@ class MessageSender {
468
443
  msgContent.push(new BinaryNode('device-identity', {}, advBytes));
469
444
  }
470
445
  if (skdmEncrypted.length > 0) {
471
- msgContent.push(DeviceManager.buildParticipantsNode(skdmEncrypted));
446
+ msgContent.push(DeviceManager.buildParticipantsNode(skdmEncrypted, null));
472
447
  }
473
- msgContent.push(new BinaryNode('enc', { type: 'skmsg', v: '2' }, skmsgCiphertext));
448
+ const skmsgAttrs = { type: 'skmsg', v: '2' };
449
+ if (mediaSubtype) skmsgAttrs.mediatype = mediaSubtype;
450
+ msgContent.push(new BinaryNode('enc', skmsgAttrs, skmsgCiphertext));
474
451
 
475
452
  const stanzaAttrs = {
476
453
  to: groupJid,
477
454
  id: msgId,
478
- type: mediaType === 'media' ? 'media' : 'text',
455
+ type: mediaType,
456
+ addressing_mode: 'pn',
479
457
  t: String(msNow())
480
458
  };
481
459
  if (phash) stanzaAttrs.phash = phash;
@@ -547,7 +525,7 @@ class MessageSender {
547
525
  key: { remoteJid: toJid, fromMe: !!fromMe, id: origMsgId }
548
526
  });
549
527
  const msgBuf = encodeMessage('protocol', revokePayload);
550
- return this._sendMessage(toJid, msgId, msgBuf, 'text', { ...opts, edit: editBit });
528
+ return this._sendMessage(toJid, msgId, msgBuf, 'protocol', { ...opts, edit: editBit });
551
529
  }
552
530
 
553
531
  // ─── Ephemeral timer DM ───────────────────────────────────────────────────
@@ -562,7 +540,7 @@ class MessageSender {
562
540
  ephemeralExpiration: seconds
563
541
  });
564
542
  const msgBuf = encodeMessage('protocol', payload);
565
- return this._sendMessage(toJid, msgId, msgBuf, 'text', opts);
543
+ return this._sendMessage(toJid, msgId, msgBuf, 'protocol', opts);
566
544
  }
567
545
 
568
546
  // ─── Utility: mark read, presence, receipt, ping ─────────────────────────
@@ -189,10 +189,8 @@ class SenderKeyCrypto {
189
189
  const messageKey = this._getMessageKey(state);
190
190
  const { iv, cipherKey, macKey } = deriveMessageKeys(messageKey);
191
191
 
192
- const padLen = 16 - (plaintext.length % 16);
193
- const padded = Buffer.concat([plaintext, Buffer.alloc(padLen, padLen)]);
194
192
  const cipher = crypto.createCipheriv('aes-256-cbc', cipherKey, iv);
195
- const encrypted = Buffer.concat([cipher.update(padded), cipher.final()]);
193
+ const encrypted = Buffer.concat([cipher.update(plaintext), cipher.final()]);
196
194
 
197
195
  const protoBody = Buffer.concat([
198
196
  fieldUint32(1, state.keyId),
@@ -230,9 +228,7 @@ class SenderKeyCrypto {
230
228
  if (!crypto.timingSafeEqual(expectedMac, mac8)) throw new Error('SenderKeyMessage MAC invalid');
231
229
 
232
230
  const decipher = crypto.createDecipheriv('aes-256-cbc', cipherKey, iv);
233
- const decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]);
234
- const padLen = decrypted[decrypted.length - 1];
235
- return decrypted.slice(0, decrypted.length - padLen);
231
+ return Buffer.concat([decipher.update(encrypted), decipher.final()]);
236
232
  }
237
233
 
238
234
  static _getMessageKeyAtIteration(state, iteration) {
@@ -36,20 +36,27 @@ async function _withMutex(key, fn) {
36
36
  }
37
37
  }
38
38
 
39
- // ─── Padding ──────────────────────────────────────────────────────────────────
40
-
41
- function randomPadding() {
42
- const padLen = (crypto.randomBytes(1)[0] & 0x0f) + 1;
43
- const pad = Buffer.alloc(padLen, 0);
44
- pad[0] = 0x80;
45
- return pad;
46
- }
39
+ const BLOCK_SIZE = 16;
47
40
 
48
41
  function pad(plaintext) {
49
- return Buffer.concat([Buffer.from(plaintext), randomPadding()]);
42
+ const buf = Buffer.from(plaintext);
43
+ const paddingLength = BLOCK_SIZE - (buf.length % BLOCK_SIZE);
44
+ const padded = Buffer.alloc(buf.length + paddingLength);
45
+ buf.copy(padded);
46
+ padded.fill(paddingLength, buf.length);
47
+ return padded;
50
48
  }
51
49
 
52
50
  function unpad(data) {
51
+ if (!data || data.length === 0) return data;
52
+ const last = data[data.length - 1];
53
+ if (last > 0 && last <= BLOCK_SIZE) {
54
+ let valid = true;
55
+ for (let i = data.length - last; i < data.length; i++) {
56
+ if (data[i] !== last) { valid = false; break; }
57
+ }
58
+ if (valid) return data.slice(0, data.length - last);
59
+ }
53
60
  for (let i = data.length - 1; i >= 0; i--) {
54
61
  if (data[i] === 0x80) return data.slice(0, i);
55
62
  if (data[i] !== 0x00) break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whalibmob",
3
- "version": "5.0.0",
3
+ "version": "5.0.1",
4
4
  "description": "WhatsApp library for interaction with WhatsApp Mobile API no web",
5
5
  "main": "index.js",
6
6
  "bin": {