whalibmob 5.26.0 → 5.28.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/index.d.ts +63 -1
- package/index.js +7 -0
- package/lib/Client.js +76 -28
- package/lib/CompanionPairing.js +1 -1
- package/lib/HistorySyncHandler.js +2 -2
- package/lib/MediaService.js +2 -2
- package/lib/PairingCode.js +4 -6
- package/lib/Registration.js +1 -1
- package/lib/Store.js +1 -1
- package/lib/appstate/AppStateSync.js +3 -3
- package/lib/appstate/LTHash.js +2 -5
- package/lib/curve.js +148 -0
- package/lib/hkdf.js +65 -0
- package/lib/messages/MediaRetry.js +2 -3
- package/lib/messages/MessageSender.js +1 -1
- package/lib/messages/ReportingToken.js +2 -2
- package/lib/noise.js +3 -3
- package/lib/proto/MessageProto.js +83 -1
- package/lib/signal/SenderKey.js +3 -3
- package/lib/signal/libsignal/curve.js +1 -1
- package/package.json +1 -1
package/index.d.ts
CHANGED
|
@@ -123,6 +123,14 @@ export interface WhalibmobClientOptions {
|
|
|
123
123
|
autoFixNumber?: boolean;
|
|
124
124
|
/** Send read receipts for incoming messages. Default `true`. */
|
|
125
125
|
autoRead?: boolean;
|
|
126
|
+
/**
|
|
127
|
+
* Refuse every incoming call. Default `true`.
|
|
128
|
+
*
|
|
129
|
+
* Turning it off does not answer calls — nothing here can — it leaves the
|
|
130
|
+
* refusal to you, which is the only way to let some calls ring and reject the
|
|
131
|
+
* rest with `rejectCall()`.
|
|
132
|
+
*/
|
|
133
|
+
autoRejectCalls?: boolean;
|
|
126
134
|
/** Refresh the announced build from the platform's store before every handshake. Default `true`. */
|
|
127
135
|
refreshVersion?: boolean;
|
|
128
136
|
/** `true` enables debug logging; an object is handed to `pino` as-is. */
|
|
@@ -227,8 +235,24 @@ export interface PollSendResult extends SendResult {
|
|
|
227
235
|
messageSecret: Buffer;
|
|
228
236
|
}
|
|
229
237
|
|
|
238
|
+
/** What a message was a reply to, and who it mentioned. */
|
|
239
|
+
export interface DecodedContextInfo {
|
|
240
|
+
/** Id of the quoted message. */
|
|
241
|
+
stanzaId?: string;
|
|
242
|
+
/** Who wrote the quoted message. */
|
|
243
|
+
participant?: Jid;
|
|
244
|
+
remoteJid?: Jid;
|
|
245
|
+
mentionedJid?: Jid[];
|
|
246
|
+
/** The quoted message, decoded the same way as any other. */
|
|
247
|
+
quotedMessage?: DecodedMessage;
|
|
248
|
+
/** The quoted message's raw protobuf bytes, for re-encoding it verbatim. */
|
|
249
|
+
quotedMessageRaw?: Buffer;
|
|
250
|
+
}
|
|
251
|
+
|
|
230
252
|
export interface DecodedBase {
|
|
231
253
|
type: string;
|
|
254
|
+
/** Present when the message quoted another or carried mentions. */
|
|
255
|
+
contextInfo?: DecodedContextInfo;
|
|
232
256
|
/**
|
|
233
257
|
* The message arrived inside a view-once envelope. The content is decoded
|
|
234
258
|
* normally — media included — this only records how it was sent.
|
|
@@ -632,7 +656,7 @@ export interface WhalibmobEvents {
|
|
|
632
656
|
message: (msg: IncomingMessage) => void;
|
|
633
657
|
receipt: (r: { type: string; id: string; from: Jid }) => void;
|
|
634
658
|
presence: (p: { from: Jid; available: boolean }) => void;
|
|
635
|
-
call: (c: { from: Jid }) => void;
|
|
659
|
+
call: (c: { from: Jid; id: string | null; status: 'offer' | 'ringing' | 'terminate' | 'unknown'; node: any }) => void;
|
|
636
660
|
notification: (node: any) => void;
|
|
637
661
|
decrypt_error: (e: { id: string; from: Jid; participant?: Jid; err: Error }) => void;
|
|
638
662
|
session_refresh: (e: { node: any }) => void;
|
|
@@ -831,6 +855,12 @@ export declare class WhalibmobClient extends EventEmitter {
|
|
|
831
855
|
*/
|
|
832
856
|
requestAppStateKeys(keyIds: string[]): Promise<string | null>;
|
|
833
857
|
|
|
858
|
+
/**
|
|
859
|
+
* Refuse one incoming call. Only reachable when the client was built with
|
|
860
|
+
* `autoRejectCalls: false`; the id and caller come off the `call` event.
|
|
861
|
+
*/
|
|
862
|
+
rejectCall(callId: string, from: Jid): boolean;
|
|
863
|
+
|
|
834
864
|
// ─── Account restriction ─────────────────────────────────────────────────
|
|
835
865
|
fetchReachoutTimelock(): Promise<ReachoutTimelockState>;
|
|
836
866
|
/** The last known state, recomputed; does not ask the server again. */
|
|
@@ -1314,6 +1344,38 @@ export declare const PushClient: LibModule;
|
|
|
1314
1344
|
export declare const Fcm: LibModule;
|
|
1315
1345
|
export declare const FcmMcs: LibModule;
|
|
1316
1346
|
|
|
1347
|
+
/**
|
|
1348
|
+
* X25519 through Node's own OpenSSL — a drop-in for `curve25519-js`, roughly
|
|
1349
|
+
* 19x faster on `sharedKey` and byte-identical to it.
|
|
1350
|
+
*
|
|
1351
|
+
* `sign` and `verify` are XEdDSA and stay on the JavaScript implementation:
|
|
1352
|
+
* Node's Ed25519 is a different scheme and would produce different bytes.
|
|
1353
|
+
*/
|
|
1354
|
+
export declare const Curve: LibModule & {
|
|
1355
|
+
generateKeyPair(seed: Buffer | Uint8Array): { private: Buffer; public: Buffer };
|
|
1356
|
+
sharedKey(priv: Buffer | Uint8Array, pub: Buffer | Uint8Array): Buffer;
|
|
1357
|
+
sign(priv: Buffer | Uint8Array, message: Buffer | Uint8Array): Buffer;
|
|
1358
|
+
verify(pub: Buffer | Uint8Array, message: Buffer | Uint8Array, signature: Buffer | Uint8Array): boolean;
|
|
1359
|
+
/** Whether Node's X25519 is in use; `false` means the JavaScript fallback is. */
|
|
1360
|
+
NATIVE: boolean;
|
|
1361
|
+
};
|
|
1362
|
+
|
|
1363
|
+
/**
|
|
1364
|
+
* HKDF-SHA256 through Node's own OpenSSL — roughly 2.2x faster than the
|
|
1365
|
+
* JavaScript implementation and byte-identical to it. Returns a `Buffer`,
|
|
1366
|
+
* not the `ArrayBuffer` Node's own `hkdfSync` hands back.
|
|
1367
|
+
*/
|
|
1368
|
+
export declare const Hkdf: LibModule & {
|
|
1369
|
+
hkdfSha256(
|
|
1370
|
+
ikm: Buffer | Uint8Array,
|
|
1371
|
+
salt: Buffer | Uint8Array | string,
|
|
1372
|
+
info: Buffer | Uint8Array | string,
|
|
1373
|
+
length: number
|
|
1374
|
+
): Buffer;
|
|
1375
|
+
/** Whether Node's HKDF is in use; `false` means the JavaScript fallback is. */
|
|
1376
|
+
NATIVE: boolean;
|
|
1377
|
+
};
|
|
1378
|
+
|
|
1317
1379
|
/** Linking to an account that already exists. */
|
|
1318
1380
|
export declare const PairingCode: LibModule;
|
|
1319
1381
|
export declare const CompanionPairing: LibModule;
|
package/index.js
CHANGED
|
@@ -129,6 +129,8 @@ const WebVersion = require('./lib/WebVersion');
|
|
|
129
129
|
const WebProto = require('./lib/webproto');
|
|
130
130
|
|
|
131
131
|
const BinaryNode = require('./lib/BinaryNode');
|
|
132
|
+
const Curve = require('./lib/curve');
|
|
133
|
+
const Hkdf = require('./lib/hkdf');
|
|
132
134
|
const Noise = require('./lib/noise');
|
|
133
135
|
const WebSocketStream = require('./lib/WebSocketStream');
|
|
134
136
|
const Socks = require('./lib/socks');
|
|
@@ -348,6 +350,11 @@ module.exports = {
|
|
|
348
350
|
FcmMcs,
|
|
349
351
|
|
|
350
352
|
// Linking to an account that already exists
|
|
353
|
+
// X25519, through Node's own OpenSSL. Drop-in for curve25519-js.
|
|
354
|
+
Curve,
|
|
355
|
+
// HKDF-SHA256, through Node's own OpenSSL.
|
|
356
|
+
Hkdf,
|
|
357
|
+
|
|
351
358
|
PairingCode,
|
|
352
359
|
CompanionPairing,
|
|
353
360
|
QrPairing,
|
package/lib/Client.js
CHANGED
|
@@ -496,6 +496,12 @@ class WhalibmobClient extends EventEmitter {
|
|
|
496
496
|
// Blue-tick every message as it arrives. Set to false to make read receipts
|
|
497
497
|
// explicit, sent only by a markRead() call.
|
|
498
498
|
this.autoRead = opts.autoRead !== false;
|
|
499
|
+
|
|
500
|
+
// Every incoming call is refused unless this is turned off. Off does not
|
|
501
|
+
// mean calls are answered — nothing here can answer one — it means the
|
|
502
|
+
// refusal is left to the caller, which is the only way to let some calls
|
|
503
|
+
// ring and reject the rest. See rejectCall().
|
|
504
|
+
this._autoRejectCalls = opts.autoRejectCalls !== false;
|
|
499
505
|
// Flipped by setOnline(). Delivery receipts sent while this is false carry
|
|
500
506
|
// type="inactive", which the sender's app records but does not render.
|
|
501
507
|
this._sendActiveReceipts = false;
|
|
@@ -4162,20 +4168,46 @@ class WhalibmobClient extends EventEmitter {
|
|
|
4162
4168
|
}
|
|
4163
4169
|
}
|
|
4164
4170
|
|
|
4165
|
-
|
|
4166
|
-
//
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4171
|
+
// What the stanza actually says, rather than the raw node alone: which call
|
|
4172
|
+
// it is, and what stage it is at. A caller deciding whether to reject needs
|
|
4173
|
+
// both, and digging them out of the node is not its job.
|
|
4174
|
+
const offer = findChild(node, 'offer');
|
|
4175
|
+
const relay = findChild(node, 'relaylatency');
|
|
4176
|
+
const term = findChild(node, 'terminate');
|
|
4177
|
+
const callId = (offer && offer.attrs && offer.attrs.call_id) ||
|
|
4178
|
+
(term && term.attrs && term.attrs.call_id) || null;
|
|
4179
|
+
const status = offer ? 'offer' : (term ? 'terminate' : (relay ? 'ringing' : 'unknown'));
|
|
4180
|
+
|
|
4181
|
+
this.emit('call', { from: attrs.from || '', id: callId, status, node });
|
|
4182
|
+
|
|
4183
|
+
// Reject every incoming call, unless the caller asked to decide for itself.
|
|
4184
|
+
// Turning this off does not answer calls — nothing here can — it only stops
|
|
4185
|
+
// the automatic refusal, so a caller can reject some and let the rest ring.
|
|
4186
|
+
if (this._autoRejectCalls && callId && status === 'offer') {
|
|
4187
|
+
this.rejectCall(callId, attrs.from || '');
|
|
4176
4188
|
}
|
|
4177
4189
|
}
|
|
4178
4190
|
|
|
4191
|
+
/**
|
|
4192
|
+
* Refuse one incoming call.
|
|
4193
|
+
*
|
|
4194
|
+
* Only needed when the client was built with `autoRejectCalls: false`; with
|
|
4195
|
+
* the default every call is refused before this could be reached. The call id
|
|
4196
|
+
* and the caller both come off the `call` event.
|
|
4197
|
+
*
|
|
4198
|
+
* @param {string} callId the `id` from the call event
|
|
4199
|
+
* @param {string} from the `from` from the call event
|
|
4200
|
+
* @returns {boolean} whether the refusal could be sent
|
|
4201
|
+
*/
|
|
4202
|
+
rejectCall(callId, from) {
|
|
4203
|
+
if (!callId || !this._socket || !this._connected) return false;
|
|
4204
|
+
this._socket.sendNode(new BinaryNode('call', {
|
|
4205
|
+
to: from || '',
|
|
4206
|
+
id: this._genMsgId()
|
|
4207
|
+
}, [new BinaryNode('reject', { call_id: callId, call_creator: from || '' }, null)]));
|
|
4208
|
+
return true;
|
|
4209
|
+
}
|
|
4210
|
+
|
|
4179
4211
|
// ─── Auth failure (server forces logout during active session) ──────────────
|
|
4180
4212
|
|
|
4181
4213
|
// <failure> ends the connection, but not every reason means the session died.
|
|
@@ -5461,9 +5493,9 @@ class WhalibmobClient extends EventEmitter {
|
|
|
5461
5493
|
// Web-mode history lands in its own files. A number can be registered over
|
|
5462
5494
|
// SMS and separately linked as a companion, and only the companion ever
|
|
5463
5495
|
// receives history — merging the two would be merging different accounts'
|
|
5464
|
-
// views of the same number.
|
|
5465
|
-
const phone =
|
|
5466
|
-
|
|
5496
|
+
// views of the same number. Same prefix the readers use.
|
|
5497
|
+
const phone = this._sessionFilePrefix();
|
|
5498
|
+
if (!phone) return;
|
|
5467
5499
|
const sessionDir = this._sessionDir;
|
|
5468
5500
|
|
|
5469
5501
|
_whaDbg('[DBG] HIST_SYNC start syncType=' + notification.syncType +
|
|
@@ -5680,15 +5712,25 @@ class WhalibmobClient extends EventEmitter {
|
|
|
5680
5712
|
}
|
|
5681
5713
|
|
|
5682
5714
|
/**
|
|
5683
|
-
*
|
|
5684
|
-
* Returns the parsed {phone}.history.json content or null if not yet synced.
|
|
5715
|
+
* The name every file of this session is built from.
|
|
5685
5716
|
*
|
|
5686
|
-
*
|
|
5717
|
+
* A number can be registered over SMS and separately linked as a companion,
|
|
5718
|
+
* and the two are different accounts' views of it, so the companion's files
|
|
5719
|
+
* carry a `.web` in the middle: 40712345678.web.history.json. Everything that
|
|
5720
|
+
* reads or writes one has to agree on that — a writer that adds the `.web`
|
|
5721
|
+
* and a reader that does not will not error, it will simply never find the
|
|
5722
|
+
* file it just wrote.
|
|
5687
5723
|
*/
|
|
5688
|
-
|
|
5724
|
+
_sessionFilePrefix() {
|
|
5689
5725
|
if (!this._store || !this._store.phoneNumber) return null;
|
|
5726
|
+
return this._store.phoneNumber + (this._mode === 'web' ? '.web' : '');
|
|
5727
|
+
}
|
|
5728
|
+
|
|
5729
|
+
_readSessionJson(suffix) {
|
|
5730
|
+
const prefix = this._sessionFilePrefix();
|
|
5731
|
+
if (!prefix) return null;
|
|
5690
5732
|
const fs = require('fs');
|
|
5691
|
-
const file = require('path').join(this._sessionDir,
|
|
5733
|
+
const file = require('path').join(this._sessionDir, prefix + suffix);
|
|
5692
5734
|
try {
|
|
5693
5735
|
if (!fs.existsSync(file)) return null;
|
|
5694
5736
|
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
@@ -5696,19 +5738,25 @@ class WhalibmobClient extends EventEmitter {
|
|
|
5696
5738
|
}
|
|
5697
5739
|
|
|
5698
5740
|
/**
|
|
5699
|
-
*
|
|
5700
|
-
* Returns the parsed {phone}.
|
|
5741
|
+
* getHistoryStore — load the persisted history store for this session.
|
|
5742
|
+
* Returns the parsed {phone}.history.json content — {phone}.web.history.json
|
|
5743
|
+
* on a companion — or null if not yet synced.
|
|
5744
|
+
*
|
|
5745
|
+
* @returns {object|null}
|
|
5746
|
+
*/
|
|
5747
|
+
getHistoryStore() {
|
|
5748
|
+
return this._readSessionJson('.history.json');
|
|
5749
|
+
}
|
|
5750
|
+
|
|
5751
|
+
/**
|
|
5752
|
+
* getMessagesStore — load the persisted messages for this session.
|
|
5753
|
+
* Returns the parsed {phone}.messages.json content — {phone}.web.messages.json
|
|
5754
|
+
* on a companion — or null if not yet synced.
|
|
5701
5755
|
*
|
|
5702
5756
|
* @returns {object|null}
|
|
5703
5757
|
*/
|
|
5704
5758
|
getMessagesStore() {
|
|
5705
|
-
|
|
5706
|
-
const fs = require('fs');
|
|
5707
|
-
const file = require('path').join(this._sessionDir, this._store.phoneNumber + '.messages.json');
|
|
5708
|
-
try {
|
|
5709
|
-
if (!fs.existsSync(file)) return null;
|
|
5710
|
-
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
5711
|
-
} catch (_) { return null; }
|
|
5759
|
+
return this._readSessionJson('.messages.json');
|
|
5712
5760
|
}
|
|
5713
5761
|
|
|
5714
5762
|
// ─── Public API ───────────────────────────────────────────────────────────
|
package/lib/CompanionPairing.js
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
// from that account.
|
|
19
19
|
|
|
20
20
|
const crypto = require('crypto');
|
|
21
|
-
const curveJs = require('
|
|
21
|
+
const curveJs = require('./curve');
|
|
22
22
|
|
|
23
23
|
const WA_ADV_ACCOUNT_SIG_PREFIX = Buffer.from([6, 0]);
|
|
24
24
|
const WA_ADV_DEVICE_SIG_PREFIX = Buffer.from([6, 1]);
|
|
@@ -26,13 +26,13 @@
|
|
|
26
26
|
*/
|
|
27
27
|
|
|
28
28
|
const crypto = require('crypto');
|
|
29
|
+
const { hkdfSha256 } = require('./hkdf');
|
|
29
30
|
const https = require('https');
|
|
30
31
|
const http = require('http');
|
|
31
32
|
const zlib = require('zlib');
|
|
32
33
|
const fs = require('fs');
|
|
33
34
|
const path = require('path');
|
|
34
35
|
|
|
35
|
-
const { hkdf } = require('@noble/hashes/hkdf');
|
|
36
36
|
const { sha256 } = require('@noble/hashes/sha256');
|
|
37
37
|
|
|
38
38
|
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
@@ -442,7 +442,7 @@ function decodeHistorySync(buf) {
|
|
|
442
442
|
function deriveHistoryKeys(mediaKeyBuf) {
|
|
443
443
|
const ikm = Buffer.isBuffer(mediaKeyBuf) ? mediaKeyBuf : Buffer.from(mediaKeyBuf);
|
|
444
444
|
const info = Buffer.from(HISTORY_HKDF_INFO, 'utf8');
|
|
445
|
-
const expanded =
|
|
445
|
+
const expanded = hkdfSha256(ikm, Buffer.alloc(0), info, EXPANDED_SIZE);
|
|
446
446
|
return {
|
|
447
447
|
iv: expanded.slice(0, IV_LEN),
|
|
448
448
|
cipherKey: expanded.slice(IV_LEN, IV_LEN + KEY_LEN),
|
package/lib/MediaService.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const crypto = require('crypto');
|
|
4
|
+
const { hkdfSha256 } = require('./hkdf');
|
|
4
5
|
const https = require('https');
|
|
5
6
|
const http = require('http');
|
|
6
7
|
const fs = require('fs');
|
|
7
8
|
const path = require('path');
|
|
8
|
-
const { hkdf } = require('@noble/hashes/hkdf');
|
|
9
9
|
const { sha256 } = require('@noble/hashes/sha256');
|
|
10
10
|
const { getDeviceConfig } = require('./DeviceConfig');
|
|
11
11
|
|
|
@@ -69,7 +69,7 @@ const MAC_LEN = 10;
|
|
|
69
69
|
function deriveMediaKeyData(mediaKey, keyName) {
|
|
70
70
|
const ikm = Buffer.isBuffer(mediaKey) ? mediaKey : Buffer.from(mediaKey);
|
|
71
71
|
const info = Buffer.from(keyName, 'utf8');
|
|
72
|
-
return
|
|
72
|
+
return hkdfSha256(ikm, Buffer.alloc(0), info, EXPANDED_SIZE);
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
function encryptMedia(plaintext, mediaKey, keyName) {
|
package/lib/PairingCode.js
CHANGED
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
// in normally.
|
|
26
26
|
|
|
27
27
|
const crypto = require('crypto');
|
|
28
|
-
const
|
|
28
|
+
const { hkdfSha256 } = require('./hkdf');
|
|
29
|
+
const curveJs = require('./curve');
|
|
29
30
|
|
|
30
31
|
// Crockford base32 minus the ambiguous characters — no I, O, U, and no 0.
|
|
31
32
|
// Five random bytes become exactly eight characters.
|
|
@@ -85,15 +86,12 @@ function aesEncryptGCM(plaintext, key, iv, aad) {
|
|
|
85
86
|
}
|
|
86
87
|
|
|
87
88
|
function hkdf(input, length, info, salt) {
|
|
88
|
-
|
|
89
|
-
const { sha256 } = require('@noble/hashes/sha256');
|
|
90
|
-
return Buffer.from(nobleHkdf(
|
|
91
|
-
sha256,
|
|
89
|
+
return hkdfSha256(
|
|
92
90
|
input,
|
|
93
91
|
salt || Buffer.alloc(32),
|
|
94
92
|
info ? Buffer.from(info, 'utf8') : Buffer.alloc(0),
|
|
95
93
|
length
|
|
96
|
-
)
|
|
94
|
+
);
|
|
97
95
|
}
|
|
98
96
|
|
|
99
97
|
function stripKeyPrefix(pub) {
|
package/lib/Registration.js
CHANGED
|
@@ -6,7 +6,7 @@ const tls = require('tls');
|
|
|
6
6
|
const fs = require('fs');
|
|
7
7
|
const os = require('os');
|
|
8
8
|
const path = require('path');
|
|
9
|
-
const curveJs = require('
|
|
9
|
+
const curveJs = require('./curve');
|
|
10
10
|
const { v4: uuidv4 } = require('uuid');
|
|
11
11
|
const { getDeviceConfig } = require('./DeviceConfig');
|
|
12
12
|
const AndroidApk = require('./AndroidApk');
|
package/lib/Store.js
CHANGED
|
@@ -5,7 +5,7 @@ const { warn: _whaWarn, dbg: _whaDbg } = require('./logger');
|
|
|
5
5
|
const fs = require('fs');
|
|
6
6
|
const path = require('path');
|
|
7
7
|
const crypto = require('crypto');
|
|
8
|
-
const curveJs = require('
|
|
8
|
+
const curveJs = require('./curve');
|
|
9
9
|
const { v4: uuidv4 } = require('uuid');
|
|
10
10
|
const { IOS_DEVICE, IOS_VERSION_FALLBACK, ANDROID_VERSION_FALLBACK,
|
|
11
11
|
platformForOs, isBusinessPlatform } = require('./constants');
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const crypto = require('crypto');
|
|
4
|
-
const {
|
|
4
|
+
const { hkdfSha256 } = require('../hkdf');
|
|
5
5
|
const { sha256 } = require('@noble/hashes/sha256');
|
|
6
6
|
const { makeGenerator, newState } = require('./LTHash');
|
|
7
7
|
const S = require('./SyncdProto');
|
|
@@ -19,8 +19,8 @@ const KEY_EXPANSION_LEN = 160;
|
|
|
19
19
|
|
|
20
20
|
function mutationKeys(keyData) {
|
|
21
21
|
const ikm = Buffer.isBuffer(keyData) ? keyData : Buffer.from(keyData);
|
|
22
|
-
const e =
|
|
23
|
-
Buffer.from(KEY_EXPANSION_INFO, 'utf8'), KEY_EXPANSION_LEN)
|
|
22
|
+
const e = hkdfSha256(ikm, Buffer.alloc(0),
|
|
23
|
+
Buffer.from(KEY_EXPANSION_INFO, 'utf8'), KEY_EXPANSION_LEN);
|
|
24
24
|
return {
|
|
25
25
|
indexKey: e.subarray(0, 32),
|
|
26
26
|
valueEncryptionKey: e.subarray(32, 64),
|
package/lib/appstate/LTHash.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
|
+
const { hkdfSha256 } = require('../hkdf');
|
|
2
3
|
|
|
3
|
-
const { hkdf } = require('@noble/hashes/hkdf');
|
|
4
|
-
const { sha256 } = require('@noble/hashes/sha256');
|
|
5
4
|
|
|
6
5
|
// A summation hash that survives being edited out of order.
|
|
7
6
|
//
|
|
@@ -30,9 +29,7 @@ class LTHash {
|
|
|
30
29
|
// 128 bytes of key material for one value.
|
|
31
30
|
_expand(value) {
|
|
32
31
|
const ikm = Buffer.isBuffer(value) ? value : Buffer.from(value);
|
|
33
|
-
return Buffer.from(
|
|
34
|
-
hkdf(sha256, ikm, Buffer.alloc(0), Buffer.from(this.salt, 'utf8'), HASH_BYTES)
|
|
35
|
-
);
|
|
32
|
+
return hkdfSha256(ikm, Buffer.alloc(0), Buffer.from(this.salt, 'utf8'), HASH_BYTES);
|
|
36
33
|
}
|
|
37
34
|
|
|
38
35
|
// Word-by-word, 16 bits at a time, little-endian, wrapping at 2^16.
|
package/lib/curve.js
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// X25519, through Node's own OpenSSL rather than a JavaScript field arithmetic
|
|
4
|
+
// implementation.
|
|
5
|
+
//
|
|
6
|
+
// Every session setup and every ratchet step does a Diffie-Hellman, and every
|
|
7
|
+
// registration derives 812 pre-keys, so this is the busiest arithmetic in the
|
|
8
|
+
// library. Measured on the same machine, over 3000 iterations each:
|
|
9
|
+
//
|
|
10
|
+
// sharedKey curve25519-js 0.765 ms/op Node 0.040 ms/op 19x
|
|
11
|
+
// generateKeyPair curve25519-js 0.779 ms/op Node 0.085 ms/op 9x
|
|
12
|
+
//
|
|
13
|
+
// The results are byte-identical, which is the only reason this swap is
|
|
14
|
+
// allowed to exist — verified over 200 random key pairs in test/curve.test.js
|
|
15
|
+
// rather than assumed from the fact that both claim to implement RFC 7748.
|
|
16
|
+
//
|
|
17
|
+
// Two things are deliberately NOT native:
|
|
18
|
+
//
|
|
19
|
+
// sign / verify are XEdDSA — signing with a Montgomery key, which is what
|
|
20
|
+
// Signal specifies and what WhatsApp's device identities are signed with.
|
|
21
|
+
// Node has Ed25519, which is a different scheme over a different encoding of
|
|
22
|
+
// the same curve; swapping one for the other would produce different bytes
|
|
23
|
+
// and every signature this library has ever written would stop verifying.
|
|
24
|
+
// Those two stay on curve25519-js, and that is why it is still a dependency.
|
|
25
|
+
//
|
|
26
|
+
// The private half of generateKeyPair is clamped here rather than left as the
|
|
27
|
+
// seed. curve25519-js clamps it before returning, callers store what they are
|
|
28
|
+
// given, and a session written with one and read with the other would not
|
|
29
|
+
// agree. Node clamps internally at use time and hands back the raw seed, so
|
|
30
|
+
// the clamp has to be done explicitly to keep the returned pair identical.
|
|
31
|
+
//
|
|
32
|
+
// The API is curve25519-js's, unchanged, so this is a drop-in for it.
|
|
33
|
+
|
|
34
|
+
const crypto = require('crypto');
|
|
35
|
+
const curveJs = require('curve25519-js');
|
|
36
|
+
|
|
37
|
+
// The DER wrappers Node wants around a bare 32-byte X25519 key. Both are fixed
|
|
38
|
+
// prefixes — the algorithm identifier and the length header — so a raw key
|
|
39
|
+
// becomes an importable one by concatenation.
|
|
40
|
+
const PRIVATE_KEY_DER_PREFIX = Buffer.from([
|
|
41
|
+
48, 46, 2, 1, 0, 48, 5, 6, 3, 43, 101, 110, 4, 34, 4, 32
|
|
42
|
+
]);
|
|
43
|
+
const PUBLIC_KEY_DER_PREFIX = Buffer.from([
|
|
44
|
+
48, 42, 48, 5, 6, 3, 43, 101, 110, 3, 33, 0
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
// Whether this Node was built with X25519. Everything below falls back to the
|
|
48
|
+
// JavaScript implementation when it was not, so an unusual build keeps working
|
|
49
|
+
// rather than failing at the first handshake.
|
|
50
|
+
const NATIVE = (() => {
|
|
51
|
+
try {
|
|
52
|
+
if (typeof crypto.diffieHellman !== 'function') return false;
|
|
53
|
+
const seed = Buffer.alloc(32, 1);
|
|
54
|
+
_importPrivate(seed);
|
|
55
|
+
return true;
|
|
56
|
+
} catch (_) { return false; }
|
|
57
|
+
})();
|
|
58
|
+
|
|
59
|
+
function _importPrivate(raw32) {
|
|
60
|
+
return crypto.createPrivateKey({
|
|
61
|
+
key: Buffer.concat([PRIVATE_KEY_DER_PREFIX, raw32]),
|
|
62
|
+
format: 'der',
|
|
63
|
+
type: 'pkcs8'
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function _importPublic(raw32) {
|
|
68
|
+
return crypto.createPublicKey({
|
|
69
|
+
key: Buffer.concat([PUBLIC_KEY_DER_PREFIX, raw32]),
|
|
70
|
+
format: 'der',
|
|
71
|
+
type: 'spki'
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// The clamp RFC 7748 specifies for X25519 private scalars, applied to a copy so
|
|
76
|
+
// a caller's seed is never modified underneath it.
|
|
77
|
+
function clamp(seed) {
|
|
78
|
+
const k = Buffer.from(seed);
|
|
79
|
+
k[0] &= 248;
|
|
80
|
+
k[31] &= 127;
|
|
81
|
+
k[31] |= 64;
|
|
82
|
+
return k;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* A key pair from 32 bytes of seed.
|
|
87
|
+
*
|
|
88
|
+
* Returns the same shape and the same bytes as curve25519-js: the private half
|
|
89
|
+
* clamped, the public half derived from it.
|
|
90
|
+
*
|
|
91
|
+
* @param {Buffer|Uint8Array} seed 32 bytes
|
|
92
|
+
* @returns {{private: Buffer, public: Buffer}}
|
|
93
|
+
*/
|
|
94
|
+
function generateKeyPair(seed) {
|
|
95
|
+
if (!seed || seed.length !== 32) {
|
|
96
|
+
throw new Error('generateKeyPair: seed must be 32 bytes, got ' +
|
|
97
|
+
(seed ? seed.length : 'nothing'));
|
|
98
|
+
}
|
|
99
|
+
if (!NATIVE) {
|
|
100
|
+
const kp = curveJs.generateKeyPair(seed);
|
|
101
|
+
return { private: Buffer.from(kp.private), public: Buffer.from(kp.public) };
|
|
102
|
+
}
|
|
103
|
+
const priv = clamp(seed);
|
|
104
|
+
const pub = crypto.createPublicKey(_importPrivate(priv))
|
|
105
|
+
.export({ format: 'der', type: 'spki' })
|
|
106
|
+
.subarray(PUBLIC_KEY_DER_PREFIX.length);
|
|
107
|
+
return { private: priv, public: Buffer.from(pub) };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* The X25519 shared secret between our private key and their public key.
|
|
112
|
+
*
|
|
113
|
+
* @param {Buffer|Uint8Array} priv 32 bytes
|
|
114
|
+
* @param {Buffer|Uint8Array} pub 32 bytes
|
|
115
|
+
* @returns {Buffer} 32 bytes
|
|
116
|
+
*/
|
|
117
|
+
function sharedKey(priv, pub) {
|
|
118
|
+
if (!priv || priv.length !== 32) throw new Error('sharedKey: private key must be 32 bytes');
|
|
119
|
+
if (!pub || pub.length !== 32) throw new Error('sharedKey: public key must be 32 bytes');
|
|
120
|
+
if (!NATIVE) return Buffer.from(curveJs.sharedKey(priv, pub));
|
|
121
|
+
return crypto.diffieHellman({
|
|
122
|
+
privateKey: _importPrivate(Buffer.from(priv)),
|
|
123
|
+
publicKey: _importPublic(Buffer.from(pub))
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* XEdDSA signature. Not native — see the note at the top of this file.
|
|
129
|
+
*
|
|
130
|
+
* @returns {Buffer} 64 bytes
|
|
131
|
+
*/
|
|
132
|
+
function sign(priv, message) {
|
|
133
|
+
return Buffer.from(curveJs.sign(priv, message));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** XEdDSA verification. Not native — see the note at the top of this file. */
|
|
137
|
+
function verify(pub, message, signature) {
|
|
138
|
+
return curveJs.verify(pub, message, signature);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
module.exports = {
|
|
142
|
+
generateKeyPair,
|
|
143
|
+
sharedKey,
|
|
144
|
+
sign,
|
|
145
|
+
verify,
|
|
146
|
+
/** Whether Node's X25519 is being used. `false` means the JS fallback is. */
|
|
147
|
+
NATIVE
|
|
148
|
+
};
|
package/lib/hkdf.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// HKDF-SHA256, through Node's own OpenSSL.
|
|
4
|
+
//
|
|
5
|
+
// Every media key, every app-state key, every Noise handshake and every sender
|
|
6
|
+
// key goes through this, so it runs on nearly every operation the library
|
|
7
|
+
// performs. Measured over 20000 iterations at the 112-byte length the media
|
|
8
|
+
// path uses:
|
|
9
|
+
//
|
|
10
|
+
// @noble/hashes 20.99 us/op Node 9.38 us/op 2.2x
|
|
11
|
+
//
|
|
12
|
+
// Byte-identical over 300 random combinations of key material, salt and info
|
|
13
|
+
// at random lengths — checked in test/hkdf.test.js rather than assumed from
|
|
14
|
+
// both implementing RFC 5869.
|
|
15
|
+
//
|
|
16
|
+
// Two details this wrapper exists to get right:
|
|
17
|
+
//
|
|
18
|
+
// Node's hkdfSync returns an ArrayBuffer, not a Buffer. Handing one to code
|
|
19
|
+
// that expects a Buffer gives an object with no `.equals`, no `.subarray`
|
|
20
|
+
// returning a Buffer, and a `.length` that is `undefined` — mistakes that
|
|
21
|
+
// surface far from here. A Buffer is what comes back from this.
|
|
22
|
+
//
|
|
23
|
+
// The argument order is @noble's, so this is a drop-in for it, with the hash
|
|
24
|
+
// argument dropped since it is always SHA-256 here.
|
|
25
|
+
|
|
26
|
+
const crypto = require('crypto');
|
|
27
|
+
|
|
28
|
+
// Whether this Node has hkdfSync. It arrived in Node 15; the fallback keeps an
|
|
29
|
+
// older one working rather than failing at the first key derivation.
|
|
30
|
+
const NATIVE = typeof crypto.hkdfSync === 'function';
|
|
31
|
+
|
|
32
|
+
let _nobleHkdf = null, _nobleSha = null;
|
|
33
|
+
function _noble() {
|
|
34
|
+
if (!_nobleHkdf) {
|
|
35
|
+
_nobleHkdf = require('@noble/hashes/hkdf').hkdf;
|
|
36
|
+
_nobleSha = require('@noble/hashes/sha256').sha256;
|
|
37
|
+
}
|
|
38
|
+
return { hkdf: _nobleHkdf, sha256: _nobleSha };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Expand key material into `length` bytes.
|
|
43
|
+
*
|
|
44
|
+
* @param {Buffer|Uint8Array} ikm the input key material
|
|
45
|
+
* @param {Buffer|Uint8Array|string} salt may be empty
|
|
46
|
+
* @param {Buffer|Uint8Array|string} info may be empty
|
|
47
|
+
* @param {number} length how many bytes to produce
|
|
48
|
+
* @returns {Buffer}
|
|
49
|
+
*/
|
|
50
|
+
function hkdfSha256(ikm, salt, info, length) {
|
|
51
|
+
if (!NATIVE) {
|
|
52
|
+
const n = _noble();
|
|
53
|
+
return Buffer.from(n.hkdf(n.sha256, ikm, salt, info, length));
|
|
54
|
+
}
|
|
55
|
+
// Buffer.from(ArrayBuffer) is a view over the same memory rather than a copy,
|
|
56
|
+
// which is fine here because the ArrayBuffer is freshly allocated by
|
|
57
|
+
// hkdfSync and nothing else holds a reference to it.
|
|
58
|
+
return Buffer.from(crypto.hkdfSync('sha256', ikm, salt, info, length));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
module.exports = {
|
|
62
|
+
hkdfSha256,
|
|
63
|
+
/** Whether Node's HKDF is in use; `false` means the JavaScript fallback is. */
|
|
64
|
+
NATIVE
|
|
65
|
+
};
|
|
@@ -28,8 +28,7 @@
|
|
|
28
28
|
// the additional data.
|
|
29
29
|
|
|
30
30
|
const crypto = require('crypto');
|
|
31
|
-
const {
|
|
32
|
-
const { sha256 } = require('@noble/hashes/sha256');
|
|
31
|
+
const { hkdfSha256 } = require('../hkdf');
|
|
33
32
|
|
|
34
33
|
const RETRY_KEY_INFO = 'WhatsApp Media Retry Notification';
|
|
35
34
|
const IV_SIZE = 12;
|
|
@@ -89,7 +88,7 @@ function readFields(buf) {
|
|
|
89
88
|
function mediaRetryKey(mediaKey) {
|
|
90
89
|
const ikm = Buffer.isBuffer(mediaKey) ? mediaKey : Buffer.from(mediaKey);
|
|
91
90
|
return Buffer.from(
|
|
92
|
-
|
|
91
|
+
hkdfSha256(ikm, Buffer.alloc(0), Buffer.from(RETRY_KEY_INFO, 'utf8'), 32));
|
|
93
92
|
}
|
|
94
93
|
|
|
95
94
|
/**
|
|
@@ -117,7 +117,7 @@ function _encodeADVSignedIdentity(details, sigKey, acctSig, devSig) {
|
|
|
117
117
|
|
|
118
118
|
let _curve25519js = null;
|
|
119
119
|
function _getCurve() {
|
|
120
|
-
if (!_curve25519js) _curve25519js = require('
|
|
120
|
+
if (!_curve25519js) _curve25519js = require('../curve');
|
|
121
121
|
return _curve25519js;
|
|
122
122
|
}
|
|
123
123
|
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
// not observable on send.
|
|
22
22
|
|
|
23
23
|
const crypto = require('crypto');
|
|
24
|
-
const {
|
|
24
|
+
const { hkdfSha256 } = require('../hkdf');
|
|
25
25
|
const { sha256 } = require('@noble/hashes/sha256');
|
|
26
26
|
|
|
27
27
|
// Protobuf field numbers on Message that the token is computed over.
|
|
@@ -224,7 +224,7 @@ function generateMsgSecretKey(modificationType, origMsgId, origMsgSender, modifi
|
|
|
224
224
|
Buffer.from(String(modificationSender), 'utf8'),
|
|
225
225
|
Buffer.from(modificationType, 'utf8')
|
|
226
226
|
]);
|
|
227
|
-
return
|
|
227
|
+
return hkdfSha256(origMsgSecret, Buffer.alloc(32), useCaseSecret, 32);
|
|
228
228
|
}
|
|
229
229
|
|
|
230
230
|
// ─── reading what we need out of the encoded message ─────────────────────────
|
package/lib/noise.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const net = require('net');
|
|
4
|
+
const { hkdfSha256 } = require('./hkdf');
|
|
4
5
|
const crypto = require('crypto');
|
|
5
6
|
const EventEmitter = require('events');
|
|
6
|
-
const curveJs = require('
|
|
7
|
+
const curveJs = require('./curve');
|
|
7
8
|
const { sha256 } = require('@noble/hashes/sha256');
|
|
8
|
-
const { hkdf } = require('@noble/hashes/hkdf');
|
|
9
9
|
const { MOBILE_PROLOGUE, WEB_PROLOGUE, WHATSAPP_HOST, WHATSAPP_PORT,
|
|
10
10
|
platformForOs } = require('./constants');
|
|
11
11
|
const { encodeHandshakeClientHello, encodeHandshakeClientFinish,
|
|
@@ -224,7 +224,7 @@ function aesgcmDecrypt(key, counter, ciphertext, aad) {
|
|
|
224
224
|
}
|
|
225
225
|
|
|
226
226
|
function deriveHkdf(salt, ikm, length) {
|
|
227
|
-
return
|
|
227
|
+
return hkdfSha256(ikm, salt, new Uint8Array(0), length);
|
|
228
228
|
}
|
|
229
229
|
|
|
230
230
|
class NoiseState {
|
|
@@ -612,6 +612,80 @@ function _unwrap(envelope, innerField) {
|
|
|
612
612
|
} catch (_) { return null; }
|
|
613
613
|
}
|
|
614
614
|
|
|
615
|
+
// ─── ContextInfo, on the way in ──────────────────────────────────────────────
|
|
616
|
+
//
|
|
617
|
+
// Every message that can be a reply carries one, always at field 17 of its own
|
|
618
|
+
// content field — and field 7 for a group invite, which numbers its own fields.
|
|
619
|
+
// It is what says the message quotes another, and what the quoted message was:
|
|
620
|
+
//
|
|
621
|
+
// ContextInfo { stanzaId=1, participant=2, quotedMessage=3, remoteJid=4,
|
|
622
|
+
// mentionedJid=15 }
|
|
623
|
+
//
|
|
624
|
+
// Without it a reply is indistinguishable from a message sent on its own, so
|
|
625
|
+
// anything that acts on what was quoted — re-sending a view-once, turning a
|
|
626
|
+
// photo into a sticker — has nothing to act on.
|
|
627
|
+
const CONTEXT_BEARING_FIELDS = [
|
|
628
|
+
[6, 17], // extendedTextMessage
|
|
629
|
+
[3, 17], // imageMessage
|
|
630
|
+
[4, 17], // contactMessage
|
|
631
|
+
[5, 17], // locationMessage
|
|
632
|
+
[7, 17], // documentMessage
|
|
633
|
+
[8, 17], // audioMessage
|
|
634
|
+
[9, 17], // videoMessage
|
|
635
|
+
[26, 17], // stickerMessage
|
|
636
|
+
[49, 17], // pollCreationMessage
|
|
637
|
+
[28, 7] // groupInviteMessage — its contextInfo is field 7
|
|
638
|
+
];
|
|
639
|
+
|
|
640
|
+
function decodeContextInfo(buf, depth) {
|
|
641
|
+
if (!buf || !Buffer.isBuffer(buf)) return null;
|
|
642
|
+
try {
|
|
643
|
+
const c = _decodeFields(buf);
|
|
644
|
+
const out = {};
|
|
645
|
+
|
|
646
|
+
const stanzaId = _str(c[1]);
|
|
647
|
+
if (stanzaId) out.stanzaId = stanzaId;
|
|
648
|
+
const participant = _str(c[2]);
|
|
649
|
+
if (participant) out.participant = participant;
|
|
650
|
+
const remoteJid = _str(c[4]);
|
|
651
|
+
if (remoteJid) out.remoteJid = remoteJid;
|
|
652
|
+
|
|
653
|
+
// Mentions are a repeated string, so one mention decodes to a bare value
|
|
654
|
+
// rather than to a list of one.
|
|
655
|
+
if (c[15] != null) {
|
|
656
|
+
const raw = Array.isArray(c[15]) ? c[15] : [c[15]];
|
|
657
|
+
const jids = raw.map(_str).filter(Boolean);
|
|
658
|
+
if (jids.length) out.mentionedJid = jids;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// The quoted message is a Message of its own, decoded the same way as any
|
|
662
|
+
// other. The depth bound is shared with the envelope unwrapping so a chain
|
|
663
|
+
// of replies quoting replies cannot recurse without end.
|
|
664
|
+
if (Buffer.isBuffer(c[3]) && c[3].length && (depth || 0) < MAX_WRAPPER_DEPTH) {
|
|
665
|
+
out.quotedMessageRaw = c[3];
|
|
666
|
+
const quoted = decodeMessageContainer(c[3], (depth || 0) + 1);
|
|
667
|
+
if (quoted && quoted.type !== 'unknown') out.quotedMessage = quoted;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
return Object.keys(out).length ? out : null;
|
|
671
|
+
} catch (_) { return null; }
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// Whichever content field the message turned out to be, its contextInfo.
|
|
675
|
+
function _contextInfoOf(f, depth) {
|
|
676
|
+
for (const [contentField, ctxField] of CONTEXT_BEARING_FIELDS) {
|
|
677
|
+
const content = f[contentField];
|
|
678
|
+
if (!Buffer.isBuffer(content)) continue;
|
|
679
|
+
try {
|
|
680
|
+
const inner = _decodeFields(content)[ctxField];
|
|
681
|
+
if (!Buffer.isBuffer(inner) || !inner.length) continue;
|
|
682
|
+
const ctx = decodeContextInfo(inner, depth);
|
|
683
|
+
if (ctx) return ctx;
|
|
684
|
+
} catch (_) { /* try the next one */ }
|
|
685
|
+
}
|
|
686
|
+
return null;
|
|
687
|
+
}
|
|
688
|
+
|
|
615
689
|
function decodeMessageContainer(buf, depth) {
|
|
616
690
|
if (!buf || buf.length === 0) return { type: 'unknown' };
|
|
617
691
|
try {
|
|
@@ -953,6 +1027,13 @@ function decodeMessageContainer(buf, depth) {
|
|
|
953
1027
|
msgResult = Object.assign({}, msgResult, { encKey: messageSecret });
|
|
954
1028
|
}
|
|
955
1029
|
if (skdmInfo && !msgResult.skdm) msgResult = Object.assign({}, msgResult, { skdm: skdmInfo });
|
|
1030
|
+
// What this message was a reply to, if anything. Read at the level that
|
|
1031
|
+
// actually carried the content, so an envelope does not overwrite the
|
|
1032
|
+
// context the message inside it already found.
|
|
1033
|
+
if (!msgResult.contextInfo) {
|
|
1034
|
+
const ctx = _contextInfoOf(f, level);
|
|
1035
|
+
if (ctx) msgResult = Object.assign({}, msgResult, { contextInfo: ctx });
|
|
1036
|
+
}
|
|
956
1037
|
return msgResult;
|
|
957
1038
|
}
|
|
958
1039
|
|
|
@@ -1025,5 +1106,6 @@ module.exports = {
|
|
|
1025
1106
|
PROTOCOL_MSG_EPHEMERAL,
|
|
1026
1107
|
GROUP_INVITE_TYPE_DEFAULT,
|
|
1027
1108
|
GROUP_INVITE_TYPE_PARENT,
|
|
1028
|
-
decodeMessageContainer
|
|
1109
|
+
decodeMessageContainer,
|
|
1110
|
+
decodeContextInfo
|
|
1029
1111
|
};
|
package/lib/signal/SenderKey.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { dbg: _whaDbg } = require('../logger');
|
|
4
|
+
const { hkdfSha256 } = require('../hkdf');
|
|
4
5
|
|
|
5
6
|
const crypto = require('crypto');
|
|
6
7
|
const fs = require('fs');
|
|
7
8
|
const path = require('path');
|
|
8
|
-
const { hkdf } = require('@noble/hashes/hkdf');
|
|
9
9
|
const { sha256 } = require('@noble/hashes/sha256');
|
|
10
10
|
const { hmac } = require('@noble/hashes/hmac');
|
|
11
|
-
const curveJs = require('
|
|
11
|
+
const curveJs = require('../curve');
|
|
12
12
|
|
|
13
13
|
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
14
14
|
|
|
@@ -63,7 +63,7 @@ function advanceChain(chainKey) {
|
|
|
63
63
|
function deriveMessageKeys(messageKey) {
|
|
64
64
|
const salt = Buffer.alloc(32);
|
|
65
65
|
const info = Buffer.from('WhisperGroup');
|
|
66
|
-
const derived =
|
|
66
|
+
const derived = hkdfSha256(messageKey, salt, info, 48);
|
|
67
67
|
return {
|
|
68
68
|
iv: derived.slice(0, 16),
|
|
69
69
|
cipherKey: derived.slice(16, 48)
|
|
@@ -3,7 +3,7 @@ const { dbg: _whaDbg, warn: _whaWarn, err: _whaErr, info: _whaInfo } = require('
|
|
|
3
3
|
|
|
4
4
|
'use strict';
|
|
5
5
|
|
|
6
|
-
const curveJs = require('
|
|
6
|
+
const curveJs = require('../../curve');
|
|
7
7
|
const nodeCrypto = require('crypto');
|
|
8
8
|
// from: https://github.com/digitalbazaar/x25519-key-agreement-key-2019/blob/master/lib/crypto.js
|
|
9
9
|
const PUBLIC_KEY_DER_PREFIX = Buffer.from([
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "whalibmob",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.28.0",
|
|
4
4
|
"description": "Node.js library for WhatsApp — register a number over SMS, or link as a companion by QR. Signal E2E encryption, media, groups, channels.",
|
|
5
5
|
"author": "Kunboruto20",
|
|
6
6
|
"main": "index.js",
|