whalibmob 5.10.12 → 5.11.0

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 (2) hide show
  1. package/lib/Client.js +118 -18
  2. package/package.json +1 -1
package/lib/Client.js CHANGED
@@ -1968,17 +1968,31 @@ class WhalibmobClient extends EventEmitter {
1968
1968
  const id = String(attrs.id || '');
1969
1969
  const ts = parseInt(attrs.t || '0', 10);
1970
1970
  const participant = String(attrs.participant || from);
1971
- const senderPn = attrs.sender_pn ? String(attrs.sender_pn) : null;
1972
- if (senderPn) {
1973
- const lidUser = from.split('@')[0].split(':')[0];
1974
- const pnUser = senderPn.split('@')[0].split(':')[0];
1975
- if (lidUser && pnUser && lidUser !== pnUser) {
1976
- this._lidToPn.set(lidUser, pnUser);
1977
- this._pnToLid.set(pnUser, lidUser);
1978
- // Persist so the mapping survives reconnects / process restarts
1979
- if (this._signal && this._signal.store && this._signal.store.setLidMapping) {
1980
- this._signal.store.setLidMapping(pnUser, lidUser);
1981
- }
1971
+ const senderPn = attrs.sender_pn ? String(attrs.sender_pn) : null;
1972
+ const senderLid = attrs.sender_lid ? String(attrs.sender_lid) : null;
1973
+
1974
+ // Learn the LID ↔ phone pairing from whichever side the stanza names.
1975
+ //
1976
+ // Decryption needs it: a message addressed by LID has to be able to find a
1977
+ // session filed under the phone JID, and the other way round. The pairing
1978
+ // is only ever visible on a stanza that carries both names, so every such
1979
+ // stanza is worth reading — sender_pn against a LID `from`, and sender_lid
1980
+ // against a phone one.
1981
+ const fromUser = from.split('@')[0].split(':')[0];
1982
+ let learnedLid = null, learnedPn = null;
1983
+ if (senderPn && from.endsWith('@lid')) {
1984
+ learnedLid = fromUser;
1985
+ learnedPn = senderPn.split('@')[0].split(':')[0];
1986
+ } else if (senderLid && !from.endsWith('@lid')) {
1987
+ learnedPn = fromUser;
1988
+ learnedLid = senderLid.split('@')[0].split(':')[0];
1989
+ }
1990
+ if (learnedLid && learnedPn && learnedLid !== learnedPn) {
1991
+ this._lidToPn.set(learnedLid, learnedPn);
1992
+ this._pnToLid.set(learnedPn, learnedLid);
1993
+ // Persist so the mapping survives reconnects / process restarts
1994
+ if (this._signal && this._signal.store && this._signal.store.setLidMapping) {
1995
+ this._signal.store.setLidMapping(learnedPn, learnedLid);
1982
1996
  }
1983
1997
  }
1984
1998
  _whaDbg('[DBG] _handleMessage from=' + from + ' participant=' + participant + ' senderPn=' + senderPn + ' id=' + id);
@@ -2050,21 +2064,107 @@ class WhalibmobClient extends EventEmitter {
2050
2064
  this._sendReadReceipt(id, fromRaw, partRaw);
2051
2065
  }
2052
2066
 
2067
+ // The other name this contact answers to, with the device kept.
2068
+ //
2069
+ // A Signal address is user plus device, so the device has to survive the
2070
+ // translation — 250139683860650:5@lid and 40752248147:5@s.whatsapp.net are
2071
+ // the same device, 40752248147:0 is a different one.
2072
+ //
2073
+ // Returns null when there is no mapping for this user yet.
2074
+ _counterpartJid(jid) {
2075
+ if (!jid) return null;
2076
+ const str = String(jid);
2077
+ const user = str.split('@')[0].split(':')[0];
2078
+ if (!user) return null;
2079
+ const colon = str.split('@')[0].indexOf(':');
2080
+ const device = colon >= 0 ? str.split('@')[0].slice(colon) : '';
2081
+
2082
+ if (str.endsWith('@lid')) {
2083
+ const pn = this._lidToPn && this._lidToPn.get(user);
2084
+ return pn ? pn + device + '@s.whatsapp.net' : null;
2085
+ }
2086
+ const lid = this._pnToLid && this._pnToLid.get(user);
2087
+ return lid ? lid + device + '@lid' : null;
2088
+ }
2089
+
2090
+ // Every address this message's session might be filed under, best first.
2091
+ _signalJidCandidates(from, participant, senderPn) {
2092
+ const out = [];
2093
+ const add = (jid) => {
2094
+ if (!jid) return;
2095
+ const str = String(jid);
2096
+ if (str && !out.includes(str)) out.push(str);
2097
+ };
2098
+
2099
+ // Unchanged from before: the phone JID the stanza named, or the sender.
2100
+ add(senderPn || ((participant && participant !== from) ? participant : from));
2101
+ // Then the other name for each of them.
2102
+ add(this._counterpartJid(out[0]));
2103
+ add(participant && participant !== from ? participant : null);
2104
+ add(from);
2105
+ add(this._counterpartJid(participant && participant !== from ? participant : from));
2106
+
2107
+ return out;
2108
+ }
2109
+
2110
+ // Try each address in turn and return the first plaintext.
2111
+ //
2112
+ // A failed attempt leaves nothing behind — libsignal either returns the
2113
+ // plaintext or throws, and it only commits the advanced session on success —
2114
+ // so trying the second address after the first misses is safe. The error
2115
+ // reported on total failure is the first one, which is the one about the
2116
+ // address the message actually named.
2117
+ async _decryptWithCandidates(candidates, encType, cipherBuf, id) {
2118
+ let firstErr = null;
2119
+
2120
+ for (let i = 0; i < candidates.length; i++) {
2121
+ try {
2122
+ const plaintext = await this._signal.decrypt(candidates[i], encType, cipherBuf);
2123
+ if (i > 0) {
2124
+ _whaDbg('[DBG] DM_DECRYPT id=' + id + ' recovered under ' + candidates[i] +
2125
+ ' after ' + candidates[0] + ' had no session');
2126
+ }
2127
+ return plaintext;
2128
+ } catch (err) {
2129
+ if (!firstErr) firstErr = err;
2130
+ // Only a missing session is worth asking a different address about.
2131
+ // A session that exists and refuses the ciphertext is a different
2132
+ // problem, and the answer is a retry receipt, not another lookup.
2133
+ if (!/no matching sessions|no session/i.test(String(err && err.message))) break;
2134
+ }
2135
+ }
2136
+
2137
+ throw firstErr || new Error('decrypt: no address to try');
2138
+ }
2139
+
2053
2140
  _decryptDMMessage({ node, from, participant, fromRaw, partRaw, senderPn, id, ts, dmEncNode }) {
2054
2141
  const encType = dmEncNode.attrs && dmEncNode.attrs.type;
2055
2142
  const cipherBuf = Buffer.isBuffer(dmEncNode.content)
2056
2143
  ? dmEncNode.content
2057
2144
  : Buffer.from(dmEncNode.content || '');
2058
2145
 
2059
- // WhatsApp LID migration: `from` may be a LID (linked identity) instead of a real phone number
2060
- // not the phone-based JID. The Signal session is stored under the phone-based JID.
2061
- // Use sender_pn (phone JID) when available, otherwise fall back to participant/from.
2062
- const sigJid = senderPn
2063
- || ((participant && participant !== from) ? participant : from);
2146
+ // The same contact is reachable under two names — a phone JID and a LID —
2147
+ // and a Signal address keeps only the user and the device, never the
2148
+ // domain. So 250139683860650:5@lid and 40752248147:5@s.whatsapp.net are
2149
+ // two different addresses for one person, and a session filed under either
2150
+ // one is invisible from the other.
2151
+ //
2152
+ // Which name a message arrives under is the server's choice and it varies
2153
+ // between messages from the same device. When the session was built under
2154
+ // the phone JID and the next message comes addressed by LID, the lookup
2155
+ // finds nothing and decryption fails with "No matching sessions found for
2156
+ // message" — while the very next message from the same contact decrypts.
2157
+ //
2158
+ // The stanza's own name is tried first, so nothing that already works
2159
+ // changes. The counterpart follows, from the LID↔phone mapping we keep.
2160
+ const candidates = this._signalJidCandidates(from, participant, senderPn);
2161
+ const sigJid = candidates[0];
2064
2162
 
2065
- _whaDbg('[DBG] DM_DECRYPT id=' + id + ' type=' + encType + ' sigJid=' + sigJid + ' cipherLen=' + cipherBuf.length);
2163
+ _whaDbg('[DBG] DM_DECRYPT id=' + id + ' type=' + encType + ' sigJid=' + sigJid +
2164
+ (candidates.length > 1 ? ' alts=' + candidates.slice(1).join(',') : '') +
2165
+ ' cipherLen=' + cipherBuf.length);
2066
2166
 
2067
- this._signal.decrypt(sigJid, encType, cipherBuf)
2167
+ this._decryptWithCandidates(candidates, encType, cipherBuf, id)
2068
2168
  .then(async plaintext => {
2069
2169
  this._resolveRetry(id);
2070
2170
  const decoded = this._decodeMsg(plaintext);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whalibmob",
3
- "version": "5.10.12",
3
+ "version": "5.11.0",
4
4
  "description": "WhatsApp library for interaction with WhatsApp Mobile API no web",
5
5
  "author": "Kunboruto20",
6
6
  "main": "index.js",