whalibmob 2.1.0 → 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/README.md CHANGED
@@ -1,354 +1,263 @@
1
1
  # whalibmob
2
2
 
3
- A pure JavaScript Node.js library and CLI for WhatsApp. Operates as a
4
- mobile iOS client with full Signal Protocol end-to-end encryption, SenderKey
5
- group messaging, multi-device fanout, persistent sessions, and automatic
6
- reconnection.
3
+ > Pure JavaScript Node.js WhatsApp client library and CLI. Operates as a real mobile iOS device with full **Signal Protocol** end-to-end encryption, SenderKey group messaging, multi-device fanout, persistent sessions, and automatic reconnection. No native binaries — runs anywhere Node.js runs.
7
4
 
8
5
  ---
9
6
 
10
- > **⚠️ IMPORTANT You need a brand-new phone number and a brand-new SIM card
11
- > dedicated to WhatsApp for this library to work for you.** Using an existing
12
- > WhatsApp account that is already active on a real phone will cause
13
- > WhatsApp to ban or revoke the session. Register a fresh number that has
14
- > never been used with WhatsApp before.
7
+ > **⚠️ IMPORTANT:** Use a **brand-new phone number** dedicated exclusively to this library. Using an existing WhatsApp account already active on a real phone will cause WhatsApp to revoke the session. Register a fresh number that has never been used with WhatsApp before.
15
8
 
16
9
  ---
17
10
 
18
- ## Features
11
+ ## Requirements
19
12
 
20
- - **Mobile iOS fingerprint** authentic iOS device headers; behaves like
21
- WhatsApp on an iPhone
22
- - **Signal Protocol E2E encryption** — X3DH session setup, Double Ratchet
23
- for 1-to-1 messages
24
- - **SenderKey group encryption** — efficient group messaging with chain-key
25
- advance and SKDM distribution
26
- - **Multi-device fanout** — messages delivered to all linked devices of each
27
- recipient via usync device query
28
- - **All media types** — images, video, audio, voice notes (PTT), documents,
29
- stickers; AES-256-CBC + HKDF-SHA256 encryption, uploaded to WhatsApp CDN
30
- - **Text, reactions, extended text** — full message type coverage
31
- - **Participant hash (phash)** — SHA-256 participant-list validation sent
32
- with every multi-device and group message
33
- - **deviceSentMessage** — own linked devices (tablet, secondary phone) receive
34
- a proper copy-sent wrapper so they show the message as sent
35
- - **senderKeyMap** — SKDM is sent to each device only once; subsequent group
36
- messages skip SKDM entirely for devices that already have the key
37
- - **Automatic reconnection** — exponential backoff (1 s → 2 → 4 → 8 → 15 → 30 s)
38
- - **Pre-key replenishment** — automatic upload when supply drops below 20
39
- - **Persistent sessions** — JSON files on disk; no re-registration after restart
40
- - **Auth confirmation** — waits for server `<success>` before declaring the
41
- connection open; handles `<failure>` (session revoked / account banned)
42
- - **Professional `wa` CLI** — yowsup-style interface for registration, sending,
43
- listening, and interactive chat
13
+ - **Node.js 18** or higher
14
+ - A phone number that can receive SMS or voice calls
44
15
 
45
16
  ---
46
17
 
47
- ## Installation
18
+ ## Global Installation
19
+
20
+ Install once, use the `wa` command from anywhere on your system:
48
21
 
49
22
  ```bash
50
- cd tools/whalibmob
51
- npm install
52
- npm link # makes `wa` available globally
23
+ npm install -g whalibmob
53
24
  ```
54
25
 
55
- Requires **Node.js 18** or higher.
26
+ Verify it works:
27
+
28
+ ```bash
29
+ wa version
30
+ ```
56
31
 
57
32
  ---
58
33
 
59
- ## CLI Usage
34
+ ## Quick Start
60
35
 
61
- ### Check if a number is registered
36
+ ### 1 — Check if a number has WhatsApp
62
37
 
63
38
  ```bash
64
39
  wa registration --check 919634847671
65
40
  ```
66
41
 
67
- ### Request a verification code
42
+ ### 2 — Request a verification code
68
43
 
69
44
  ```bash
70
- # via SMS (default)
45
+ # Via SMS (default)
71
46
  wa registration --request-code 919634847671
72
47
 
73
- # via voice call
48
+ # Via voice call
74
49
  wa registration --request-code 919634847671 --method voice
75
50
 
76
- # via backup WhatsApp account (wa_old)
51
+ # Via an old WhatsApp account
77
52
  wa registration --request-code 919634847671 --method wa_old
78
53
  ```
79
54
 
80
- ### Register with the received code
55
+ ### 3 — Register with the code you received
81
56
 
82
57
  ```bash
83
58
  wa registration --register 919634847671 --code 123456
84
59
  ```
85
60
 
86
- ### Connect and enter an interactive shell
61
+ ### 4 Connect
87
62
 
88
63
  ```bash
89
64
  wa connect 919634847671
90
65
  ```
91
66
 
92
- Interactive commands at the `wa>` prompt:
67
+ You will enter an interactive shell:
93
68
 
94
69
  ```
95
- /send <jid> <text> Send a text message
96
- /presence Set presence to available
97
- /presence away Set presence to unavailable
98
- /quit Disconnect and exit
70
+ wa> /send 911234567890@s.whatsapp.net Hello!
71
+ wa> /presence
72
+ wa> /presence away
73
+ wa> /quit
99
74
  ```
100
75
 
101
- ### Send messages
76
+ ---
77
+
78
+ ## CLI Reference
79
+
80
+ ### Registration
81
+
82
+ | Command | Description |
83
+ |---|---|
84
+ | `wa registration --check <phone>` | Check if a number has WhatsApp |
85
+ | `wa registration --request-code <phone>` | Request SMS verification code |
86
+ | `wa registration --request-code <phone> --method voice` | Request code via voice call |
87
+ | `wa registration --request-code <phone> --method wa_old` | Request code via old WA account |
88
+ | `wa registration --register <phone> --code <code>` | Complete registration |
89
+
90
+ ### Connection
91
+
92
+ | Command | Description |
93
+ |---|---|
94
+ | `wa connect <phone>` | Connect and open interactive shell |
95
+ | `wa listen <phone>` | Connect and print all incoming messages |
96
+
97
+ ### Sending Messages
102
98
 
103
99
  ```bash
104
- # Text
105
- wa send 919634847671 --to 911234567890@s.whatsapp.net --text "Hello!"
100
+ # Text message
101
+ wa send <phone> --to <jid> --text "Hello!"
106
102
 
107
- # Image with caption
108
- wa send 919634847671 --to 911234567890@s.whatsapp.net --image ./photo.jpg --caption "Look at this"
103
+ # Image (with optional caption)
104
+ wa send <phone> --to <jid> --image ./photo.jpg --caption "Check this out"
109
105
 
110
- # Video
111
- wa send 919634847671 --to 911234567890@s.whatsapp.net --video ./clip.mp4
106
+ # Video (with optional caption)
107
+ wa send <phone> --to <jid> --video ./clip.mp4 --caption "Watch this"
112
108
 
113
- # Voice note (PTT)
114
- wa send 919634847671 --to 911234567890@s.whatsapp.net --ptt ./voice.ogg
109
+ # Voice note (push-to-talk)
110
+ wa send <phone> --to <jid> --ptt ./voice.ogg
115
111
 
116
112
  # Audio file
117
- wa send 919634847671 --to 911234567890@s.whatsapp.net --audio ./song.mp3
113
+ wa send <phone> --to <jid> --audio ./song.mp3
118
114
 
119
115
  # Document
120
- wa send 919634847671 --to 911234567890@s.whatsapp.net --document ./report.pdf
116
+ wa send <phone> --to <jid> --document ./report.pdf
121
117
 
122
118
  # Sticker
123
- wa send 919634847671 --to 911234567890@s.whatsapp.net --sticker ./sticker.webp
119
+ wa send <phone> --to <jid> --sticker ./sticker.webp
124
120
 
125
121
  # Emoji reaction
126
- wa send 919634847671 --to 911234567890@s.whatsapp.net --reaction 👍 --msg-id 3EB0ABCDEF123456
122
+ wa send <phone> --to <jid> --reaction 👍 --msg-id 3EB0ABCDEF123456
127
123
 
128
124
  # Group message
129
- wa send 919634847671 --to 120363000000000000@g.us --text "Hi everyone!"
130
- ```
131
-
132
- ### Listen for incoming messages
133
-
134
- ```bash
135
- wa listen 919634847671
136
- ```
137
-
138
- ### Print version
139
-
140
- ```bash
141
- wa version
125
+ wa send <phone> --to 120363000000000000@g.us --text "Hi everyone!"
142
126
  ```
143
127
 
144
- ### Full options reference
128
+ ### JID Format
145
129
 
146
- | Flag | Description |
130
+ | Target | JID |
147
131
  |---|---|
148
- | `--session <dir>` | Session directory (default: `~/.waSession`) |
149
- | `--method sms\|voice\|wa_old` | Verification code delivery method |
150
- | `--code <code>` | Verification code |
151
- | `--to <jid>` | Recipient JID (`phone@s.whatsapp.net` or `groupid@g.us`) |
152
- | `--text <msg>` | Text message body |
153
- | `--caption <text>` | Caption for media messages |
154
- | `--reaction <emoji>` | Emoji to send as a reaction |
155
- | `--msg-id <id>` | Message ID to react to (required with `--reaction`) |
132
+ | Individual | `919634847671@s.whatsapp.net` |
133
+ | Group | `120363000000000000@g.us` |
156
134
 
157
- ---
135
+ > Phone numbers must include the country code, without the `+` prefix.
158
136
 
159
- ## Library Usage
137
+ ### All Options
160
138
 
161
- ### Connect and send messages
139
+ | Flag | Default | Description |
140
+ |---|---|---|
141
+ | `--session <dir>` | `~/.waSession` | Directory where session files are stored |
142
+ | `--method` | `sms` | Verification method: `sms`, `voice`, or `wa_old` |
143
+ | `--code` | — | Verification code received via SMS/voice |
144
+ | `--to` | — | Recipient JID |
145
+ | `--text` | — | Text message body |
146
+ | `--caption` | — | Caption for image or video |
147
+ | `--reaction` | — | Emoji to react with |
148
+ | `--msg-id` | — | Message ID to react to (required with `--reaction`) |
162
149
 
163
- ```javascript
164
- const { WhalibmobClient } = require('whalibmob');
150
+ ---
165
151
 
166
- const client = new WhalibmobClient({ sessionDir: '/path/to/sessions' });
152
+ ## Sessions
167
153
 
168
- client.on('connected', async () => {
169
- // Text
170
- await client.sendText('919876543210@s.whatsapp.net', 'Hello!');
171
-
172
- // Image
173
- await client.sendImage('919876543210@s.whatsapp.net', '/path/to/photo.jpg', {
174
- caption: 'My photo'
175
- });
176
-
177
- // Image from Buffer
178
- const buf = require('fs').readFileSync('/path/to/photo.jpg');
179
- await client.sendImage('919876543210@s.whatsapp.net', buf, { caption: 'From buffer' });
180
-
181
- // Voice note
182
- await client.sendAudio('919876543210@s.whatsapp.net', '/path/to/voice.ogg', {
183
- ptt: true,
184
- seconds: 12
185
- });
186
-
187
- // Document
188
- await client.sendDocument('919876543210@s.whatsapp.net', '/path/to/file.pdf', {
189
- fileName: 'report.pdf',
190
- mimetype: 'application/pdf'
191
- });
192
-
193
- // Reaction
194
- await client.sendReaction('919876543210@s.whatsapp.net', '3EB0ABC123', '👍');
195
-
196
- // Group message
197
- await client.sendText('120363000000000000@g.us', 'Hi group!');
198
- });
154
+ Sessions are stored as JSON files in `~/.waSession/` by default.
155
+ Each phone number gets its own file: `~/.waSession/<phone>.json`.
199
156
 
200
- client.on('message', (msg) => {
201
- console.log('From:', msg.from, '|', msg.text);
202
- });
157
+ You only register once. After that, just connect:
203
158
 
204
- client.on('auth_failure', ({ reason }) => {
205
- console.error('Session revoked by WhatsApp:', reason);
206
- // reason values: '401', '403', 'not_authorized', 'device_removed', etc.
207
- });
159
+ ```bash
160
+ wa connect 919634847671
161
+ ```
208
162
 
209
- client.on('close', () => console.log('Disconnected'));
210
- client.on('error', (err) => console.error(err));
163
+ To use a custom session directory:
211
164
 
212
- await client.init('919634847671');
165
+ ```bash
166
+ wa connect 919634847671 --session /data/my-sessions
167
+ wa send 919634847671 --session /data/my-sessions --to 911234567890@s.whatsapp.net --text "Hi"
213
168
  ```
214
169
 
215
- ### Registration (programmatic)
170
+ ---
171
+
172
+ ## Library API
216
173
 
217
- ```javascript
174
+ ```js
218
175
  const {
219
- createNewStore,
220
- saveStore,
176
+ WhalibmobClient,
221
177
  loadStore,
222
- checkIfRegistered,
223
- requestSmsCode,
224
- verifyCode
178
+ checkNumberStatus,
179
+ makeJid,
180
+ generateMessageId
225
181
  } = require('whalibmob');
226
-
227
- const storePath = '/path/to/sessions/919634847671.json';
228
-
229
- // Create or load a session store
230
- let store = loadStore(storePath) || createNewStore('919634847671');
231
-
232
- // Check if already registered
233
- const status = await checkIfRegistered(store);
234
- console.log(status.status); // "ok" if registered
235
-
236
- // Request verification code — methods: "sms", "voice", "wa_old"
237
- const res = await requestSmsCode(store, 'sms');
238
- console.log(res.status); // "sent" on success
239
-
240
- // Verify the received code and save session
241
- const reg = await verifyCode(store, '123456');
242
- if (reg.status === 'ok') {
243
- saveStore(store, storePath);
244
- console.log('Registered!');
245
- }
246
182
  ```
247
183
 
248
- ---
184
+ ### Connect and send a message
249
185
 
250
- ## Events
186
+ ```js
187
+ const { WhalibmobClient, loadStore } = require('whalibmob');
188
+ const path = require('path');
251
189
 
252
- | Event | Payload | Description |
253
- |---|---|---|
254
- | `connected` | — | Server confirmed `<success>`, session is live |
255
- | `close` | — | Connection closed |
256
- | `disconnected` | — | TCP connection dropped (reconnect scheduled) |
257
- | `reconnecting` | `{ delay, attempt }` | About to reconnect after backoff delay |
258
- | `reconnected` | — | Reconnection succeeded |
259
- | `auth_failure` | `{ reason, node }` | Server sent `<failure>` — session revoked or account banned |
260
- | `error` | `Error` | Non-fatal error occurred |
261
- | `message` | `{ id, from, participant, ts, text?, plaintext?, isGroup? }` | Incoming message (plaintext is a Buffer) |
262
- | `receipt` | `{ id, from, type }` | Delivery or read receipt |
263
- | `presence` | `{ from, available }` | Contact came online / went offline |
264
- | `call` | `{ from, node }` | Incoming call (auto-rejected by the library) |
265
- | `notification` | `{ type, attrs, node }` | Raw server notification |
266
- | `decrypt_error` | `{ id, from, err }` | Decryption failed; retry request sent automatically |
267
- | `media_conn` | `{ hosts, auth }` | Media upload server connection ready |
268
- | `session_refresh` | `{ node }` | Server sent a late `<success>` (re-auth flow) |
269
- | `stream_error` | `{ reason }` | Server-side stream error |
270
-
271
- ---
190
+ const sessionDir = path.join(process.env.HOME, '.waSession');
191
+ const store = loadStore('919634847671', sessionDir);
272
192
 
273
- ## Session Files
193
+ const client = new WhalibmobClient(store, { sessionDir });
274
194
 
275
- Sessions are stored as JSON in the session directory (default `~/.waSession`):
276
-
277
- ```
278
- ~/.waSession/919634847671.json # WhatsApp credentials and keys
279
- ~/.waSession/919634847671.signal.json # Signal Protocol session state
280
- ~/.waSession/919634847671.sk.json # SenderKey group state + SKDM map
281
- ```
282
-
283
- Once registered, sessions persist across restarts. Re-registration is only
284
- required if WhatsApp revokes the session (you will receive an `auth_failure`
285
- event with reason `401` or `not_authorized`).
286
-
287
- ---
288
-
289
- ## Signal Protocol
195
+ client.on('connected', async () => {
196
+ await client.sendText('911234567890@s.whatsapp.net', 'Hello from whalibmob!');
197
+ });
290
198
 
291
- End-to-end encryption follows the Signal Protocol spec:
199
+ client.on('message', msg => {
200
+ console.log('Received:', msg);
201
+ });
292
202
 
293
- 1. On first connection, pre-keys are generated and uploaded to WhatsApp.
294
- 2. When contacting a new recipient, the library fetches their pre-key bundle
295
- and runs X3DH to establish a session.
296
- 3. All subsequent messages use the Double Ratchet algorithm.
297
- 4. Pre-keys are automatically replenished when supply drops below 20.
298
- 5. Session state is persisted to disk between restarts.
203
+ client.on('disconnected', ({ reason }) => {
204
+ console.log('Disconnected:', reason);
205
+ });
299
206
 
300
- ---
207
+ await client.connect();
208
+ ```
301
209
 
302
- ## Group Messaging (SenderKey)
210
+ ### Send media
303
211
 
304
- Group messages use the SenderKey protocol for efficient delivery:
212
+ ```js
213
+ await client.sendImage ('911234567890@s.whatsapp.net', './photo.jpg', 'Caption');
214
+ await client.sendVideo ('911234567890@s.whatsapp.net', './clip.mp4', 'Caption');
215
+ await client.sendPtt ('911234567890@s.whatsapp.net', './voice.ogg');
216
+ await client.sendAudio ('911234567890@s.whatsapp.net', './song.mp3');
217
+ await client.sendDocument('911234567890@s.whatsapp.net', './report.pdf');
218
+ await client.sendSticker ('911234567890@s.whatsapp.net', './sticker.webp');
219
+ ```
305
220
 
306
- 1. Each sender generates a SenderKey (signing key pair + chain key).
307
- 2. On first group message, a SenderKey Distribution Message (SKDM) is
308
- encrypted individually for every device of every group member and sent.
309
- 3. Subsequent messages are encrypted once with the SenderKey and broadcast
310
- — no per-member re-encryption needed.
311
- 4. The library tracks which devices have already received the SKDM (persisted
312
- in `.sk.json`) and skips them on subsequent messages.
313
- 5. The chain key advances with each message (HMAC-SHA256).
314
- 6. Out-of-order message decryption is supported via cached message keys.
221
+ ### Check a number
315
222
 
316
- ---
223
+ ```js
224
+ const { checkNumberStatus } = require('whalibmob');
317
225
 
318
- ## Multi-Device
226
+ const result = await checkNumberStatus('919634847671');
227
+ // result.status → 'registered' | 'registered_blocked' | 'not_registered' | 'cooldown' | 'unknown'
228
+ console.log(result.status);
229
+ ```
319
230
 
320
- The library fully supports WhatsApp's multi-device architecture:
231
+ ### Events
321
232
 
322
- - Device lists are fetched via usync queries.
323
- - Signal sessions are established for each linked device.
324
- - All outgoing DMs are fanned out to every device of every recipient as well
325
- as the sender's own linked devices (tablet, secondary phone, etc.).
326
- - Own linked devices receive a `deviceSentMessage` wrapper so they display
327
- the message as sent (not received).
328
- - All multi-device stanzas include a `phash` (SHA-256 participant hash) so
329
- the server can validate the participant list.
330
- - SKDM distribution in groups covers all member devices.
233
+ | Event | Payload | Description |
234
+ |---|---|---|
235
+ | `connected` | | Session authenticated and ready |
236
+ | `disconnected` | `{ reason }` | Connection closed |
237
+ | `auth_failure` | `{ reason }` | Auth failed (banned or session revoked) |
238
+ | `message` | message object | Incoming message received |
239
+ | `message.sent` | message object | Outgoing message confirmed by server |
240
+ | `error` | Error | Unhandled transport error |
331
241
 
332
242
  ---
333
243
 
334
- ## Noise Handshake & Auth
335
-
336
- The transport uses **Noise_XX_25519_AESGCM_SHA256**:
244
+ ## Features
337
245
 
338
- 1. TCP connection to `g.whatsapp.net:443`.
339
- 2. Client sends `ClientHello` with ephemeral X25519 public key.
340
- 3. Server replies with `ServerHello` (ephemeral + encrypted static + payload).
341
- 4. Three Diffie-Hellman steps (EE, SE, SS) derive the session keys.
342
- 5. Client sends `ClientFinish` (encrypted static + encrypted payload).
343
- 6. **Library waits for the server's `<success>` or `<failure>` node.**
344
- 7. Only after `<success>` is the channel opened and the `connected` event
345
- emitted. A `<failure>` rejects with an `auth_failure` event.
246
+ - **Mobile iOS fingerprint** — authentic iOS headers; WhatsApp servers treat it as an iPhone
247
+ - **Signal Protocol E2E encryption** X3DH key agreement, Double Ratchet for 1-to-1 messages, fully inlined (no native binaries, no GitHub dependencies)
248
+ - **SenderKey group encryption** efficient group messaging with chain-key advance and SKDM distribution
249
+ - **Multi-device fanout** messages delivered to every linked device of each recipient
250
+ - **All media types** images, video, audio, voice notes, documents, stickers
251
+ - **Media encryption** AES-256-CBC + HKDF-SHA256; files uploaded to WhatsApp CDN
252
+ - **Emoji reactions** react to any message by its ID
253
+ - **Automatic reconnection** exponential backoff (1 s → 2 → 4 → 8 → 15 → 30 s cap)
254
+ - **Pre-key replenishment** — automatic upload when supply drops below threshold
255
+ - **Persistent sessions** — JSON files on disk; no re-registration after restart
256
+ - **Zero native dependencies** — pure JavaScript; works on Linux, macOS, Windows, Termux (Android)
346
257
 
347
258
  ---
348
259
 
349
- ## Automatic Reconnection
350
-
351
- The client reconnects on disconnect with exponential backoff:
260
+ ## Reconnection Backoff
352
261
 
353
262
  | Attempt | Delay |
354
263
  |---|---|
@@ -361,16 +270,26 @@ The client reconnects on disconnect with exponential backoff:
361
270
 
362
271
  ---
363
272
 
364
- ## Media Encryption
365
-
366
- Media files use WhatsApp's standard encryption scheme:
273
+ ## Media Encryption Scheme
367
274
 
368
275
  1. A random 32-byte media key is generated.
369
- 2. HKDF-SHA256 expands it to produce the IV, cipher key, and MAC key.
276
+ 2. HKDF-SHA256 expands it into an IV, cipher key, and MAC key.
370
277
  3. The file is encrypted with AES-256-CBC.
371
278
  4. A 10-byte HMAC-SHA256 MAC is appended.
372
- 5. The encrypted ciphertext is uploaded to WhatsApp's CDN.
373
- 6. The media key and URL are sent in the message envelope.
279
+ 5. The ciphertext is uploaded to WhatsApp CDN.
280
+ 6. The media key and CDN URL are embedded in the message envelope.
281
+
282
+ ---
283
+
284
+ ## Transport
285
+
286
+ Uses **Noise_XX_25519_AESGCM_SHA256** over TCP to `g.whatsapp.net:443`:
287
+
288
+ 1. Client sends `ClientHello` with an ephemeral X25519 public key.
289
+ 2. Server replies `ServerHello` (ephemeral + encrypted static + payload).
290
+ 3. Three DH steps (EE, SE, SS) derive the final session keys.
291
+ 4. Client sends `ClientFinish` (encrypted static + encrypted payload).
292
+ 5. Library waits for server `<success>` before emitting `connected`, or `<failure>` for `auth_failure`.
374
293
 
375
294
  ---
376
295
 
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,13 +1,25 @@
1
1
  {
2
2
  "name": "whalibmob",
3
- "version": "2.1.0",
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": {
7
7
  "wa": "./cli.js"
8
8
  },
9
+ "files": [
10
+ "index.js",
11
+ "cli.js",
12
+ "lib/**/*.js"
13
+ ],
9
14
  "type": "commonjs",
10
15
  "license": "MIT",
16
+ "keywords": [
17
+ "whatsapp",
18
+ "signal-protocol",
19
+ "mobile",
20
+ "client",
21
+ "encryption"
22
+ ],
11
23
  "dependencies": {
12
24
  "@noble/curves": "^1.8.1",
13
25
  "@noble/hashes": "^1.7.2",