feat(events): decrypt event responses (#2056)
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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[] } } = {}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user