From a70e4da1c42ee5f6ceedb92470efbac37bdb1561 Mon Sep 17 00:00:00 2001 From: vini Date: Wed, 19 Nov 2025 10:20:49 -0300 Subject: [PATCH] feat(signal): better parity with whatsapp web (#1967) --- src/Socket/messages-recv.ts | 18 ++++++++++++--- src/Socket/messages-send.ts | 25 ++++++++++++++------ src/Socket/socket.ts | 46 ++++++++++++++++++++++++++++++++++++- 3 files changed, 78 insertions(+), 11 deletions(-) diff --git a/src/Socket/messages-recv.ts b/src/Socket/messages-recv.ts index 91a265d7..38d4b31b 100644 --- a/src/Socket/messages-recv.ts +++ b/src/Socket/messages-recv.ts @@ -108,6 +108,9 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => { useClones: false }) + // Debounce identity-change session refreshes per JID to avoid bursts + const identityAssertDebounce = new NodeCache({ stdTTL: 5, useClones: false }) + let sendActiveReceipts = false const fetchMessageHistory = async ( @@ -546,8 +549,17 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => { const identityNode = getBinaryNodeChild(node, 'identity') if (identityNode) { logger.info({ jid: from }, 'identity changed') - // not handling right now - // signal will override new identity anyway + if (identityAssertDebounce.get(from!)) { + logger.debug({ jid: from }, 'skipping identity assert (debounced)') + return + } + + identityAssertDebounce.set(from!, true) + try { + await assertSessions([from!], true) + } catch (error) { + logger.warn({ error, jid: from }, 'failed to assert sessions after identity change') + } } else { logger.info({ node }, 'unknown encrypt notification') } @@ -1007,7 +1019,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => { } } - await assertSessions([participant]) + await assertSessions([participant], true) if (isJidGroup(remoteJid)) { await authState.keys.set({ 'sender-key-memory': { [remoteJid]: null } }) diff --git a/src/Socket/messages-send.ts b/src/Socket/messages-send.ts index 8388297a..72a47080 100644 --- a/src/Socket/messages-send.ts +++ b/src/Socket/messages-send.ts @@ -299,6 +299,16 @@ export const makeMessagesSocket = (config: SocketConfig) => { if (lidResults.length > 0) { logger.trace('Storing LID maps from device call') await signalRepository.lidMapping.storeLIDPNMappings(lidResults.map(a => ({ lid: a.lid as string, pn: a.id }))) + + // Force-refresh sessions for newly mapped LIDs to align identity addressing + try { + const lids = lidResults.map(a => a.lid as string) + if (lids.length) { + await assertSessions(lids, true) + } + } catch (e) { + logger.warn({ e, count: lidResults.length }, 'failed to assert sessions for newly mapped LIDs') + } } const extracted = extractDeviceJids( @@ -373,7 +383,7 @@ export const makeMessagesSocket = (config: SocketConfig) => { return deviceResults } - const assertSessions = async (jids: string[]) => { + const assertSessions = async (jids: string[], force?: boolean) => { let didFetchNewSession = false const uniqueJids = [...new Set(jids)] // Deduplicate JIDs const jidsRequiringFetch: string[] = [] @@ -385,14 +395,14 @@ export const makeMessagesSocket = (config: SocketConfig) => { const signalId = signalRepository.jidToSignalProtocolAddress(jid) const cachedSession = peerSessionsCache.get(signalId) if (cachedSession !== undefined) { - if (cachedSession) { + if (cachedSession && !force) { continue // Session exists in cache } } else { const sessionValidation = await signalRepository.validateSession(jid) const hasSession = sessionValidation.exists peerSessionsCache.set(signalId, hasSession) - if (hasSession) { + if (hasSession && !force) { continue } } @@ -423,10 +433,11 @@ export const makeMessagesSocket = (config: SocketConfig) => { { tag: 'key', attrs: {}, - content: wireJids.map(jid => ({ - tag: 'user', - attrs: { jid } - })) + content: wireJids.map(jid => { + const attrs: { [key: string]: string } = { jid } + if (force) attrs.reason = 'identity' + return { tag: 'user', attrs } + }) } ] }) diff --git a/src/Socket/socket.ts b/src/Socket/socket.ts index efc75253..aec395d0 100644 --- a/src/Socket/socket.ts +++ b/src/Socket/socket.ts @@ -30,7 +30,9 @@ import { getNextPreKeysNode, makeEventBuffer, makeNoiseHandler, - promiseTimeout + promiseTimeout, + signedKeyPair, + xmppSignedPreKey } from '../Utils' import { getPlatformId } from '../Utils/browser-utils' import { @@ -204,6 +206,39 @@ export const makeSocket = (config: SocketConfig) => { return result } + // Validate current key-bundle on server; on failure, trigger pre-key upload and rethrow + const digestKeyBundle = async (): Promise => { + const res = await query({ + tag: 'iq', + attrs: { to: S_WHATSAPP_NET, type: 'get', xmlns: 'encrypt' }, + content: [{ tag: 'digest', attrs: {} }] + }) + const digestNode = getBinaryNodeChild(res, 'digest') + if (!digestNode) { + await uploadPreKeys() + throw new Error('encrypt/get digest returned no digest node') + } + } + + // Rotate our signed pre-key on server; on failure, run digest as fallback and rethrow + const rotateSignedPreKey = async (): Promise => { + const newId = (creds.signedPreKey.keyId || 0) + 1 + const skey = await signedKeyPair(creds.signedIdentityKey, newId) + await query({ + tag: 'iq', + attrs: { to: S_WHATSAPP_NET, type: 'set', xmlns: 'encrypt' }, + content: [ + { + tag: 'rotate', + attrs: {}, + content: [xmppSignedPreKey(skey)] + } + ] + }) + // Persist new signed pre-key in creds + ev.emit('creds.update', { signedPreKey: skey }) + } + const executeUSyncQuery = async (usyncQuery: USyncQuery) => { if (usyncQuery.protocols.length === 0) { throw new Boom('USyncQuery must have at least one protocol') @@ -869,6 +904,13 @@ export const makeSocket = (config: SocketConfig) => { try { await uploadPreKeysToServerIfRequired() await sendPassiveIq('active') + + // After successful login, validate our key-bundle against server + try { + await digestKeyBundle() + } catch (e) { + logger.warn({ e }, 'failed to run digest after login') + } } catch (err) { logger.warn({ err }, 'failed to send initial passive iq') } @@ -1007,6 +1049,8 @@ export const makeSocket = (config: SocketConfig) => { onUnexpectedError, uploadPreKeys, uploadPreKeysToServerIfRequired, + digestKeyBundle, + rotateSignedPreKey, requestPairingCode, wamBuffer: publicWAMBuffer, /** Waits for the connection to WA to reach a state */