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 +41 -0
- package/lib/Attestation.js +15 -7
- package/lib/Client.js +28 -4
- package/lib/DeviceManager.js +9 -24
- package/lib/Registration.js +5 -2
- package/lib/Store.js +0 -8
- package/lib/messages/MessageSender.js +15 -0
- package/package.json +1 -1
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 (_) {}
|
package/lib/Attestation.js
CHANGED
|
@@ -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
|
|
881
|
-
|
|
882
|
-
|
|
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
|
)
|
package/lib/DeviceManager.js
CHANGED
|
@@ -305,30 +305,15 @@ class DeviceManager {
|
|
|
305
305
|
async fetchBundles(jids) {
|
|
306
306
|
if (!jids || jids.length === 0) return new Map();
|
|
307
307
|
|
|
308
|
-
//
|
|
309
|
-
//
|
|
310
|
-
//
|
|
311
|
-
//
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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' },
|
package/lib/Registration.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
}
|