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 +47 -7
- package/lib/Client.js +3 -1
- package/lib/DeviceManager.js +8 -6
- package/lib/Registration.js +59 -27
- package/lib/Store.js +1 -2
- package/lib/{messages/MessageSender.js → messages /MessageSender.js } +31 -53
- package/lib/signal/SenderKey.js +2 -6
- package/lib/signal/SignalProtocol.js +16 -9
- package/package.json +1 -1
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
|
-
|
|
786
|
-
|
|
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
|
-
|
|
922
|
-
|
|
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'
|
|
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,
|
package/lib/DeviceManager.js
CHANGED
|
@@ -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
|
-
|
|
240
|
-
|
|
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
|
}
|
package/lib/Registration.js
CHANGED
|
@@ -189,9 +189,9 @@ function computeToken(waVersion, national) {
|
|
|
189
189
|
|
|
190
190
|
// ---------- Byte helpers ----------
|
|
191
191
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
return
|
|
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',
|
|
244
|
-
'lc',
|
|
245
|
-
'authkey',
|
|
246
|
-
'e_regid', intToBytes(store.registrationId, 4)
|
|
247
|
-
'e_keytype', Buffer.from([SIGNAL_KEY_TYPE])
|
|
248
|
-
'e_ident',
|
|
249
|
-
'e_skey_id', intToBytes(store.signedPreKey.id, 3)
|
|
250
|
-
'e_skey_val',
|
|
251
|
-
'e_skey_sig', store.signedPreKey.signature
|
|
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
|
|
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);
|
|
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])
|
|
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
|
-
|
|
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',
|
|
440
|
-
'sim_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
|
-
|
|
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
|
-
|
|
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(
|
|
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()
|
|
130
|
-
const
|
|
131
|
-
|
|
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, '
|
|
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
|
-
|
|
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
|
-
|
|
391
|
-
|
|
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
|
-
|
|
384
|
+
const encAttrs = { v: '2', type };
|
|
385
|
+
if (mediaSubtype) encAttrs.mediatype = mediaSubtype;
|
|
386
|
+
msgContent = [...devIdNodes, new BinaryNode('enc', encAttrs, ciphertext)];
|
|
396
387
|
} else {
|
|
397
|
-
|
|
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
|
|
411
|
-
const ownJid
|
|
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
|
-
|
|
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
|
|
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, '
|
|
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, '
|
|
543
|
+
return this._sendMessage(toJid, msgId, msgBuf, 'protocol', opts);
|
|
566
544
|
}
|
|
567
545
|
|
|
568
546
|
// ─── Utility: mark read, presence, receipt, ping ─────────────────────────
|
package/lib/signal/SenderKey.js
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|