feat(events): decrypt event responses (#2056)

This commit is contained in:
vini
2025-11-19 10:17:21 -03:00
committed by GitHub
parent b4863b624e
commit 40f128d88a
4 changed files with 154 additions and 5 deletions
+4 -2
View File
@@ -63,7 +63,8 @@ export const makeChatsSocket = (config: SocketConfig) => {
fireInitQueries,
appStateMacVerification,
shouldIgnoreJid,
shouldSyncHistoryMessage
shouldSyncHistoryMessage,
getMessage
} = config
const sock = makeSocket(config)
const { ev, ws, authState, generateMessageTag, sendNode, query, signalRepository, onUnexpectedError } = sock
@@ -1086,7 +1087,8 @@ export const makeChatsSocket = (config: SocketConfig) => {
creds: authState.creds,
keyStore: authState.keys,
logger,
options: config.options
options: config.options,
getMessage
})
])
+1 -1
View File
@@ -45,7 +45,7 @@ export const BufferJSON = {
}
export const getKeyAuthor = (key: WAMessageKey | undefined | null, meId = 'me') =>
(key?.fromMe ? meId : key?.participant || key?.remoteJid) || ''
(key?.fromMe ? meId : key?.participantAlt || key?.remoteJidAlt || key?.participant || key?.remoteJid) || ''
export const writeRandomPadMax16 = (msg: Uint8Array) => {
const pad = randomBytes(1)
+48
View File
@@ -847,6 +847,19 @@ export const updateMessageWithPollUpdate = (msg: Pick<WAMessage, 'pollUpdates'>,
msg.pollUpdates = reactions
}
/** Update the message with a new event response */
export const updateMessageWithEventResponse = (
msg: Pick<WAMessage, 'eventResponses'>,
update: proto.IEventResponse
) => {
const authorID = getKeyAuthor(update.eventResponseMessageKey)
const responses = (msg.eventResponses || []).filter(r => getKeyAuthor(r.eventResponseMessageKey) !== authorID)
responses.push(update)
msg.eventResponses = responses
}
type VoteAggregation = {
name: string
voters: string[]
@@ -903,6 +916,41 @@ export function getAggregateVotesInPollMessage(
return Object.values(voteHashMap)
}
type ResponseAggregation = {
response: string
responders: string[]
}
/**
* Aggregates all event responses in an event message.
* @param msg the event creation message
* @param meId your jid
* @returns A list of response types & their responders
*/
export function getAggregateResponsesInEventMessage(
{ eventResponses }: Pick<WAMessage, 'eventResponses'>,
meId?: string
) {
const responseTypes = ['GOING', 'NOT_GOING', 'MAYBE']
const responseMap: { [_: string]: ResponseAggregation } = {}
for (const type of responseTypes) {
responseMap[type] = {
response: type,
responders: []
}
}
for (const update of eventResponses || []) {
const responseType = (update as any).eventResponse || 'UNKNOWN'
if (responseType !== 'UNKNOWN' && responseMap[responseType]) {
responseMap[responseType].responders.push(getKeyAuthor(update.eventResponseMessageKey, meId))
}
}
return Object.values(responseMap)
}
/** Given a list of message keys, aggregates them by chat & sender. Useful for sending read receipts in bulk */
export const aggregateMessageKeysNotFromMe = (keys: WAMessageKey[]) => {
const keyMap: { [id: string]: { jid: string; participant: string | undefined; messageIds: string[] } } = {}
+101 -2
View File
@@ -12,6 +12,7 @@ import type {
RequestJoinMethod,
SignalKeyStoreWithTransaction,
SignalRepositoryWithLIDStore,
SocketConfig,
WAMessage,
WAMessageKey
} from '../Types'
@@ -23,12 +24,13 @@ import {
isHostedPnUser,
isJidBroadcast,
isJidStatusBroadcast,
isLidUser,
jidDecode,
jidEncode,
jidNormalizedUser
} from '../WABinary'
import { aesDecryptGCM, hmacSign } from './crypto'
import { toNumber } from './generics'
import { getKeyAuthor, toNumber } from './generics'
import { downloadAndProcessHistorySyncNotification } from './history'
import type { ILogger } from './logger'
@@ -41,6 +43,7 @@ type ProcessMessageContext = {
logger?: ILogger
options: RequestInit
signalRepository: SignalRepositoryWithLIDStore
getMessage: SocketConfig['getMessage']
}
const REAL_MSG_STUB_TYPES = new Set([
@@ -144,6 +147,17 @@ type PollContext = {
voterJid: string
}
type EventContext = {
/** normalised jid of the person that created the event */
eventCreatorJid: string
/** ID of the event creation message */
eventMsgId: string
/** event creation message enc key */
eventEncKey: Uint8Array
/** jid of the person that responded */
responderJid: string
}
/**
* Decrypt a poll vote
* @param vote encrypted vote
@@ -174,6 +188,36 @@ export function decryptPollVote(
}
}
/**
* Decrypt an event response
* @param response encrypted event response
* @param ctx additional info about the event required for decryption
* @returns event response message
*/
export function decryptEventResponse(
{ encPayload, encIv }: proto.Message.IPollEncValue,
{ eventCreatorJid, eventMsgId, eventEncKey, responderJid }: EventContext
) {
const sign = Buffer.concat([
toBinary(eventMsgId),
toBinary(eventCreatorJid),
toBinary(responderJid),
toBinary('Event Response'),
new Uint8Array([1])
])
const key0 = hmacSign(eventEncKey, new Uint8Array(32), 'sha256')
const decKey = hmacSign(sign, key0, 'sha256')
const aad = toBinary(`${eventMsgId}\u0000${responderJid}`)
const decrypted = aesDecryptGCM(encPayload!, decKey, encIv!, aad)
return proto.Message.EventResponseMessage.decode(decrypted)
function toBinary(txt: string) {
return Buffer.from(txt)
}
}
const processMessage = async (
message: WAMessage,
{
@@ -184,7 +228,8 @@ const processMessage = async (
signalRepository,
keyStore,
logger,
options
options,
getMessage
}: ProcessMessageContext
) => {
const meId = creds.me!.id
@@ -363,6 +408,60 @@ const processMessage = async (
key: content.reactionMessage?.key!
}
])
} else if (content?.encEventResponseMessage) {
const encEventResponse = content.encEventResponseMessage
const creationMsgKey = encEventResponse.eventCreationMessageKey!
// we need to fetch the event creation message to get the event enc key
const eventMsg = await getMessage(creationMsgKey)
if (eventMsg) {
try {
const meIdNormalised = jidNormalizedUser(meId)
// all jids need to be PN
const eventCreatorKey = creationMsgKey.participant || creationMsgKey.remoteJid!
const eventCreatorPn = isLidUser(eventCreatorKey)
? await signalRepository.lidMapping.getPNForLID(eventCreatorKey)
: eventCreatorKey
const eventCreatorJid = getKeyAuthor(
{ remoteJid: jidNormalizedUser(eventCreatorPn!), fromMe: meIdNormalised === eventCreatorPn },
meIdNormalised
)
const responderJid = getKeyAuthor(message.key, meIdNormalised)
const eventEncKey = eventMsg?.messageContextInfo?.messageSecret
if (!eventEncKey) {
logger?.warn({ creationMsgKey }, 'event response: missing messageSecret for decryption')
} else {
const responseMsg = decryptEventResponse(encEventResponse, {
eventEncKey,
eventCreatorJid,
eventMsgId: creationMsgKey.id!,
responderJid
})
const eventResponse = {
eventResponseMessageKey: message.key,
senderTimestampMs: responseMsg.timestampMs!,
response: responseMsg
}
ev.emit('messages.update', [
{
key: creationMsgKey,
update: {
eventResponses: [eventResponse]
}
}
])
}
} catch (err) {
logger?.warn({ err, creationMsgKey }, 'failed to decrypt event response')
}
} else {
logger?.warn({ creationMsgKey }, 'event creation message not found, cannot decrypt response')
}
} else if (message.messageStubType) {
const jid = message.key?.remoteJid!
//let actor = whatsappID (message.participant)