diff --git a/src/Socket/messages-recv.ts b/src/Socket/messages-recv.ts index 85160d03c99..42988893b32 100644 --- a/src/Socket/messages-recv.ts +++ b/src/Socket/messages-recv.ts @@ -5,6 +5,7 @@ import NodeCache from 'node-cache' import { proto } from '../../WAProto' import { DEFAULT_CACHE_TTLS, KEY_BUNDLE_TYPE, MIN_PREKEY_COUNT } from '../Defaults' import { MessageReceiptType, MessageRelayOptions, MessageUserReceipt, SocketConfig, WACallEvent, WAMessageKey, WAMessageStatus, WAMessageStubType, WAPatchName } from '../Types' +import { getBotJid } from '../WABinary' import { aesDecryptCTR, aesEncryptGCM, @@ -198,7 +199,8 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => { } if(node.attrs.recipient) { - receipt.attrs.recipient = node.attrs.recipient + //Fixes problem with retry that is never done when it is @bot + receipt.attrs.recipient = getBotJid(node.attrs.recipient); } if(node.attrs.participant) { @@ -753,13 +755,13 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => { } } - const { fullMessage: msg, category, author, decrypt } = decryptMessageNode( node, authState.creds.me!.id, authState.creds.me!.lid || '', signalRepository, logger, + getMessage ) if(response && msg?.messageStubParameters?.[0] === NO_MESSAGE_FOUND_ERROR_TEXT) { diff --git a/src/Socket/messages-send.ts b/src/Socket/messages-send.ts index 09c07a8ac63..722bc0f5598 100644 --- a/src/Socket/messages-send.ts +++ b/src/Socket/messages-send.ts @@ -6,7 +6,7 @@ import { DEFAULT_CACHE_TTLS, WA_DEFAULT_EPHEMERAL } from '../Defaults' import { AnyMessageContent, MediaConnInfo, MessageReceiptType, MessageRelayOptions, MiscMessageGenerationOptions, SocketConfig, WAMessageKey } from '../Types' import { aggregateMessageKeysNotFromMe, assertMediaContent, bindWaitForEvent, decryptMediaRetryData, encodeSignedDeviceIdentity, encodeWAMessage, encryptMediaRetryRequest, extractDeviceJids, generateMessageIDV2, generateWAMessage, getStatusCodeForMediaRetry, getUrlFromDirectPath, getWAUploadToServer, normalizeMessageContent, parseAndInjectE2ESessions, unixTimestampSeconds } from '../Utils' import { getUrlInfo } from '../Utils/link-preview' -import { areJidsSameUser, BinaryNode, BinaryNodeAttributes, getBinaryNodeChild, getBinaryNodeChildren, isJidGroup, isJidUser, jidDecode, jidEncode, jidNormalizedUser, JidWithDevice, S_WHATSAPP_NET } from '../WABinary' +import { areJidsSameUser, BinaryNode, BinaryNodeAttributes, getBinaryNodeChild, getBinaryNodeChildren, getBotJid, isJidGroup, isJidUser, jidDecode, jidEncode, jidNormalizedUser, JidWithDevice, S_WHATSAPP_NET } from '../WABinary' import { USyncQuery, USyncUser } from '../WAUSync' import { makeGroupsSocket } from './groups' @@ -39,10 +39,10 @@ export const makeMessagesSocket = (config: SocketConfig) => { }) let mediaConn: Promise - const refreshMediaConn = async(forceGet = false) => { + const refreshMediaConn = async (forceGet = false) => { const media = await mediaConn - if(!media || forceGet || (new Date().getTime() - media.fetchDate.getTime()) > media.ttl * 1000) { - mediaConn = (async() => { + if (!media || forceGet || (new Date().getTime() - media.fetchDate.getTime()) > media.ttl * 1000) { + mediaConn = (async () => { const result = await query({ tag: 'iq', attrs: { @@ -50,7 +50,7 @@ export const makeMessagesSocket = (config: SocketConfig) => { xmlns: 'w:m', to: S_WHATSAPP_NET, }, - content: [ { tag: 'media_conn', attrs: { } } ] + content: [{ tag: 'media_conn', attrs: {} }] }) const mediaConnNode = getBinaryNodeChild(result, 'media_conn') const node: MediaConnInfo = { @@ -73,10 +73,10 @@ export const makeMessagesSocket = (config: SocketConfig) => { } /** - * generic send receipt function - * used for receipts of phone call, read, delivery etc. - * */ - const sendReceipt = async(jid: string, participant: string | undefined, messageIds: string[], type: MessageReceiptType) => { + * generic send receipt function + * used for receipts of phone call, read, delivery etc. + * */ + const sendReceipt = async (jid: string, participant: string | undefined, messageIds: string[], type: MessageReceiptType) => { const node: BinaryNode = { tag: 'receipt', attrs: { @@ -84,30 +84,30 @@ export const makeMessagesSocket = (config: SocketConfig) => { }, } const isReadReceipt = type === 'read' || type === 'read-self' - if(isReadReceipt) { + if (isReadReceipt) { node.attrs.t = unixTimestampSeconds().toString() } - if(type === 'sender' && isJidUser(jid)) { + if (type === 'sender' && isJidUser(jid)) { node.attrs.recipient = jid node.attrs.to = participant! } else { node.attrs.to = jid - if(participant) { + if (participant) { node.attrs.participant = participant } } - if(type) { + if (type) { node.attrs.type = type } const remainingMessageIds = messageIds.slice(1) - if(remainingMessageIds.length) { + if (remainingMessageIds.length) { node.content = [ { tag: 'list', - attrs: { }, + attrs: {}, content: remainingMessageIds.map(id => ({ tag: 'item', attrs: { id } @@ -121,38 +121,38 @@ export const makeMessagesSocket = (config: SocketConfig) => { } /** Correctly bulk send receipts to multiple chats, participants */ - const sendReceipts = async(keys: WAMessageKey[], type: MessageReceiptType) => { + const sendReceipts = async (keys: WAMessageKey[], type: MessageReceiptType) => { const recps = aggregateMessageKeysNotFromMe(keys) - for(const { jid, participant, messageIds } of recps) { + for (const { jid, participant, messageIds } of recps) { await sendReceipt(jid, participant, messageIds, type) } } /** Bulk read messages. Keys can be from different chats & participants */ - const readMessages = async(keys: WAMessageKey[]) => { + const readMessages = async (keys: WAMessageKey[]) => { const privacySettings = await fetchPrivacySettings() // based on privacy settings, we have to change the read type const readType = privacySettings.readreceipts === 'all' ? 'read' : 'read-self' await sendReceipts(keys, readType) - } + } /** Fetch all the devices we've to send a message to */ - const getUSyncDevices = async(jids: string[], useCache: boolean, ignoreZeroDevices: boolean) => { + const getUSyncDevices = async (jids: string[], useCache: boolean, ignoreZeroDevices: boolean) => { const deviceResults: JidWithDevice[] = [] - if(!useCache) { + if (!useCache) { logger.debug('not using cache for devices') } const toFetch: string[] = [] jids = Array.from(new Set(jids)) - for(let jid of jids) { + for (let jid of jids) { const user = jidDecode(jid)?.user jid = jidNormalizedUser(jid) - if(useCache) { + if (useCache) { const devices = userDevicesCache.get(user!) - if(devices) { + if (devices) { deviceResults.push(...devices) logger.trace({ user }, 'using cache for devices') @@ -164,7 +164,7 @@ export const makeMessagesSocket = (config: SocketConfig) => { } } - if(!toFetch.length) { + if (!toFetch.length) { return deviceResults } @@ -172,24 +172,24 @@ export const makeMessagesSocket = (config: SocketConfig) => { .withContext('message') .withDeviceProtocol() - for(const jid of toFetch) { + for (const jid of toFetch) { query.withUser(new USyncUser().withId(jid)) } const result = await sock.executeUSyncQuery(query) - if(result) { + if (result) { const extracted = extractDeviceJids(result?.list, authState.creds.me!.id, ignoreZeroDevices) const deviceMap: { [_: string]: JidWithDevice[] } = {} - for(const item of extracted) { + for (const item of extracted) { deviceMap[item.user] = deviceMap[item.user] || [] deviceMap[item.user].push(item) deviceResults.push(item) } - for(const key in deviceMap) { + for (const key in deviceMap) { userDevicesCache.set(key, deviceMap[key]) } } @@ -197,10 +197,10 @@ export const makeMessagesSocket = (config: SocketConfig) => { return deviceResults } - const assertSessions = async(jids: string[], force: boolean) => { + const assertSessions = async (jids: string[], force: boolean) => { let didFetchNewSession = false let jidsRequiringFetch: string[] = [] - if(force) { + if (force) { jidsRequiringFetch = jids } else { const addrs = jids.map(jid => ( @@ -208,16 +208,16 @@ export const makeMessagesSocket = (config: SocketConfig) => { .jidToSignalProtocolAddress(jid) )) const sessions = await authState.keys.get('session', addrs) - for(const jid of jids) { + for (const jid of jids) { const signalId = signalRepository .jidToSignalProtocolAddress(jid) - if(!sessions[signalId]) { + if (!sessions[signalId]) { jidsRequiringFetch.push(jid) } } } - if(jidsRequiringFetch.length) { + if (jidsRequiringFetch.length) { logger.debug({ jidsRequiringFetch }, 'fetching sessions') const result = await query({ tag: 'iq', @@ -229,7 +229,7 @@ export const makeMessagesSocket = (config: SocketConfig) => { content: [ { tag: 'key', - attrs: { }, + attrs: {}, content: jidsRequiringFetch.map( jid => ({ tag: 'user', @@ -247,11 +247,11 @@ export const makeMessagesSocket = (config: SocketConfig) => { return didFetchNewSession } - const sendPeerDataOperationMessage = async( + const sendPeerDataOperationMessage = async ( pdoMessage: proto.Message.IPeerDataOperationRequestMessage ): Promise => { //TODO: for later, abstract the logic to send a Peer Message instead of just PDO - useful for App State Key Resync with phone - if(!authState.creds.me?.id) { + if (!authState.creds.me?.id) { throw new Boom('Not authenticated') } @@ -275,7 +275,7 @@ export const makeMessagesSocket = (config: SocketConfig) => { return msgId } - const createParticipantNodes = async( + const createParticipantNodes = async ( jids: string[], message: proto.IMessage, extraAttrs?: BinaryNode['attrs'] @@ -289,7 +289,7 @@ export const makeMessagesSocket = (config: SocketConfig) => { async jid => { const { type, ciphertext } = await signalRepository .encryptMessage({ jid, data: bytes }) - if(type === 'pkmsg') { + if (type === 'pkmsg') { shouldIncludeDeviceIdentity = true } @@ -313,7 +313,7 @@ export const makeMessagesSocket = (config: SocketConfig) => { return { nodes, shouldIncludeDeviceIdentity } } - const relayMessage = async( + const relayMessage = async ( jid: string, message: proto.IMessage, { messageId: msgId, participant, additionalAttributes, additionalNodes, useUserDevicesCache, useCachedGroupMetadata, statusJidList }: MessageRelayOptions @@ -346,11 +346,11 @@ export const makeMessagesSocket = (config: SocketConfig) => { const extraAttrs = {} - if(participant) { + if (participant) { // when the retry request is not for a group // only send to the specific device that asked for a retry // otherwise the message is sent out to every device that should be a recipient - if(!isGroup && !isStatus) { + if (!isGroup && !isStatus) { additionalAttributes = { ...additionalAttributes, 'device_fanout': 'false' } } @@ -359,41 +359,41 @@ export const makeMessagesSocket = (config: SocketConfig) => { } await authState.keys.transaction( - async() => { + async () => { const mediaType = getMediaType(message) - if(mediaType) { + if (mediaType) { extraAttrs['mediatype'] = mediaType } - if(normalizeMessageContent(message)?.pinInChatMessage) { + if (normalizeMessageContent(message)?.pinInChatMessage) { extraAttrs['decrypt-fail'] = 'hide' } - if(isGroup || isStatus) { + if (isGroup || isStatus) { const [groupData, senderKeyMap] = await Promise.all([ - (async() => { + (async () => { let groupData = useCachedGroupMetadata && cachedGroupMetadata ? await cachedGroupMetadata(jid) : undefined - if(groupData && Array.isArray(groupData?.participants)) { + if (groupData && Array.isArray(groupData?.participants)) { logger.trace({ jid, participants: groupData.participants.length }, 'using cached group metadata') - } else if(!isStatus) { + } else if (!isStatus) { groupData = await groupMetadata(jid) } return groupData })(), - (async() => { - if(!participant && !isStatus) { + (async () => { + if (!participant && !isStatus) { const result = await authState.keys.get('sender-key-memory', [jid]) - return result[jid] || { } + return result[jid] || {} } - return { } + return {} })() ]) - if(!participant) { + if (!participant) { const participantsList = (groupData && !isStatus) ? groupData.participants.map(p => p.id) : [] - if(isStatus && statusJidList) { + if (isStatus && statusJidList) { participantsList.push(...statusJidList) } @@ -414,9 +414,9 @@ export const makeMessagesSocket = (config: SocketConfig) => { const senderKeyJids: string[] = [] // ensure a connection is established with every device - for(const { user, device } of devices) { + for (const { user, device } of devices) { const jid = jidEncode(user, isLid ? 'lid' : 's.whatsapp.net', device) - if(!senderKeyMap[jid] || !!participant) { + if (!senderKeyMap[jid] || !!participant) { senderKeyJids.push(jid) // store that this person has had the sender keys sent to them senderKeyMap[jid] = true @@ -425,7 +425,7 @@ export const makeMessagesSocket = (config: SocketConfig) => { // if there are some participants with whom the session has not been established // if there are, we re-send the senderkey - if(senderKeyJids.length) { + if (senderKeyJids.length) { logger.debug({ senderKeyJids }, 'sending new sender key') const senderKeyMsg: proto.IMessage = { @@ -453,14 +453,14 @@ export const makeMessagesSocket = (config: SocketConfig) => { } else { const { user: meUser } = jidDecode(meId)! - if(!participant) { + if (!participant) { devices.push({ user }) - if(user !== meUser) { + if (user !== meUser) { devices.push({ user: meUser }) } - if(additionalAttributes?.['category'] !== 'peer') { - const additionalDevices = await getUSyncDevices([ meId, jid ], !!useUserDevicesCache, true) + if (additionalAttributes?.['category'] !== 'peer') { + const additionalDevices = await getUSyncDevices([meId, jid], !!useUserDevicesCache, true) devices.push(...additionalDevices) } } @@ -468,10 +468,10 @@ export const makeMessagesSocket = (config: SocketConfig) => { const allJids: string[] = [] const meJids: string[] = [] const otherJids: string[] = [] - for(const { user, device } of devices) { + for (const { user, device } of devices) { const isMe = user === meUser const jid = jidEncode(isMe && isLid ? authState.creds?.me?.lid!.split(':')[0] || user : user, isLid ? 'lid' : 's.whatsapp.net', device) - if(isMe) { + if (isMe) { meJids.push(jid) } else { otherJids.push(jid) @@ -495,16 +495,16 @@ export const makeMessagesSocket = (config: SocketConfig) => { shouldIncludeDeviceIdentity = shouldIncludeDeviceIdentity || s1 || s2 } - if(participants.length) { - if(additionalAttributes?.['category'] === 'peer') { + if (participants.length) { + if (additionalAttributes?.['category'] === 'peer') { const peerNode = participants[0]?.content?.[0] as BinaryNode - if(peerNode) { + if (peerNode) { binaryNodeContent.push(peerNode) // push only enc } } else { binaryNodeContent.push({ tag: 'participants', - attrs: { }, + attrs: {}, content: participants }) } @@ -522,11 +522,11 @@ export const makeMessagesSocket = (config: SocketConfig) => { // if the participant to send to is explicitly specified (generally retry recp) // ensure the message is only sent to that person // if a retry receipt is sent to everyone -- it'll fail decryption for everyone else who received the msg - if(participant) { - if(isJidGroup(destinationJid)) { + if (participant) { + if (isJidGroup(destinationJid)) { stanza.attrs.to = destinationJid stanza.attrs.participant = participant.jid - } else if(areJidsSameUser(participant.jid, meId)) { + } else if (areJidsSameUser(participant.jid, meId)) { stanza.attrs.to = participant.jid stanza.attrs.recipient = destinationJid } else { @@ -536,17 +536,17 @@ export const makeMessagesSocket = (config: SocketConfig) => { stanza.attrs.to = destinationJid } - if(shouldIncludeDeviceIdentity) { + if (shouldIncludeDeviceIdentity) { (stanza.content as BinaryNode[]).push({ tag: 'device-identity', - attrs: { }, + attrs: {}, content: encodeSignedDeviceIdentity(authState.creds.account!, true) }) logger.debug({ jid }, 'adding device identity') } - if(additionalNodes && additionalNodes.length > 0) { + if (additionalNodes && additionalNodes.length > 0) { (stanza.content as BinaryNode[]).push(...additionalNodes) } @@ -561,7 +561,7 @@ export const makeMessagesSocket = (config: SocketConfig) => { const getMessageType = (message: proto.IMessage) => { - if(message.pollCreationMessage || message.pollCreationMessageV2 || message.pollCreationMessageV3) { + if (message.pollCreationMessage || message.pollCreationMessageV2 || message.pollCreationMessageV3) { return 'poll' } @@ -569,40 +569,40 @@ export const makeMessagesSocket = (config: SocketConfig) => { } const getMediaType = (message: proto.IMessage) => { - if(message.imageMessage) { + if (message.imageMessage) { return 'image' - } else if(message.videoMessage) { + } else if (message.videoMessage) { return message.videoMessage.gifPlayback ? 'gif' : 'video' - } else if(message.audioMessage) { + } else if (message.audioMessage) { return message.audioMessage.ptt ? 'ptt' : 'audio' - } else if(message.contactMessage) { + } else if (message.contactMessage) { return 'vcard' - } else if(message.documentMessage) { + } else if (message.documentMessage) { return 'document' - } else if(message.contactsArrayMessage) { + } else if (message.contactsArrayMessage) { return 'contact_array' - } else if(message.liveLocationMessage) { + } else if (message.liveLocationMessage) { return 'livelocation' - } else if(message.stickerMessage) { + } else if (message.stickerMessage) { return 'sticker' - } else if(message.listMessage) { + } else if (message.listMessage) { return 'list' - } else if(message.listResponseMessage) { + } else if (message.listResponseMessage) { return 'list_response' - } else if(message.buttonsResponseMessage) { + } else if (message.buttonsResponseMessage) { return 'buttons_response' - } else if(message.orderMessage) { + } else if (message.orderMessage) { return 'order' - } else if(message.productMessage) { + } else if (message.productMessage) { return 'product' - } else if(message.interactiveResponseMessage) { + } else if (message.interactiveResponseMessage) { return 'native_flow_response' - } else if(message.groupInviteMessage) { + } else if (message.groupInviteMessage) { return 'url' } } - const getPrivacyTokens = async(jids: string[]) => { + const getPrivacyTokens = async (jids: string[]) => { const t = unixTimestampSeconds().toString() const result = await query({ tag: 'iq', @@ -614,7 +614,7 @@ export const makeMessagesSocket = (config: SocketConfig) => { content: [ { tag: 'tokens', - attrs: { }, + attrs: {}, content: jids.map( jid => ({ tag: 'token', @@ -650,7 +650,7 @@ export const makeMessagesSocket = (config: SocketConfig) => { sendPeerDataOperationMessage, createParticipantNodes, getUSyncDevices, - updateMediaMessage: async(message: proto.IWebMessageInfo) => { + updateMediaMessage: async (message: proto.IWebMessageInfo) => { const content = assertMediaContent(message.message) const mediaKey = content.mediaKey! const meId = authState.creds.me!.id @@ -662,13 +662,13 @@ export const makeMessagesSocket = (config: SocketConfig) => { sendNode(node), waitForMsgMediaUpdate(update => { const result = update.find(c => c.key.id === message.key.id) - if(result) { - if(result.error) { + if (result) { + if (result.error) { error = result.error } else { try { const media = decryptMediaRetryData(result.media!, mediaKey, result.key.id!) - if(media.result !== proto.MediaRetryNotification.ResultType.SUCCESS) { + if (media.result !== proto.MediaRetryNotification.ResultType.SUCCESS) { const resultStr = proto.MediaRetryNotification.ResultType[media.result] throw new Boom( `Media re-upload failed by device (${resultStr})`, @@ -680,7 +680,7 @@ export const makeMessagesSocket = (config: SocketConfig) => { content.url = getUrlFromDirectPath(content.directPath) logger.debug({ directPath: media.directPath, key: result.key }, 'media update successful') - } catch(err) { + } catch (err) { error = err } } @@ -691,7 +691,7 @@ export const makeMessagesSocket = (config: SocketConfig) => { ] ) - if(error) { + if (error) { throw error } @@ -701,13 +701,13 @@ export const makeMessagesSocket = (config: SocketConfig) => { return message }, - sendMessage: async( + sendMessage: async ( jid: string, content: AnyMessageContent, - options: MiscMessageGenerationOptions = { } + options: MiscMessageGenerationOptions = {} ) => { const userJid = authState.creds.me!.id - if( + if ( typeof content === 'object' && 'disappearingMessagesInChat' in content && typeof content['disappearingMessagesInChat'] !== 'undefined' && @@ -719,6 +719,9 @@ export const makeMessagesSocket = (config: SocketConfig) => { disappearingMessagesInChat await groupToggleEphemeral(jid, value) } else { + if (jid.endsWith('@bot')) { + jid = getBotJid(jid); + } const fullMsg = await generateWAMessage( jid, content, @@ -731,7 +734,7 @@ export const makeMessagesSocket = (config: SocketConfig) => { thumbnailWidth: linkPreviewImageThumbnailWidth, fetchOpts: { timeout: 3_000, - ...axiosOptions || { } + ...axiosOptions || {} }, logger, uploadImage: generateHighQualityLinkPreview @@ -752,21 +755,21 @@ export const makeMessagesSocket = (config: SocketConfig) => { const isEditMsg = 'edit' in content && !!content.edit const isPinMsg = 'pin' in content && !!content.pin const isPollMessage = 'poll' in content && !!content.poll - const additionalAttributes: BinaryNodeAttributes = { } + const additionalAttributes: BinaryNodeAttributes = {} const additionalNodes: BinaryNode[] = [] // required for delete - if(isDeleteMsg) { + if (isDeleteMsg) { // if the chat is a group, and I am not the author, then delete the message as an admin - if(isJidGroup(content.delete?.remoteJid as string) && !content.delete?.fromMe) { + if (isJidGroup(content.delete?.remoteJid as string) && !content.delete?.fromMe) { additionalAttributes.edit = '8' } else { additionalAttributes.edit = '7' } - } else if(isEditMsg) { + } else if (isEditMsg) { additionalAttributes.edit = '1' - } else if(isPinMsg) { + } else if (isPinMsg) { additionalAttributes.edit = '2' - } else if(isPollMessage) { + } else if (isPollMessage) { additionalNodes.push({ tag: 'meta', attrs: { @@ -775,12 +778,12 @@ export const makeMessagesSocket = (config: SocketConfig) => { } as BinaryNode) } - if('cachedGroupMetadata' in options) { + if ('cachedGroupMetadata' in options) { console.warn('cachedGroupMetadata in sendMessage are deprecated, now cachedGroupMetadata is part of the socket config.') } await relayMessage(jid, fullMsg.message!, { messageId: fullMsg.key.id!, useCachedGroupMetadata: options.useCachedGroupMetadata, additionalAttributes, statusJidList: options.statusJidList, additionalNodes }) - if(config.emitOwnEvents) { + if (config.emitOwnEvents) { process.nextTick(() => { processingMutex.mutex(() => ( upsertMessage(fullMsg, 'append') diff --git a/src/Utils/decode-wa-message.ts b/src/Utils/decode-wa-message.ts index d8c0d868793..13eaa563367 100644 --- a/src/Utils/decode-wa-message.ts +++ b/src/Utils/decode-wa-message.ts @@ -2,11 +2,21 @@ import { Boom } from '@hapi/boom' import { Logger } from 'pino' import { proto } from '../../WAProto' import { SignalRepository, WAMessageKey } from '../Types' -import { areJidsSameUser, BinaryNode, isJidBroadcast, isJidGroup, isJidNewsletter, isJidStatusBroadcast, isJidUser, isLidUser } from '../WABinary' +import { areJidsSameUser, BinaryNode, isJidBroadcast, isJidGroup, isJidMetaAI, isJidNewsletter, isJidStatusBroadcast, isJidUser, isLidUser } from '../WABinary' import { unpadRandomMax16 } from './generics' +import { createDecipheriv } from 'crypto' +import hkdf from 'futoin-hkdf' export const NO_MESSAGE_FOUND_ERROR_TEXT = 'Message absent from node' export const MISSING_KEYS_ERROR_TEXT = 'Key used already or never filled' +const BOT_MESSAGE_CONSTANT = "Bot Message" +const KEY_LENGTH = 32 + +interface MessageKey { + targetId: string | null; + participant: string; + meId: string; +} export const NACK_REASONS = { ParsingError: 487, @@ -26,6 +36,137 @@ export const NACK_REASONS = { type MessageType = 'chat' | 'peer_broadcast' | 'other_broadcast' | 'group' | 'direct_peer_status' | 'other_status' | 'newsletter' +const deriveMessageSecret = async (messageSecret: Buffer | Uint8Array): Promise => { + // Always convert to Buffer to ensure compatibility + const secretBuffer = Buffer.isBuffer(messageSecret) + ? messageSecret + : Buffer.from(messageSecret.buffer, messageSecret.byteOffset, messageSecret.length); + + return await hkdf( + secretBuffer, + KEY_LENGTH, + { salt: undefined, info: BOT_MESSAGE_CONSTANT, hash: "SHA-256" } + ); +}; + +const buildDecryptionKey = async ( + messageID: string, + botJID: string, + targetJID: string, + messageSecret: Buffer | Uint8Array +): Promise => { + const derivedSecret = await deriveMessageSecret(messageSecret); + const useCaseSecret = Buffer.concat([ + Buffer.from(messageID), + Buffer.from(targetJID), + Buffer.from(botJID), + Buffer.from("") + ]); + return await hkdf( + derivedSecret, + KEY_LENGTH, + { salt: undefined, info: useCaseSecret, hash: "SHA-256" } + ); +}; + +const decryptBotMessage = async ( + encPayload: Buffer | Uint8Array, + encIv: Buffer | Uint8Array, + messageID: string, + botJID: string, + decryptionKey: Buffer | Uint8Array +): Promise => { + encPayload = Buffer.isBuffer(encPayload) ? encPayload : Buffer.from(encPayload); + encIv = Buffer.isBuffer(encIv) ? encIv : Buffer.from(encIv); + decryptionKey = Buffer.isBuffer(decryptionKey) ? decryptionKey : Buffer.from(decryptionKey); + + if (encIv.length !== 12) { + throw new Error(`IV size incorrect: expected 12, got ${encIv.length}`); + } + + const authTag = encPayload.slice(-16); + const encryptedData = encPayload.slice(0, -16); + + if (encryptedData.length < 16) { + throw new Error(`Encrypted data too short: ${encryptedData.length} bytes`); + } + + const aad = Buffer.concat([ + Buffer.from(messageID), + Buffer.from([0]), + Buffer.from(botJID) + ]); + + try { + const decipher = createDecipheriv("aes-256-gcm", decryptionKey, encIv); + decipher.setAAD(aad); + decipher.setAuthTag(authTag); + const decrypted = Buffer.concat([ + decipher.update(encryptedData), + decipher.final() + ]); + + return decrypted; + + } catch (error) { + console.error("Decrypt - Failed with:", (error as Error).message); + throw error; + } +}; + +const decryptMsmsgBotMessage = async ( + messageSecret: Buffer | Uint8Array, + messageKey: MessageKey, + msMsg: proto.IMessageSecretMessage, +): Promise => { + try { + const { targetId, participant: botJID, meId: targetJID } = messageKey; + + if (!targetId || !botJID || !targetJID || !messageSecret) { + throw new Error("Missing required components for decryption"); + } + + const decryptionKey = await buildDecryptionKey( + targetId, + botJID, + targetJID, + messageSecret + ); + + if (!msMsg.encPayload) { + throw new Error('Missing encPayload'); + } + + if (!msMsg.encIv) { + throw new Error('Missing encIv'); + } + + return await decryptBotMessage( + msMsg.encPayload, + msMsg.encIv, + targetId, + botJID, + decryptionKey + ); + } catch (error) { + console.error("Failed to decrypt bot message:", error); + throw error; + } +}; + +const decryptBotMsg = async ( + content: Buffer | Uint8Array, + { messageKey, messageSecret }: { messageKey: MessageKey; messageSecret: Buffer | Uint8Array } +): Promise => { + try { + const msMsg = proto.MessageSecretMessage.decode(content); + return await decryptMsmsgBotMessage(messageSecret, messageKey, msMsg); + } catch (error) { + console.error("Error in decryptBotMsg:", error); + throw error; + } +}; + /** * Decode the received node as a message. * @note this will only parse the message, not decrypt it @@ -47,9 +188,9 @@ export function decodeMessageNode( const isMe = (jid: string) => areJidsSameUser(jid, meId) const isMeLid = (jid: string) => areJidsSameUser(jid, meLid) - if(isJidUser(from)) { - if(recipient) { - if(!isMe(from)) { + if (isJidMetaAI(from) || isJidUser(from)) { + if (recipient) { + if (!isMe(from)) { throw new Boom('receipient present, but msg not from me', { data: stanza }) } @@ -60,9 +201,9 @@ export function decodeMessageNode( msgType = 'chat' author = from - } else if(isLidUser(from)) { - if(recipient) { - if(!isMeLid(from)) { + } else if (isLidUser(from)) { + if (recipient) { + if (!isMeLid(from)) { throw new Boom('receipient present, but msg not from me', { data: stanza }) } @@ -73,21 +214,21 @@ export function decodeMessageNode( msgType = 'chat' author = from - } else if(isJidGroup(from)) { - if(!participant) { + } else if (isJidGroup(from)) { + if (!participant) { throw new Boom('No participant in group message') } msgType = 'group' author = participant chatId = from - } else if(isJidBroadcast(from)) { - if(!participant) { + } else if (isJidBroadcast(from)) { + if (!participant) { throw new Boom('No participant in group message') } const isParticipantMe = isMe(participant) - if(isJidStatusBroadcast(from)) { + if (isJidStatusBroadcast(from)) { msgType = isParticipantMe ? 'direct_peer_status' : 'other_status' } else { msgType = isParticipantMe ? 'peer_broadcast' : 'other_broadcast' @@ -95,7 +236,7 @@ export function decodeMessageNode( chatId = from author = participant - } else if(isJidNewsletter(from)) { + } else if (isJidNewsletter(from)) { msgType = 'newsletter' chatId = from author = from @@ -120,7 +261,7 @@ export function decodeMessageNode( broadcast: isJidBroadcast(from) } - if(key.fromMe) { + if (key.fromMe) { fullMessage.status = proto.WebMessageInfo.Status.SERVER_ACK } @@ -131,33 +272,59 @@ export function decodeMessageNode( } } +type GetMessage = (key: WAMessageKey) => Promise; + export const decryptMessageNode = ( stanza: BinaryNode, meId: string, meLid: string, repository: SignalRepository, - logger: Logger + logger: Logger, + getMessage: GetMessage ) => { const { fullMessage, author, sender } = decodeMessageNode(stanza, meId, meLid) + let metaTargetId: string | null = null + let botEditTargetId: string | null = null + let botType: string | null = null return { fullMessage, category: stanza.attrs.category, author, async decrypt() { let decryptables = 0 - if(Array.isArray(stanza.content)) { - for(const { tag, attrs, content } of stanza.content) { - if(tag === 'verified_name' && content instanceof Uint8Array) { + if (Array.isArray(stanza.content)) { + let hasMsmsg = false; + for (const { attrs } of stanza.content) { + if (attrs?.type === 'msmsg') { + hasMsmsg = true; + break; + } + } + if (hasMsmsg) { + for (const { tag, attrs } of stanza.content) { + if (tag === 'meta' && attrs?.target_id) { + metaTargetId = attrs.target_id; + } + if (tag === 'bot' && attrs?.edit_target_id) { + botEditTargetId = attrs.edit_target_id; + } + if (tag === 'bot' && attrs?.edit) { + botType = attrs.edit; + } + } + } + for (const { tag, attrs, content } of stanza.content) { + if (tag === 'verified_name' && content instanceof Uint8Array) { const cert = proto.VerifiedNameCertificate.decode(content) const details = proto.VerifiedNameCertificate.Details.decode(cert.details) fullMessage.verifiedBizName = details.verifiedName } - if(tag !== 'enc' && tag !== 'plaintext') { + if (tag !== 'enc' && tag !== 'plaintext') { continue } - if(!(content instanceof Uint8Array)) { + if (!(content instanceof Uint8Array)) { continue } @@ -168,49 +335,76 @@ export const decryptMessageNode = ( try { const e2eType = tag === 'plaintext' ? 'plaintext' : attrs.type switch (e2eType) { - case 'skmsg': - msgBuffer = await repository.decryptGroupMessage({ - group: sender, - authorJid: author, - msg: content - }) - break - case 'pkmsg': - case 'msg': - const user = isJidUser(sender) ? sender : author - msgBuffer = await repository.decryptMessage({ - jid: user, - type: e2eType, - ciphertext: content - }) - break - case 'plaintext': - msgBuffer = content - break - default: - throw new Error(`Unknown e2e type: ${e2eType}`) + case 'skmsg': + msgBuffer = await repository.decryptGroupMessage({ + group: sender, + authorJid: author, + msg: content + }) + break + case 'pkmsg': + case 'msg': + const user = isJidUser(sender) ? sender : author + msgBuffer = await repository.decryptMessage({ + jid: user, + type: e2eType, + ciphertext: content + }) + break + case 'msmsg': //Message Secret Message + let msgRequestkey = { + remoteJid: stanza.attrs.from, + id: metaTargetId + } + const message = await getMessage(msgRequestkey); + const messageSecret = message?.messageContextInfo?.messageSecret + if (!messageSecret) { + throw new Error('Message secret not found'); + } + //Only decrypts when it is the complete message + if (botType == 'last') { + const newkey: MessageKey = { + participant: stanza.attrs.from, + meId: stanza.attrs.from.endsWith(`@bot`) ? + `${meLid.split(`:`)[0]}@lid` : + `${meId.split(`:`)[0]}@s.whatsapp.net`, + targetId: botEditTargetId + }; + + msgBuffer = await decryptBotMsg(content, { + messageKey: newkey, + messageSecret + }); + } else return; + break; + case 'plaintext': + msgBuffer = content + break + default: + throw new Error(`Unknown e2e type: ${e2eType}`) } - let msg: proto.IMessage = proto.Message.decode(e2eType !== 'plaintext' ? unpadRandomMax16(msgBuffer) : msgBuffer) - msg = msg.deviceSentMessage?.message || msg - if(msg.senderKeyDistributionMessage) { + let msg: proto.IMessage = proto.Message.decode(e2eType !== 'plaintext' && !hasMsmsg ? unpadRandomMax16(msgBuffer) : msgBuffer) + //It's necessary to save the messageContextInfo in the store to decrypt messages from bots + msg = msg.deviceSentMessage?.message ? { ...msg.deviceSentMessage.message, messageContextInfo: msg.messageContextInfo } : msg; + if (msg.senderKeyDistributionMessage) { //eslint-disable-next-line max-depth - try { + try { await repository.processSenderKeyDistributionMessage({ authorJid: author, item: msg.senderKeyDistributionMessage }) - } catch(err) { + } catch (err) { logger.error({ key: fullMessage.key, err }, 'failed to decrypt message') - } + } } - if(fullMessage.message) { + if (fullMessage.message) { Object.assign(fullMessage.message, msg) } else { fullMessage.message = msg } - } catch(err) { + } catch (err) { logger.error( { key: fullMessage.key, err }, 'failed to decrypt message' @@ -222,7 +416,7 @@ export const decryptMessageNode = ( } // if nothing was found to decrypt - if(!decryptables) { + if (!decryptables) { fullMessage.messageStubType = proto.WebMessageInfo.StubType.CIPHERTEXT fullMessage.messageStubParameters = [NO_MESSAGE_FOUND_ERROR_TEXT] } diff --git a/src/Utils/messages.ts b/src/Utils/messages.ts index 3e802159aa9..af4346b07bb 100644 --- a/src/Utils/messages.ts +++ b/src/Utils/messages.ts @@ -360,6 +360,11 @@ export const generateWAMessageContent = async( } m.extendedTextMessage = extContent + + //WhatsApp always sends secretMessage along with the text message + m.messageContextInfo = { + messageSecret: randomBytes(32) + } } else if('contacts' in message) { const contactLen = message.contacts.contacts.length if(!contactLen) { diff --git a/src/WABinary/jid-utils.ts b/src/WABinary/jid-utils.ts index 9808acb9b97..dbd8b16b45c 100644 --- a/src/WABinary/jid-utils.ts +++ b/src/WABinary/jid-utils.ts @@ -16,6 +16,25 @@ export type FullJid = JidWithDevice & { domainType?: number } +export const getBotJid = (jid: string): string => { + const BOT_MAP = new Map([["867051314767696", "13135550002"], ["1061492271844689", "13135550005"], ["245886058483988", "13135550009"], ["3509905702656130", "13135550012"], ["1059680132034576", "13135550013"], ["715681030623646", "13135550014"], ["1644971366323052", "13135550015"], ["582497970646566", "13135550019"], ["645459357769306", "13135550022"], ["294997126699143", "13135550023"], ["1522631578502677", "13135550027"], ["719421926276396", "13135550030"], ["1788488635002167", "13135550031"], ["24232338603080193", "13135550033"], ["689289903143209", "13135550035"], ["871626054177096", "13135550039"], ["362351902849370", "13135550042"], ["1744617646041527", "13135550043"], ["893887762270570", "13135550046"], ["1155032702135830", "13135550047"], ["333931965993883", "13135550048"], ["853748013058752", "13135550049"], ["1559068611564819", "13135550053"], ["890487432705716", "13135550054"], ["240254602395494", "13135550055"], ["1578420349663261", "13135550062"], ["322908887140421", "13135550065"], ["3713961535514771", "13135550067"], ["997884654811738", "13135550070"], ["403157239387035", "13135550081"], ["535242369074963", "13135550082"], ["946293427247659", "13135550083"], ["3664707673802291", "13135550084"], ["1821827464894892", "13135550085"], ["1760312477828757", "13135550086"], ["439480398712216", "13135550087"], ["1876735582800984", "13135550088"], ["984025089825661", "13135550089"], ["1001336351558186", "13135550090"], ["3739346336347061", "13135550091"], ["3632749426974980", "13135550092"], ["427864203481615", "13135550093"], ["1434734570493055", "13135550094"], ["992873449225921", "13135550095"], ["813087747426445", "13135550096"], ["806369104931434", "13135550098"], ["1220982902403148", "13135550099"], ["1365893374104393", "13135550100"], ["686482033622048", "13135550200"], ["1454999838411253", "13135550201"], ["718584497008509", "13135550202"], ["743520384213443", "13135550301"], ["1147715789823789", "13135550302"], ["1173034540372201", "13135550303"], ["974785541030953", "13135550304"], ["1122200255531507", "13135550305"], ["899669714813162", "13135550306"], ["631880108970650", "13135550307"], ["435816149330026", "13135550308"], ["1368717161184556", "13135550309"], ["7849963461784891", "13135550310"], ["3609617065968984", "13135550312"], ["356273980574602", "13135550313"], ["1043447920539760", "13135550314"], ["1052764336525346", "13135550315"], ["2631118843732685", "13135550316"], ["510505411332176", "13135550317"], ["1945664239227513", "13135550318"], ["1518594378764656", "13135550319"], ["1378821579456138", "13135550320"], ["490214716896013", "13135550321"], ["1028577858870699", "13135550322"], ["308915665545959", "13135550323"], ["845884253678900", "13135550324"], ["995031308616442", "13135550325"], ["2787365464763437", "13135550326"], ["1532790990671645", "13135550327"], ["302617036180485", "13135550328"], ["723376723197227", "13135550329"], ["8393570407377966", "13135550330"], ["1931159970680725", "13135550331"], ["401073885688605", "13135550332"], ["2234478453565422", "13135550334"], ["814748673882312", "13135550335"], ["26133635056281592", "13135550336"], ["1439804456676119", "13135550337"], ["889851503172161", "13135550338"], ["1018283232836879", "13135550339"], ["1012781386779537", "13135559000"], ["823280953239532", "13135559001"], ["1597090934573334", "13135559002"], ["485965054020343", "13135559003"], ["1033381648363446", "13135559004"], ["491802010206446", "13135559005"], ["1017139033184870", "13135559006"], ["499638325922174", "13135559008"], ["468946335863664", "13135559009"], ["1570389776875816", "13135559010"], ["1004342694328995", "13135559011"], ["1012240323971229", "13135559012"], ["392171787222419", "13135559013"], ["952081212945019", "13135559016"], ["444507875070178", "13135559017"], ["1274819440594668", "13135559018"], ["1397041101147050", "13135559019"], ["425657699872640", "13135559020"], ["532292852562549", "13135559021"], ["705863241720292", "13135559022"], ["476449815183959", "13135559023"], ["488071553854222", "13135559024"], ["468693832665397", "13135559025"], ["517422564037340", "13135559026"], ["819805466613825", "13135559027"], ["1847708235641382", "13135559028"], ["716282970644228", "13135559029"], ["521655380527741", "13135559030"], ["476193631941905", "13135559031"], ["485600497445562", "13135559032"], ["440217235683910", "13135559033"], ["523342446758478", "13135559034"], ["514784864360240", "13135559035"], ["505790121814530", "13135559036"], ["420008964419580", "13135559037"], ["492141680204555", "13135559038"], ["388462787271952", "13135559039"], ["423473920752072", "13135559040"], ["489574180468229", "13135559041"], ["432360635854105", "13135559042"], ["477878201669248", "13135559043"], ["351656951234045", "13135559044"], ["430178036732582", "13135559045"], ["434537312944552", "13135559046"], ["1240614300631808", "13135559047"], ["473135945605128", "13135559048"], ["423669800729310", "13135559049"], ["3685666705015792", "13135559050"], ["504196509016638", "13135559051"], ["346844785189449", "13135559052"], ["504823088911074", "13135559053"], ["402669415797083", "13135559054"], ["490939640234431", "13135559055"], ["875124128063715", "13135559056"], ["468788962654605", "13135559057"], ["562386196354570", "13135559058"], ["372159285928791", "13135559059"], ["531017479591050", "13135559060"], ["1328873881401826", "13135559061"], ["1608363646390484", "13135559062"], ["1229628561554232", "13135559063"], ["348802211530364", "13135559064"], ["3708535859420184", "13135559065"], ["415517767742187", "13135559066"], ["479330341612638", "13135559067"], ["480785414723083", "13135559068"], ["387299107507991", "13135559069"], ["333389813188944", "13135559070"], ["391794130316996", "13135559071"], ["457893470576314", "13135559072"], ["435550496166469", "13135559073"], ["1620162702100689", "13135559074"], ["867491058616043", "13135559075"], ["816224117357759", "13135559076"], ["334065176362830", "13135559077"], ["489973170554709", "13135559078"], ["473060669049665", "13135559079"], ["1221505815643060", "13135559080"], ["889000703096359", "13135559081"], ["475235961979883", "13135559082"], ["3434445653519934", "13135559084"], ["524503026827421", "13135559085"], ["1179639046403856", "13135559086"], ["471563305859144", "13135559087"], ["533896609192881", "13135559088"], ["365443583168041", "13135559089"], ["836082305329393", "13135559090"], ["1056787705969916", "13135559091"], ["503312598958357", "13135559092"], ["3718606738453460", "13135559093"], ["826066052850902", "13135559094"], ["1033611345091888", "13135559095"], ["3868390816783240", "13135559096"], ["7462677740498860", "13135559097"], ["436288576108573", "13135559098"], ["1047559746718900", "13135559099"], ["1099299455255491", "13135559100"], ["1202037301040633", "13135559101"], ["1720619402074074", "13135559102"], ["1030422235101467", "13135559103"], ["827238979523502", "13135559104"], ["1516443722284921", "13135559105"], ["1174442747196709", "13135559106"], ["1653165225503842", "13135559107"], ["1037648777635013", "13135559108"], ["551617757299900", "13135559109"], ["1158813558718726", "13135559110"], ["2463236450542262", "13135559111"], ["1550393252501466", "13135559112"], ["2057065188042796", "13135559113"], ["506163028760735", "13135559114"], ["2065249100538481", "13135559115"], ["1041382867195858", "13135559116"], ["886500209499603", "13135559117"], ["1491615624892655", "13135559118"], ["486563697299617", "13135559119"], ["1175736513679463", "13135559120"], ["491811473512352", "13165550064"]]); + + const sepIdx = typeof jid === 'string' ? jid.indexOf('@') : -1 + if(sepIdx < 0) { + return jid + } + + const server = jid!.slice(sepIdx + 1) + if(server !== 'bot') return jid + + const user = jid!.slice(0, sepIdx) + const mappedNumber = BOT_MAP.get(user) + + return mappedNumber ? `${mappedNumber}@s.whatsapp.net` : jid +} + + + export const jidEncode = (user: string | number | null, server: JidServer, device?: number, agent?: number) => { return `${user || ''}${!!agent ? `_${agent}` : ''}${!!device ? `:${device}` : ''}@${server}` } @@ -44,6 +63,8 @@ export const jidDecode = (jid: string | undefined): FullJid | undefined => { export const areJidsSameUser = (jid1: string | undefined, jid2: string | undefined) => ( jidDecode(jid1)?.user === jidDecode(jid2)?.user ) +/** is the jid a bot */ +export const isJidMetaAI = (jid: string | undefined) => (jid?.endsWith('@bot')) /** is the jid a user */ export const isJidUser = (jid: string | undefined) => (jid?.endsWith('@s.whatsapp.net')) /** is the jid a group */