@ryuu-reinzz/baileys 3.0.0-beta.2 → 3.0.0-beta.21
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/LICENSE +1 -1
- package/README.md +6 -0
- package/WAProto/fix-imports.js +70 -18
- package/WAProto/index.js +197 -160
- package/lib/Defaults/index.js +17 -4
- package/lib/Signal/libsignal.js +63 -2
- package/lib/Signal/lid-mapping.js +170 -70
- package/lib/Socket/Client/websocket.js +5 -1
- package/lib/Socket/business.js +11 -8
- package/lib/Socket/chats.js +55 -28
- package/lib/Socket/index.js +0 -6
- package/lib/Socket/messages-recv.js +152 -75
- package/lib/Socket/messages-send.js +230 -148
- package/lib/Socket/socket.js +69 -15
- package/lib/Utils/auth-utils.js +53 -20
- package/lib/Utils/chat-utils.js +100 -51
- package/lib/Utils/crypto.js +2 -26
- package/lib/Utils/event-buffer.js +33 -7
- package/lib/Utils/generics.js +4 -1
- package/lib/Utils/history.js +46 -5
- package/lib/Utils/identity-change-handler.js +49 -0
- package/lib/Utils/index.js +2 -0
- package/lib/Utils/lt-hash.js +2 -42
- package/lib/Utils/make-mutex.js +20 -27
- package/lib/Utils/message-retry-manager.js +58 -5
- package/lib/Utils/messages-media.js +151 -40
- package/lib/Utils/messages.js +43 -23
- package/lib/Utils/noise-handler.js +139 -85
- package/lib/Utils/process-message.js +57 -14
- package/lib/Utils/reporting-utils.js +258 -0
- package/lib/Utils/sync-action-utils.js +48 -0
- package/lib/Utils/tc-token-utils.js +18 -0
- package/lib/Utils/use-sqlite-auth-state.js +122 -0
- package/lib/WABinary/decode.js +24 -0
- package/lib/WABinary/encode.js +5 -1
- package/lib/WABinary/generic-utils.js +19 -8
- package/package.json +7 -2
package/lib/Socket/socket.js
CHANGED
|
@@ -3,7 +3,7 @@ import { randomBytes } from 'crypto';
|
|
|
3
3
|
import { URL } from 'url';
|
|
4
4
|
import { promisify } from 'util';
|
|
5
5
|
import { proto } from '../../WAProto/index.js';
|
|
6
|
-
import { DEF_CALLBACK_PREFIX, DEF_TAG_PREFIX, INITIAL_PREKEY_COUNT, MIN_PREKEY_COUNT, MIN_UPLOAD_INTERVAL, NOISE_WA_HEADER, UPLOAD_TIMEOUT } from '../Defaults/index.js';
|
|
6
|
+
import { DEF_CALLBACK_PREFIX, DEF_TAG_PREFIX, INITIAL_PREKEY_COUNT, MIN_PREKEY_COUNT, MIN_UPLOAD_INTERVAL, NOISE_WA_HEADER, PROCESSABLE_HISTORY_TYPES, TimeMs, UPLOAD_TIMEOUT } from '../Defaults/index.js';
|
|
7
7
|
import { DisconnectReason } from '../Types/index.js';
|
|
8
8
|
import { addTransactionCapability, aesEncryptCTR, bindWaitForConnectionUpdate, bytesToCrockford, configureSuccessfulPairing, Curve, derivePairingCodeKey, generateLoginNode, generateMdTagPrefix, generateRegistrationNode, getCodeFromWSError, getErrorCodeFromStreamError, getNextPreKeysNode, makeEventBuffer, makeNoiseHandler, promiseTimeout, signedKeyPair, xmppSignedPreKey } from '../Utils/index.js';
|
|
9
9
|
import { getPlatformId } from '../Utils/browser-utils.js';
|
|
@@ -20,10 +20,16 @@ import { WebSocketClient } from './Client/index.js';
|
|
|
20
20
|
export const makeSocket = (config) => {
|
|
21
21
|
const { waWebSocketUrl, connectTimeoutMs, logger, keepAliveIntervalMs, browser, auth: authState, printQRInTerminal, defaultQueryTimeoutMs, transactionOpts, qrTimeout, makeSignalRepository } = config;
|
|
22
22
|
const publicWAMBuffer = new BinaryInfo();
|
|
23
|
+
let serverTimeOffsetMs = 0;
|
|
23
24
|
const uqTagId = generateMdTagPrefix();
|
|
24
25
|
const generateMessageTag = () => `${uqTagId}${epoch++}`;
|
|
25
26
|
if (printQRInTerminal) {
|
|
26
|
-
|
|
27
|
+
logger.warn({}, '⚠️ The printQRInTerminal option has been deprecated. You will no longer receive QR codes in the terminal automatically. Please listen to the connection.update event yourself and handle the QR your way. You can remove this message by removing this opttion. This message will be removed in a future version.');
|
|
28
|
+
}
|
|
29
|
+
const syncDisabled = PROCESSABLE_HISTORY_TYPES.map(syncType => config.shouldSyncHistoryMessage({ syncType })).filter(x => x === false)
|
|
30
|
+
.length === PROCESSABLE_HISTORY_TYPES.length;
|
|
31
|
+
if (syncDisabled) {
|
|
32
|
+
logger.warn('⚠️ DANGER: DISABLING ALL SYNC BY shouldSyncHistoryMsg PREVENTS BAILEYS FROM ACCESSING INITIAL LID MAPPINGS, LEADING TO INSTABILIY AND SESSION ERRORS');
|
|
27
33
|
}
|
|
28
34
|
const url = typeof waWebSocketUrl === 'string' ? new URL(waWebSocketUrl) : waWebSocketUrl;
|
|
29
35
|
if (config.mobile || url.protocol === 'tcp:') {
|
|
@@ -304,7 +310,7 @@ export const makeSocket = (config) => {
|
|
|
304
310
|
const result = await awaitNextMessage(init);
|
|
305
311
|
const handshake = proto.HandshakeMessage.decode(result);
|
|
306
312
|
logger.trace({ handshake }, 'handshake recv from WA');
|
|
307
|
-
const keyEnc =
|
|
313
|
+
const keyEnc = noise.processHandshake(handshake, creds.noiseKey);
|
|
308
314
|
let node;
|
|
309
315
|
if (!creds.me) {
|
|
310
316
|
node = generateRegistrationNode(creds, config);
|
|
@@ -468,7 +474,7 @@ export const makeSocket = (config) => {
|
|
|
468
474
|
}
|
|
469
475
|
});
|
|
470
476
|
};
|
|
471
|
-
const end = (error) => {
|
|
477
|
+
const end = async (error) => {
|
|
472
478
|
if (closed) {
|
|
473
479
|
logger.trace({ trace: error?.stack }, 'connection already closed');
|
|
474
480
|
return;
|
|
@@ -482,7 +488,7 @@ export const makeSocket = (config) => {
|
|
|
482
488
|
ws.removeAllListeners('message');
|
|
483
489
|
if (!ws.isClosed && !ws.isClosing) {
|
|
484
490
|
try {
|
|
485
|
-
ws.close();
|
|
491
|
+
await ws.close();
|
|
486
492
|
}
|
|
487
493
|
catch { }
|
|
488
494
|
}
|
|
@@ -526,7 +532,7 @@ export const makeSocket = (config) => {
|
|
|
526
532
|
it could be that the network is down
|
|
527
533
|
*/
|
|
528
534
|
if (diff > keepAliveIntervalMs + 5000) {
|
|
529
|
-
end(new Boom('Connection was lost', { statusCode: DisconnectReason.connectionLost }));
|
|
535
|
+
void end(new Boom('Connection was lost', { statusCode: DisconnectReason.connectionLost }));
|
|
530
536
|
}
|
|
531
537
|
else if (ws.isOpen) {
|
|
532
538
|
// if its all good, send a keep alive request
|
|
@@ -580,7 +586,7 @@ export const makeSocket = (config) => {
|
|
|
580
586
|
]
|
|
581
587
|
});
|
|
582
588
|
}
|
|
583
|
-
end(new Boom(msg || 'Intentional Logout', { statusCode: DisconnectReason.loggedOut }));
|
|
589
|
+
void end(new Boom(msg || 'Intentional Logout', { statusCode: DisconnectReason.loggedOut }));
|
|
584
590
|
};
|
|
585
591
|
const requestPairingCode = async (phoneNumber, customPairingCode) => {
|
|
586
592
|
const pairingCode = customPairingCode ?? bytesToCrockford(randomBytes(5));
|
|
@@ -672,13 +678,13 @@ export const makeSocket = (config) => {
|
|
|
672
678
|
}
|
|
673
679
|
catch (err) {
|
|
674
680
|
logger.error({ err }, 'error in validating connection');
|
|
675
|
-
end(err);
|
|
681
|
+
void end(err);
|
|
676
682
|
}
|
|
677
683
|
});
|
|
678
684
|
ws.on('error', mapWebSocketError(end));
|
|
679
|
-
ws.on('close', () => end(new Boom('Connection Terminated', { statusCode: DisconnectReason.connectionClosed })));
|
|
685
|
+
ws.on('close', () => void end(new Boom('Connection Terminated', { statusCode: DisconnectReason.connectionClosed })));
|
|
680
686
|
// the server terminated the connection
|
|
681
|
-
ws.on('CB:xmlstreamend', () => end(new Boom('Connection Terminated by Server', { statusCode: DisconnectReason.connectionClosed })));
|
|
687
|
+
ws.on('CB:xmlstreamend', () => void end(new Boom('Connection Terminated by Server', { statusCode: DisconnectReason.connectionClosed })));
|
|
682
688
|
// QR gen
|
|
683
689
|
ws.on('CB:iq,type:set,pair-device', async (stanza) => {
|
|
684
690
|
const iq = {
|
|
@@ -702,7 +708,7 @@ export const makeSocket = (config) => {
|
|
|
702
708
|
}
|
|
703
709
|
const refNode = refNodes.shift();
|
|
704
710
|
if (!refNode) {
|
|
705
|
-
end(new Boom('QR refs attempts ended', { statusCode: DisconnectReason.timedOut }));
|
|
711
|
+
void end(new Boom('QR refs attempts ended', { statusCode: DisconnectReason.timedOut }));
|
|
706
712
|
return;
|
|
707
713
|
}
|
|
708
714
|
const ref = refNode.content.toString('utf-8');
|
|
@@ -718,20 +724,23 @@ export const makeSocket = (config) => {
|
|
|
718
724
|
ws.on('CB:iq,,pair-success', async (stanza) => {
|
|
719
725
|
logger.debug('pair success recv');
|
|
720
726
|
try {
|
|
727
|
+
updateServerTimeOffset(stanza);
|
|
721
728
|
const { reply, creds: updatedCreds } = configureSuccessfulPairing(stanza, creds);
|
|
722
729
|
logger.info({ me: updatedCreds.me, platform: updatedCreds.platform }, 'pairing configured successfully, expect to restart the connection...');
|
|
723
730
|
ev.emit('creds.update', updatedCreds);
|
|
724
731
|
ev.emit('connection.update', { isNewLogin: true, qr: undefined });
|
|
725
732
|
await sendNode(reply);
|
|
733
|
+
void sendUnifiedSession();
|
|
726
734
|
}
|
|
727
735
|
catch (error) {
|
|
728
736
|
logger.info({ trace: error.stack }, 'error in pairing');
|
|
729
|
-
end(error);
|
|
737
|
+
void end(error);
|
|
730
738
|
}
|
|
731
739
|
});
|
|
732
740
|
// login complete
|
|
733
741
|
ws.on('CB:success', async (node) => {
|
|
734
742
|
try {
|
|
743
|
+
updateServerTimeOffset(node);
|
|
735
744
|
await uploadPreKeysToServerIfRequired();
|
|
736
745
|
await sendPassiveIq('active');
|
|
737
746
|
// After successful login, validate our key-bundle against server
|
|
@@ -749,6 +758,7 @@ export const makeSocket = (config) => {
|
|
|
749
758
|
clearTimeout(qrTimer); // will never happen in all likelyhood -- but just in case WA sends success on first try
|
|
750
759
|
ev.emit('creds.update', { me: { ...authState.creds.me, lid: node.attrs.lid } });
|
|
751
760
|
ev.emit('connection.update', { connection: 'open' });
|
|
761
|
+
void sendUnifiedSession();
|
|
752
762
|
if (node.attrs.lid && authState.creds.me?.id) {
|
|
753
763
|
const myLID = node.attrs.lid;
|
|
754
764
|
process.nextTick(async () => {
|
|
@@ -777,15 +787,15 @@ export const makeSocket = (config) => {
|
|
|
777
787
|
const [reasonNode] = getAllBinaryNodeChildren(node);
|
|
778
788
|
logger.error({ reasonNode, fullErrorNode: node }, 'stream errored out');
|
|
779
789
|
const { reason, statusCode } = getErrorCodeFromStreamError(node);
|
|
780
|
-
end(new Boom(`Stream Errored (${reason})`, { statusCode, data: reasonNode || node }));
|
|
790
|
+
void end(new Boom(`Stream Errored (${reason})`, { statusCode, data: reasonNode || node }));
|
|
781
791
|
});
|
|
782
792
|
// stream fail, possible logout
|
|
783
793
|
ws.on('CB:failure', (node) => {
|
|
784
794
|
const reason = +(node.attrs.reason || 500);
|
|
785
|
-
end(new Boom('Connection Failure', { statusCode: reason, data: node.attrs }));
|
|
795
|
+
void end(new Boom('Connection Failure', { statusCode: reason, data: node.attrs }));
|
|
786
796
|
});
|
|
787
797
|
ws.on('CB:ib,,downgrade_webclient', () => {
|
|
788
|
-
end(new Boom('Multi-device beta not joined', { statusCode: DisconnectReason.multideviceMismatch }));
|
|
798
|
+
void end(new Boom('Multi-device beta not joined', { statusCode: DisconnectReason.multideviceMismatch }));
|
|
789
799
|
});
|
|
790
800
|
ws.on('CB:ib,,offline_preview', async (node) => {
|
|
791
801
|
logger.info('offline preview received', JSON.stringify(node));
|
|
@@ -839,6 +849,48 @@ export const makeSocket = (config) => {
|
|
|
839
849
|
}
|
|
840
850
|
Object.assign(creds, update);
|
|
841
851
|
});
|
|
852
|
+
const updateServerTimeOffset = ({ attrs }) => {
|
|
853
|
+
const tValue = attrs?.t;
|
|
854
|
+
if (!tValue) {
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
const parsed = Number(tValue);
|
|
858
|
+
if (Number.isNaN(parsed) || parsed <= 0) {
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
const localMs = Date.now();
|
|
862
|
+
serverTimeOffsetMs = parsed * 1000 - localMs;
|
|
863
|
+
logger.debug({ offset: serverTimeOffsetMs }, 'calculated server time offset');
|
|
864
|
+
};
|
|
865
|
+
const getUnifiedSessionId = () => {
|
|
866
|
+
const offsetMs = 3 * TimeMs.Day;
|
|
867
|
+
const now = Date.now() + serverTimeOffsetMs;
|
|
868
|
+
const id = (now + offsetMs) % TimeMs.Week;
|
|
869
|
+
return id.toString();
|
|
870
|
+
};
|
|
871
|
+
const sendUnifiedSession = async () => {
|
|
872
|
+
if (!ws.isOpen) {
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
const node = {
|
|
876
|
+
tag: 'ib',
|
|
877
|
+
attrs: {},
|
|
878
|
+
content: [
|
|
879
|
+
{
|
|
880
|
+
tag: 'unified_session',
|
|
881
|
+
attrs: {
|
|
882
|
+
id: getUnifiedSessionId()
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
]
|
|
886
|
+
};
|
|
887
|
+
try {
|
|
888
|
+
await sendNode(node);
|
|
889
|
+
}
|
|
890
|
+
catch (error) {
|
|
891
|
+
logger.debug({ error }, 'failed to send unified_session telemetry');
|
|
892
|
+
}
|
|
893
|
+
};
|
|
842
894
|
return {
|
|
843
895
|
type: 'md',
|
|
844
896
|
ws,
|
|
@@ -862,6 +914,8 @@ export const makeSocket = (config) => {
|
|
|
862
914
|
digestKeyBundle,
|
|
863
915
|
rotateSignedPreKey,
|
|
864
916
|
requestPairingCode,
|
|
917
|
+
updateServerTimeOffset,
|
|
918
|
+
sendUnifiedSession,
|
|
865
919
|
wamBuffer: publicWAMBuffer,
|
|
866
920
|
/** Waits for the connection to WA to reach a state */
|
|
867
921
|
waitForConnectionUpdate: bindWaitForConnectionUpdate(ev),
|
package/lib/Utils/auth-utils.js
CHANGED
|
@@ -46,6 +46,7 @@ export function makeCacheableSignalKeyStore(store, logger, _cache) {
|
|
|
46
46
|
const item = fetched[id];
|
|
47
47
|
if (item) {
|
|
48
48
|
data[id] = item;
|
|
49
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
|
|
49
50
|
await cache.set(getUniqueId(type, id), item);
|
|
50
51
|
}
|
|
51
52
|
}
|
|
@@ -81,9 +82,11 @@ export function makeCacheableSignalKeyStore(store, logger, _cache) {
|
|
|
81
82
|
*/
|
|
82
83
|
export const addTransactionCapability = (state, logger, { maxCommitRetries, delayBetweenTriesMs }) => {
|
|
83
84
|
const txStorage = new AsyncLocalStorage();
|
|
84
|
-
// Queues for concurrency control
|
|
85
|
+
// Queues for concurrency control (keyed by signal data type - bounded set)
|
|
85
86
|
const keyQueues = new Map();
|
|
87
|
+
// Transaction mutexes with reference counting for cleanup
|
|
86
88
|
const txMutexes = new Map();
|
|
89
|
+
const txMutexRefCounts = new Map();
|
|
87
90
|
// Pre-key manager for specialized operations
|
|
88
91
|
const preKeyManager = new PreKeyManager(state, logger);
|
|
89
92
|
/**
|
|
@@ -101,9 +104,32 @@ export const addTransactionCapability = (state, logger, { maxCommitRetries, dela
|
|
|
101
104
|
function getTxMutex(key) {
|
|
102
105
|
if (!txMutexes.has(key)) {
|
|
103
106
|
txMutexes.set(key, new Mutex());
|
|
107
|
+
txMutexRefCounts.set(key, 0);
|
|
104
108
|
}
|
|
105
109
|
return txMutexes.get(key);
|
|
106
110
|
}
|
|
111
|
+
/**
|
|
112
|
+
* Acquire a reference to a transaction mutex
|
|
113
|
+
*/
|
|
114
|
+
function acquireTxMutexRef(key) {
|
|
115
|
+
const count = txMutexRefCounts.get(key) ?? 0;
|
|
116
|
+
txMutexRefCounts.set(key, count + 1);
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Release a reference to a transaction mutex and cleanup if no longer needed
|
|
120
|
+
*/
|
|
121
|
+
function releaseTxMutexRef(key) {
|
|
122
|
+
const count = (txMutexRefCounts.get(key) ?? 1) - 1;
|
|
123
|
+
txMutexRefCounts.set(key, count);
|
|
124
|
+
// Cleanup if no more references and mutex is not locked
|
|
125
|
+
if (count <= 0) {
|
|
126
|
+
const mutex = txMutexes.get(key);
|
|
127
|
+
if (mutex && !mutex.isLocked()) {
|
|
128
|
+
txMutexes.delete(key);
|
|
129
|
+
txMutexRefCounts.delete(key);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
107
133
|
/**
|
|
108
134
|
* Check if currently in a transaction
|
|
109
135
|
*/
|
|
@@ -209,25 +235,32 @@ export const addTransactionCapability = (state, logger, { maxCommitRetries, dela
|
|
|
209
235
|
return work();
|
|
210
236
|
}
|
|
211
237
|
// New transaction - acquire mutex and create context
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
238
|
+
const mutex = getTxMutex(key);
|
|
239
|
+
acquireTxMutexRef(key);
|
|
240
|
+
try {
|
|
241
|
+
return await mutex.runExclusive(async () => {
|
|
242
|
+
const ctx = {
|
|
243
|
+
cache: {},
|
|
244
|
+
mutations: {},
|
|
245
|
+
dbQueries: 0
|
|
246
|
+
};
|
|
247
|
+
logger.trace('entering transaction');
|
|
248
|
+
try {
|
|
249
|
+
const result = await txStorage.run(ctx, work);
|
|
250
|
+
// Commit mutations
|
|
251
|
+
await commitWithRetry(ctx.mutations);
|
|
252
|
+
logger.trace({ dbQueries: ctx.dbQueries }, 'transaction completed');
|
|
253
|
+
return result;
|
|
254
|
+
}
|
|
255
|
+
catch (error) {
|
|
256
|
+
logger.error({ error }, 'transaction failed, rolling back');
|
|
257
|
+
throw error;
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
finally {
|
|
262
|
+
releaseTxMutexRef(key);
|
|
263
|
+
}
|
|
231
264
|
}
|
|
232
265
|
};
|
|
233
266
|
};
|
package/lib/Utils/chat-utils.js
CHANGED
|
@@ -1,41 +1,37 @@
|
|
|
1
1
|
import { Boom } from '@hapi/boom';
|
|
2
|
+
import { expandAppStateKeys } from 'whatsapp-rust-bridge';
|
|
2
3
|
import { proto } from '../../WAProto/index.js';
|
|
3
4
|
import { LabelAssociationType } from '../Types/LabelAssociation.js';
|
|
4
5
|
import { getBinaryNodeChild, getBinaryNodeChildren, isJidGroup, jidNormalizedUser } from '../WABinary/index.js';
|
|
5
|
-
import { aesDecrypt, aesEncrypt,
|
|
6
|
+
import { aesDecrypt, aesEncrypt, hmacSign } from './crypto.js';
|
|
6
7
|
import { toNumber } from './generics.js';
|
|
7
8
|
import { LT_HASH_ANTI_TAMPERING } from './lt-hash.js';
|
|
8
9
|
import { downloadContentFromMessage } from './messages-media.js';
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
import { emitSyncActionResults, processContactAction } from './sync-action-utils.js';
|
|
11
|
+
const mutationKeys = (keydata) => {
|
|
12
|
+
const keys = expandAppStateKeys(keydata);
|
|
11
13
|
return {
|
|
12
|
-
indexKey:
|
|
13
|
-
valueEncryptionKey:
|
|
14
|
-
valueMacKey:
|
|
15
|
-
snapshotMacKey:
|
|
16
|
-
patchMacKey:
|
|
14
|
+
indexKey: keys.indexKey,
|
|
15
|
+
valueEncryptionKey: keys.valueEncryptionKey,
|
|
16
|
+
valueMacKey: keys.valueMacKey,
|
|
17
|
+
snapshotMacKey: keys.snapshotMacKey,
|
|
18
|
+
patchMacKey: keys.patchMacKey
|
|
17
19
|
};
|
|
18
20
|
};
|
|
19
21
|
const generateMac = (operation, data, keyId, key) => {
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
return Buffer.concat([buff, Buffer.from(keyId, 'base64')]);
|
|
32
|
-
};
|
|
33
|
-
const keyData = getKeyData();
|
|
34
|
-
const last = Buffer.alloc(8); // 8 bytes
|
|
35
|
-
last.set([keyData.length], last.length - 1);
|
|
36
|
-
const total = Buffer.concat([keyData, data, last]);
|
|
22
|
+
const opByte = operation === proto.SyncdMutation.SyncdOperation.SET ? 0x01 : 0x02;
|
|
23
|
+
const keyIdBuffer = typeof keyId === 'string' ? Buffer.from(keyId, 'base64') : keyId;
|
|
24
|
+
const keyData = new Uint8Array(1 + keyIdBuffer.length);
|
|
25
|
+
keyData[0] = opByte;
|
|
26
|
+
keyData.set(keyIdBuffer, 1);
|
|
27
|
+
const last = new Uint8Array(8);
|
|
28
|
+
last[7] = keyData.length;
|
|
29
|
+
const total = new Uint8Array(keyData.length + data.length + last.length);
|
|
30
|
+
total.set(keyData, 0);
|
|
31
|
+
total.set(data, keyData.length);
|
|
32
|
+
total.set(last, keyData.length + data.length);
|
|
37
33
|
const hmac = hmacSign(total, key, 'sha512');
|
|
38
|
-
return hmac.
|
|
34
|
+
return hmac.subarray(0, 32);
|
|
39
35
|
};
|
|
40
36
|
const to64BitNetworkOrder = (e) => {
|
|
41
37
|
const buff = Buffer.alloc(8);
|
|
@@ -58,20 +54,18 @@ const makeLtHashGenerator = ({ indexValueMap, hash }) => {
|
|
|
58
54
|
delete indexValueMap[indexMacBase64];
|
|
59
55
|
}
|
|
60
56
|
else {
|
|
61
|
-
addBuffs.push(
|
|
57
|
+
addBuffs.push(valueMac);
|
|
62
58
|
// add this index into the history map
|
|
63
59
|
indexValueMap[indexMacBase64] = { valueMac };
|
|
64
60
|
}
|
|
65
61
|
if (prevOp) {
|
|
66
|
-
subBuffs.push(
|
|
62
|
+
subBuffs.push(prevOp.valueMac);
|
|
67
63
|
}
|
|
68
64
|
},
|
|
69
|
-
finish:
|
|
70
|
-
const
|
|
71
|
-
const result = await LT_HASH_ANTI_TAMPERING.subtractThenAdd(hashArrayBuffer, addBuffs, subBuffs);
|
|
72
|
-
const buffer = Buffer.from(result);
|
|
65
|
+
finish: () => {
|
|
66
|
+
const result = LT_HASH_ANTI_TAMPERING.subtractThenAdd(hash, subBuffs, addBuffs);
|
|
73
67
|
return {
|
|
74
|
-
hash:
|
|
68
|
+
hash: Buffer.from(result),
|
|
75
69
|
indexValueMap
|
|
76
70
|
};
|
|
77
71
|
}
|
|
@@ -101,14 +95,14 @@ export const encodeSyncdPatch = async ({ type, index, syncAction, apiVersion, op
|
|
|
101
95
|
version: apiVersion
|
|
102
96
|
});
|
|
103
97
|
const encoded = proto.SyncActionData.encode(dataProto).finish();
|
|
104
|
-
const keyValue =
|
|
98
|
+
const keyValue = mutationKeys(key.keyData);
|
|
105
99
|
const encValue = aesEncrypt(encoded, keyValue.valueEncryptionKey);
|
|
106
100
|
const valueMac = generateMac(operation, encValue, encKeyId, keyValue.valueMacKey);
|
|
107
101
|
const indexMac = hmacSign(indexBuffer, keyValue.indexKey);
|
|
108
102
|
// update LT hash
|
|
109
103
|
const generator = makeLtHashGenerator(state);
|
|
110
104
|
generator.mix({ indexMac, valueMac, operation });
|
|
111
|
-
Object.assign(state,
|
|
105
|
+
Object.assign(state, generator.finish());
|
|
112
106
|
state.version += 1;
|
|
113
107
|
const snapshotMac = generateSnapshotMac(state.hash, state.version, type, keyValue.snapshotMacKey);
|
|
114
108
|
const patch = {
|
|
@@ -136,6 +130,7 @@ export const encodeSyncdPatch = async ({ type, index, syncAction, apiVersion, op
|
|
|
136
130
|
};
|
|
137
131
|
export const decodeSyncdMutations = async (msgMutations, initialState, getAppStateSyncKey, onMutation, validateMacs) => {
|
|
138
132
|
const ltGenerator = makeLtHashGenerator(initialState);
|
|
133
|
+
const derivedKeyCache = new Map();
|
|
139
134
|
// indexKey used to HMAC sign record.index.blob
|
|
140
135
|
// valueEncryptionKey used to AES-256-CBC encrypt record.value.blob[0:-32]
|
|
141
136
|
// the remaining record.value.blob[0:-32] is the mac, it the HMAC sign of key.keyId + decoded proto data + length of bytes in keyId
|
|
@@ -145,9 +140,9 @@ export const decodeSyncdMutations = async (msgMutations, initialState, getAppSta
|
|
|
145
140
|
const operation = 'operation' in msgMutation ? msgMutation.operation : proto.SyncdMutation.SyncdOperation.SET;
|
|
146
141
|
const record = 'record' in msgMutation && !!msgMutation.record ? msgMutation.record : msgMutation;
|
|
147
142
|
const key = await getKey(record.keyId.id);
|
|
148
|
-
const content =
|
|
149
|
-
const encContent = content.
|
|
150
|
-
const ogValueMac = content.
|
|
143
|
+
const content = record.value.blob;
|
|
144
|
+
const encContent = content.subarray(0, -32);
|
|
145
|
+
const ogValueMac = content.subarray(-32);
|
|
151
146
|
if (validateMacs) {
|
|
152
147
|
const contentHmac = generateMac(operation, encContent, record.keyId.id, key.valueMacKey);
|
|
153
148
|
if (Buffer.compare(contentHmac, ogValueMac) !== 0) {
|
|
@@ -170,9 +165,13 @@ export const decodeSyncdMutations = async (msgMutations, initialState, getAppSta
|
|
|
170
165
|
operation: operation
|
|
171
166
|
});
|
|
172
167
|
}
|
|
173
|
-
return
|
|
168
|
+
return ltGenerator.finish();
|
|
174
169
|
async function getKey(keyId) {
|
|
175
170
|
const base64Key = Buffer.from(keyId).toString('base64');
|
|
171
|
+
const cached = derivedKeyCache.get(base64Key);
|
|
172
|
+
if (cached) {
|
|
173
|
+
return cached;
|
|
174
|
+
}
|
|
176
175
|
const keyEnc = await getAppStateSyncKey(base64Key);
|
|
177
176
|
if (!keyEnc) {
|
|
178
177
|
throw new Boom(`failed to find key "${base64Key}" to decode mutation`, {
|
|
@@ -180,7 +179,9 @@ export const decodeSyncdMutations = async (msgMutations, initialState, getAppSta
|
|
|
180
179
|
data: { msgMutations }
|
|
181
180
|
});
|
|
182
181
|
}
|
|
183
|
-
|
|
182
|
+
const keys = mutationKeys(keyEnc.keyData);
|
|
183
|
+
derivedKeyCache.set(base64Key, keys);
|
|
184
|
+
return keys;
|
|
184
185
|
}
|
|
185
186
|
};
|
|
186
187
|
export const decodeSyncdPatch = async (msg, name, initialState, getAppStateSyncKey, onMutation, validateMacs) => {
|
|
@@ -190,7 +191,7 @@ export const decodeSyncdPatch = async (msg, name, initialState, getAppStateSyncK
|
|
|
190
191
|
if (!mainKeyObj) {
|
|
191
192
|
throw new Boom(`failed to find key "${base64Key}" to decode patch`, { statusCode: 404, data: { msg } });
|
|
192
193
|
}
|
|
193
|
-
const mainKey =
|
|
194
|
+
const mainKey = mutationKeys(mainKeyObj.keyData);
|
|
194
195
|
const mutationmacs = msg.mutations.map(mutation => mutation.record.value.blob.slice(-32));
|
|
195
196
|
const patchMac = generatePatchMac(msg.snapshotMac, mutationmacs, toNumber(msg.version.version), name, mainKey.patchMacKey);
|
|
196
197
|
if (Buffer.compare(patchMac, msg.patchMac) !== 0) {
|
|
@@ -268,7 +269,7 @@ export const decodeSyncdSnapshot = async (name, snapshot, getAppStateSyncKey, mi
|
|
|
268
269
|
if (!keyEnc) {
|
|
269
270
|
throw new Boom(`failed to find key "${base64Key}" to decode mutation`);
|
|
270
271
|
}
|
|
271
|
-
const result =
|
|
272
|
+
const result = mutationKeys(keyEnc.keyData);
|
|
272
273
|
const computedSnapshotMac = generateSnapshotMac(newState.hash, newState.version, name, result.snapshotMacKey);
|
|
273
274
|
if (Buffer.compare(snapshot.mac, computedSnapshotMac) !== 0) {
|
|
274
275
|
throw new Boom(`failed to verify LTHash at ${newState.version} of ${name} from snapshot`);
|
|
@@ -310,7 +311,7 @@ export const decodePatches = async (name, syncds, initial, getAppStateSyncKey, o
|
|
|
310
311
|
if (!keyEnc) {
|
|
311
312
|
throw new Boom(`failed to find key "${base64Key}" to decode mutation`);
|
|
312
313
|
}
|
|
313
|
-
const result =
|
|
314
|
+
const result = mutationKeys(keyEnc.keyData);
|
|
314
315
|
const computedSnapshotMac = generateSnapshotMac(newState.hash, newState.version, name, result.snapshotMacKey);
|
|
315
316
|
if (Buffer.compare(snapshotMac, computedSnapshotMac) !== 0) {
|
|
316
317
|
throw new Boom(`failed to verify LTHash at ${newState.version} of ${name}`);
|
|
@@ -665,14 +666,8 @@ export const processSyncAction = (syncAction, ev, me, initialSyncOpts, logger) =
|
|
|
665
666
|
});
|
|
666
667
|
}
|
|
667
668
|
else if (action?.contactAction) {
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
id: id,
|
|
671
|
-
name: action.contactAction.fullName,
|
|
672
|
-
lid: action.contactAction.lidJid || undefined,
|
|
673
|
-
phoneNumber: action.contactAction.pnJid || undefined
|
|
674
|
-
}
|
|
675
|
-
]);
|
|
669
|
+
const results = processContactAction(action.contactAction, id, logger);
|
|
670
|
+
emitSyncActionResults(ev, results);
|
|
676
671
|
}
|
|
677
672
|
else if (action?.pushNameSetting) {
|
|
678
673
|
const name = action?.pushNameSetting?.name;
|
|
@@ -741,6 +736,60 @@ export const processSyncAction = (syncAction, ev, me, initialSyncOpts, logger) =
|
|
|
741
736
|
}
|
|
742
737
|
});
|
|
743
738
|
}
|
|
739
|
+
else if (action?.localeSetting?.locale) {
|
|
740
|
+
ev.emit('settings.update', { setting: 'locale', value: action.localeSetting.locale });
|
|
741
|
+
}
|
|
742
|
+
else if (action?.timeFormatAction) {
|
|
743
|
+
ev.emit('settings.update', { setting: 'timeFormat', value: action.timeFormatAction });
|
|
744
|
+
}
|
|
745
|
+
else if (action?.pnForLidChatAction) {
|
|
746
|
+
if (action.pnForLidChatAction.pnJid) {
|
|
747
|
+
ev.emit('lid-mapping.update', { lid: id, pn: action.pnForLidChatAction.pnJid });
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
else if (action?.privacySettingRelayAllCalls) {
|
|
751
|
+
ev.emit('settings.update', {
|
|
752
|
+
setting: 'privacySettingRelayAllCalls',
|
|
753
|
+
value: action.privacySettingRelayAllCalls
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
else if (action?.statusPrivacy) {
|
|
757
|
+
ev.emit('settings.update', { setting: 'statusPrivacy', value: action.statusPrivacy });
|
|
758
|
+
}
|
|
759
|
+
else if (action?.lockChatAction) {
|
|
760
|
+
ev.emit('chats.lock', { id: id, locked: !!action.lockChatAction.locked });
|
|
761
|
+
}
|
|
762
|
+
else if (action?.privacySettingDisableLinkPreviewsAction) {
|
|
763
|
+
ev.emit('settings.update', {
|
|
764
|
+
setting: 'disableLinkPreviews',
|
|
765
|
+
value: action.privacySettingDisableLinkPreviewsAction
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
else if (action?.notificationActivitySettingAction?.notificationActivitySetting) {
|
|
769
|
+
ev.emit('settings.update', {
|
|
770
|
+
setting: 'notificationActivitySetting',
|
|
771
|
+
value: action.notificationActivitySettingAction.notificationActivitySetting
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
else if (action?.lidContactAction) {
|
|
775
|
+
ev.emit('contacts.upsert', [
|
|
776
|
+
{
|
|
777
|
+
id: id,
|
|
778
|
+
name: action.lidContactAction.fullName ||
|
|
779
|
+
action.lidContactAction.firstName ||
|
|
780
|
+
action.lidContactAction.username ||
|
|
781
|
+
undefined,
|
|
782
|
+
lid: id,
|
|
783
|
+
phoneNumber: undefined
|
|
784
|
+
}
|
|
785
|
+
]);
|
|
786
|
+
}
|
|
787
|
+
else if (action?.privacySettingChannelsPersonalisedRecommendationAction) {
|
|
788
|
+
ev.emit('settings.update', {
|
|
789
|
+
setting: 'channelsPersonalisedRecommendation',
|
|
790
|
+
value: action.privacySettingChannelsPersonalisedRecommendationAction
|
|
791
|
+
});
|
|
792
|
+
}
|
|
744
793
|
else {
|
|
745
794
|
logger?.debug({ syncAction, id }, 'unprocessable update');
|
|
746
795
|
}
|
package/lib/Utils/crypto.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createCipheriv, createDecipheriv, createHash, createHmac, randomBytes } from 'crypto';
|
|
2
2
|
import * as curve from 'libsignal/src/curve.js';
|
|
3
3
|
import { KEY_BUNDLE_TYPE } from '../Defaults/index.js';
|
|
4
|
+
export { md5, hkdf } from 'whatsapp-rust-bridge';
|
|
4
5
|
// insure browser & node compatibility
|
|
5
6
|
const { subtle } = globalThis.crypto;
|
|
6
7
|
/** prefix version byte to the pub keys, required for some curve crypto functions */
|
|
@@ -69,7 +70,7 @@ export function aesDecryptCTR(ciphertext, key, iv) {
|
|
|
69
70
|
}
|
|
70
71
|
/** decrypt AES 256 CBC; where the IV is prefixed to the buffer */
|
|
71
72
|
export function aesDecrypt(buffer, key) {
|
|
72
|
-
return aesDecryptWithIV(buffer.
|
|
73
|
+
return aesDecryptWithIV(buffer.subarray(16), key, buffer.subarray(0, 16));
|
|
73
74
|
}
|
|
74
75
|
/** decrypt AES 256 CBC */
|
|
75
76
|
export function aesDecryptWithIV(buffer, key, IV) {
|
|
@@ -94,31 +95,6 @@ export function hmacSign(buffer, key, variant = 'sha256') {
|
|
|
94
95
|
export function sha256(buffer) {
|
|
95
96
|
return createHash('sha256').update(buffer).digest();
|
|
96
97
|
}
|
|
97
|
-
export function md5(buffer) {
|
|
98
|
-
return createHash('md5').update(buffer).digest();
|
|
99
|
-
}
|
|
100
|
-
// HKDF key expansion
|
|
101
|
-
export async function hkdf(buffer, expandedLength, info) {
|
|
102
|
-
// Normalize to a Uint8Array whose underlying buffer is a regular ArrayBuffer (not ArrayBufferLike)
|
|
103
|
-
// Cloning via new Uint8Array(...) guarantees the generic parameter is ArrayBuffer which satisfies WebCrypto types.
|
|
104
|
-
const inputKeyMaterial = new Uint8Array(buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer));
|
|
105
|
-
// Set default values if not provided
|
|
106
|
-
const salt = info.salt ? new Uint8Array(info.salt) : new Uint8Array(0);
|
|
107
|
-
const infoBytes = info.info ? new TextEncoder().encode(info.info) : new Uint8Array(0);
|
|
108
|
-
// Import the input key material (cast to BufferSource to appease TS DOM typings)
|
|
109
|
-
const importedKey = await subtle.importKey('raw', inputKeyMaterial, { name: 'HKDF' }, false, [
|
|
110
|
-
'deriveBits'
|
|
111
|
-
]);
|
|
112
|
-
// Derive bits using HKDF
|
|
113
|
-
const derivedBits = await subtle.deriveBits({
|
|
114
|
-
name: 'HKDF',
|
|
115
|
-
hash: 'SHA-256',
|
|
116
|
-
salt: salt,
|
|
117
|
-
info: infoBytes
|
|
118
|
-
}, importedKey, expandedLength * 8 // Convert bytes to bits
|
|
119
|
-
);
|
|
120
|
-
return Buffer.from(derivedBits);
|
|
121
|
-
}
|
|
122
98
|
export async function derivePairingCodeKey(pairingCode, salt) {
|
|
123
99
|
// Convert inputs to formats Web Crypto API can work with
|
|
124
100
|
const encoder = new TextEncoder();
|