@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.
@@ -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
- console.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.');
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 = await noise.processHandshake(handshake, creds.noiseKey);
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),
@@ -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
- return getTxMutex(key).runExclusive(async () => {
213
- const ctx = {
214
- cache: {},
215
- mutations: {},
216
- dbQueries: 0
217
- };
218
- logger.trace('entering transaction');
219
- try {
220
- const result = await txStorage.run(ctx, work);
221
- // Commit mutations
222
- await commitWithRetry(ctx.mutations);
223
- logger.trace({ dbQueries: ctx.dbQueries }, 'transaction completed');
224
- return result;
225
- }
226
- catch (error) {
227
- logger.error({ error }, 'transaction failed, rolling back');
228
- throw error;
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
  };
@@ -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, hkdf, hmacSign } from './crypto.js';
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
- const mutationKeys = async (keydata) => {
10
- const expanded = await hkdf(keydata, 160, { info: 'WhatsApp Mutation Keys' });
10
+ import { emitSyncActionResults, processContactAction } from './sync-action-utils.js';
11
+ const mutationKeys = (keydata) => {
12
+ const keys = expandAppStateKeys(keydata);
11
13
  return {
12
- indexKey: expanded.slice(0, 32),
13
- valueEncryptionKey: expanded.slice(32, 64),
14
- valueMacKey: expanded.slice(64, 96),
15
- snapshotMacKey: expanded.slice(96, 128),
16
- patchMacKey: expanded.slice(128, 160)
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 getKeyData = () => {
21
- let r;
22
- switch (operation) {
23
- case proto.SyncdMutation.SyncdOperation.SET:
24
- r = 0x01;
25
- break;
26
- case proto.SyncdMutation.SyncdOperation.REMOVE:
27
- r = 0x02;
28
- break;
29
- }
30
- const buff = Buffer.from([r]);
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.slice(0, 32);
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(new Uint8Array(valueMac).buffer);
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(new Uint8Array(prevOp.valueMac).buffer);
62
+ subBuffs.push(prevOp.valueMac);
67
63
  }
68
64
  },
69
- finish: async () => {
70
- const hashArrayBuffer = new Uint8Array(hash).buffer;
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: buffer,
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 = await mutationKeys(key.keyData);
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, await generator.finish());
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 = Buffer.from(record.value.blob);
149
- const encContent = content.slice(0, -32);
150
- const ogValueMac = content.slice(-32);
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 await ltGenerator.finish();
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
- return mutationKeys(keyEnc.keyData);
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 = await mutationKeys(mainKeyObj.keyData);
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 = await mutationKeys(keyEnc.keyData);
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 = await mutationKeys(keyEnc.keyData);
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
- ev.emit('contacts.upsert', [
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
  }
@@ -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.slice(16, buffer.length), key, buffer.slice(0, 16));
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();