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 +57 -1
- package/lib/proto/MessageProto.js +11 -7
- package/lib/signal/SignalProtocol.js +45 -18
- package/package.json +1 -1
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
|
|
14
|
+
let val = Math.floor(n);
|
|
13
15
|
while (val > 127) {
|
|
14
|
-
bytes.push((val
|
|
15
|
-
val = val
|
|
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(
|
|
105
|
-
field(
|
|
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
|
-
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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 ────────────────────────────────────
|