whalibmob 2.1.1 → 3.0.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.
package/lib/noise.js CHANGED
@@ -10,6 +10,61 @@ const { MOBILE_PROLOGUE, WHATSAPP_HOST, WHATSAPP_PORT } = require('./constants')
10
10
  const { encodeHandshakeClientHello, encodeHandshakeClientFinish,
11
11
  decodeServerHello, encodeClientPayload } = require('./proto');
12
12
 
13
+ // ─── CertChain validator (matches Baileys WA_CERT_DETAILS.SERIAL = 0) ────────
14
+ //
15
+ // Server payload inside ServerHello is a CertChain protobuf:
16
+ // CertChain.field2 (intermediate) → NoiseCertificate
17
+ // NoiseCertificate.field1 (details) → NoiseCertificate.Details
18
+ // Details.field2 (issuerSerial) must === 0
19
+ //
20
+ // All decoded manually to avoid a protobufjs dependency in noise.js.
21
+
22
+ function _pbReadVarint(buf, off) {
23
+ let v = 0, shift = 0;
24
+ while (off.pos < buf.length) {
25
+ const b = buf[off.pos++];
26
+ v |= (b & 0x7f) << shift;
27
+ if (!(b & 0x80)) break;
28
+ shift += 7;
29
+ }
30
+ return v;
31
+ }
32
+
33
+ function _pbReadFields(buf) {
34
+ const fields = {};
35
+ const off = { pos: 0 };
36
+ while (off.pos < buf.length) {
37
+ const tag = _pbReadVarint(buf, off);
38
+ const fn = tag >>> 3;
39
+ const wt = tag & 7;
40
+ if (wt === 0) {
41
+ fields[fn] = _pbReadVarint(buf, off);
42
+ } else if (wt === 2) {
43
+ const len = _pbReadVarint(buf, off);
44
+ fields[fn] = buf.slice(off.pos, off.pos + len);
45
+ off.pos += len;
46
+ } else {
47
+ break; // unexpected wire type — stop parsing
48
+ }
49
+ }
50
+ return fields;
51
+ }
52
+
53
+ function validateServerCert(payloadBuf) {
54
+ // payloadBuf is the decrypted ServerHello.payload (CertChain protobuf)
55
+ const chain = _pbReadFields(payloadBuf);
56
+ const intermediate = chain[2]; // field 2 = intermediate NoiseCertificate
57
+ if (!Buffer.isBuffer(intermediate)) return; // no intermediate cert — skip
58
+ const cert = _pbReadFields(intermediate);
59
+ const details = cert[1]; // field 1 = details bytes
60
+ if (!Buffer.isBuffer(details)) return;
61
+ const det = _pbReadFields(details);
62
+ const issuerSerial = det[2]; // field 2 = issuerSerial (uint32)
63
+ if (issuerSerial !== undefined && issuerSerial !== 0) {
64
+ throw new Error('WA server cert validation failed: issuerSerial=' + issuerSerial + ' expected 0');
65
+ }
66
+ }
67
+
13
68
  // ─── Curve25519 DH helpers ───────────────────────────────────────────────────
14
69
 
15
70
  function stripKeyPrefix(pub) {
@@ -216,7 +271,8 @@ class NoiseSocket extends EventEmitter {
216
271
  this.noiseState.mixKey(sharedSE);
217
272
 
218
273
  if (serverHello.payload) {
219
- this.noiseState.decryptWithAd(serverHello.payload);
274
+ const certPlain = this.noiseState.decryptWithAd(serverHello.payload);
275
+ validateServerCert(certPlain);
220
276
  }
221
277
 
222
278
  const noisePriv = this.store.noiseKeyPair.private;
@@ -8,11 +8,13 @@ function tag(fieldNumber, wireType) {
8
8
  }
9
9
 
10
10
  function encodeVarint(n) {
11
+ // Use Math.floor/% instead of bitwise ops to handle values > 2^31 safely
12
+ // (JavaScript bitwise ops coerce to Int32, breaking timestamps in ms, large file sizes etc.)
11
13
  const bytes = [];
12
- let val = n >>> 0;
14
+ let val = Math.floor(n);
13
15
  while (val > 127) {
14
- bytes.push((val & 0x7f) | 0x80);
15
- val = val >>> 7;
16
+ bytes.push((val % 128) | 0x80);
17
+ val = Math.floor(val / 128);
16
18
  }
17
19
  bytes.push(val);
18
20
  return Buffer.from(bytes);
@@ -101,23 +103,25 @@ function encodeVideoMessage(opts) {
101
103
  field(9, WIRE_VARINT, varint(opts.height || 0)),
102
104
  field(10, WIRE_VARINT, varint(opts.width || 0)),
103
105
  field(11, WIRE_LEN, bytes(opts.fileEncSha256)),
104
- field(12, WIRE_LEN, str(opts.directPath)),
105
- field(13, WIRE_VARINT, varintS(opts.mediaKeyTimestamp || Math.floor(Date.now() / 1000)))
106
+ field(13, WIRE_LEN, str(opts.directPath)),
107
+ field(14, WIRE_VARINT, varintS(opts.mediaKeyTimestamp || Math.floor(Date.now() / 1000)))
106
108
  ];
107
- if (opts.jpegThumbnail) parts.push(field(16, WIRE_LEN, bytes(opts.jpegThumbnail)));
108
109
  if (opts.gifPlayback) parts.push(field(8, WIRE_VARINT, bool(true)));
110
+ if (opts.jpegThumbnail) parts.push(field(16, WIRE_LEN, bytes(opts.jpegThumbnail)));
109
111
  if (opts.contextInfo) parts.push(field(17, WIRE_LEN, encodeContextInfo(opts.contextInfo)));
110
112
  return Buffer.concat(parts);
111
113
  }
112
114
 
113
115
  function encodeAudioMessage(opts) {
116
+ // ptt (field 6) goes between seconds (5) and mediaKey (7) — must be in field-number order
117
+ const pttField = opts.ptt ? field(6, WIRE_VARINT, bool(true)) : Buffer.alloc(0);
114
118
  const parts = [
115
119
  field(1, WIRE_LEN, str(opts.url)),
116
120
  field(2, WIRE_LEN, str(opts.mimetype || 'audio/ogg; codecs=opus')),
117
121
  field(3, WIRE_LEN, bytes(opts.fileSha256)),
118
122
  field(4, WIRE_VARINT, varint(opts.fileLength)),
119
123
  field(5, WIRE_VARINT, varint(opts.seconds || 0)),
120
- field(6, WIRE_VARINT, bool(opts.ptt || false)),
124
+ pttField,
121
125
  field(7, WIRE_LEN, bytes(opts.mediaKey)),
122
126
  field(8, WIRE_LEN, bytes(opts.fileEncSha256)),
123
127
  field(9, WIRE_LEN, str(opts.directPath)),
@@ -11,6 +11,31 @@ const PRE_KEY_COUNT = 100;
11
11
  const PRE_KEY_MIN = 10;
12
12
  const PRE_KEY_START = 1;
13
13
 
14
+ // ─── Per-JID async mutex (matches Baileys encryptionMutex pattern) ────────────
15
+ // Prevents race conditions when two messages are encrypted simultaneously
16
+ // for the same Signal session (e.g. rapid sends or parallel fanout calls).
17
+
18
+ const _mutexQueues = new Map();
19
+
20
+ async function _withMutex(key, fn) {
21
+ // If no queue exists for this key, run immediately
22
+ if (!_mutexQueues.has(key)) {
23
+ _mutexQueues.set(key, Promise.resolve());
24
+ }
25
+ const prev = _mutexQueues.get(key);
26
+ let releasePrev;
27
+ const next = new Promise(r => { releasePrev = r; });
28
+ _mutexQueues.set(key, next);
29
+ await prev;
30
+ try {
31
+ return await fn();
32
+ } finally {
33
+ releasePrev();
34
+ // Cleanup: if no more waiters, remove from map
35
+ if (_mutexQueues.get(key) === next) _mutexQueues.delete(key);
36
+ }
37
+ }
38
+
14
39
  // ─── Padding ──────────────────────────────────────────────────────────────────
15
40
 
16
41
  function randomPadding() {
@@ -159,14 +184,17 @@ class SignalProtocol {
159
184
  // ─── 1-to-1 encrypt / decrypt ─────────────────────────────────────────────
160
185
 
161
186
  async encrypt(jid, plaintext) {
162
- const addr = jidToAddress(jid);
163
- const cipher = new SessionCipher(this.store, addr);
164
- const padded = pad(Buffer.from(plaintext));
165
- const result = await cipher.encrypt(padded);
166
- return {
167
- type: result.type === 3 ? 'pkmsg' : 'msg',
168
- ciphertext: Buffer.from(result.body, 'binary')
169
- };
187
+ // Mutex per JID — prevents Signal session race conditions (matches Baileys encryptionMutex)
188
+ return _withMutex(jid, async () => {
189
+ const addr = jidToAddress(jid);
190
+ const cipher = new SessionCipher(this.store, addr);
191
+ const padded = pad(Buffer.from(plaintext));
192
+ const result = await cipher.encrypt(padded);
193
+ return {
194
+ type: result.type === 3 ? 'pkmsg' : 'msg',
195
+ ciphertext: Buffer.from(result.body, 'binary')
196
+ };
197
+ });
170
198
  }
171
199
 
172
200
  async decrypt(jid, type, ciphertext) {
@@ -182,18 +210,17 @@ class SignalProtocol {
182
210
  }
183
211
 
184
212
  // ─── Bulk encrypt for multiple devices (MD fanout) ─────────────────────────
213
+ // All devices encrypted in parallel (like Baileys Promise.all).
214
+ // Each JID is protected by its own mutex so parallel calls on the
215
+ // same JID are still serialized — no Signal session corruption possible.
185
216
  // Returns [{jid, type, ciphertext}]
186
217
  async bulkEncryptForDevices(jids, plaintext) {
187
- const results = [];
188
- for (const jid of jids) {
189
- try {
190
- const enc = await this.encrypt(jid, plaintext);
191
- results.push({ jid, type: enc.type, ciphertext: enc.ciphertext });
192
- } catch (err) {
193
- // Skip devices with no session or errors
194
- }
195
- }
196
- return results;
218
+ const settled = await Promise.allSettled(
219
+ jids.map(jid => this.encrypt(jid, plaintext).then(enc => ({ jid, type: enc.type, ciphertext: enc.ciphertext })))
220
+ );
221
+ return settled
222
+ .filter(r => r.status === 'fulfilled')
223
+ .map(r => r.value);
197
224
  }
198
225
 
199
226
  // ─── SenderKey group encrypt / decrypt ────────────────────────────────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whalibmob",
3
- "version": "2.1.1",
3
+ "version": "3.0.0",
4
4
  "description": "WhatsApp mobile-only Node.js client library with Signal Protocol encryption",
5
5
  "main": "index.js",
6
6
  "bin": {