feat(events): decrypt event responses (#2056)
This commit is contained in:
+4
-2
@@ -63,7 +63,8 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
|||||||
fireInitQueries,
|
fireInitQueries,
|
||||||
appStateMacVerification,
|
appStateMacVerification,
|
||||||
shouldIgnoreJid,
|
shouldIgnoreJid,
|
||||||
shouldSyncHistoryMessage
|
shouldSyncHistoryMessage,
|
||||||
|
getMessage
|
||||||
} = config
|
} = config
|
||||||
const sock = makeSocket(config)
|
const sock = makeSocket(config)
|
||||||
const { ev, ws, authState, generateMessageTag, sendNode, query, signalRepository, onUnexpectedError } = sock
|
const { ev, ws, authState, generateMessageTag, sendNode, query, signalRepository, onUnexpectedError } = sock
|
||||||
@@ -1086,7 +1087,8 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
|||||||
creds: authState.creds,
|
creds: authState.creds,
|
||||||
keyStore: authState.keys,
|
keyStore: authState.keys,
|
||||||
logger,
|
logger,
|
||||||
options: config.options
|
options: config.options,
|
||||||
|
getMessage
|
||||||
})
|
})
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export const BufferJSON = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const getKeyAuthor = (key: WAMessageKey | undefined | null, meId = 'me') =>
|
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) => {
|
export const writeRandomPadMax16 = (msg: Uint8Array) => {
|
||||||
const pad = randomBytes(1)
|
const pad = randomBytes(1)
|
||||||
|
|||||||
@@ -847,6 +847,19 @@ export const updateMessageWithPollUpdate = (msg: Pick<WAMessage, 'pollUpdates'>,
|
|||||||
msg.pollUpdates = reactions
|
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 = {
|
type VoteAggregation = {
|
||||||
name: string
|
name: string
|
||||||
voters: string[]
|
voters: string[]
|
||||||
@@ -903,6 +916,41 @@ export function getAggregateVotesInPollMessage(
|
|||||||
return Object.values(voteHashMap)
|
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 */
|
/** Given a list of message keys, aggregates them by chat & sender. Useful for sending read receipts in bulk */
|
||||||
export const aggregateMessageKeysNotFromMe = (keys: WAMessageKey[]) => {
|
export const aggregateMessageKeysNotFromMe = (keys: WAMessageKey[]) => {
|
||||||
const keyMap: { [id: string]: { jid: string; participant: string | undefined; messageIds: string[] } } = {}
|
const keyMap: { [id: string]: { jid: string; participant: string | undefined; messageIds: string[] } } = {}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import type {
|
|||||||
RequestJoinMethod,
|
RequestJoinMethod,
|
||||||
SignalKeyStoreWithTransaction,
|
SignalKeyStoreWithTransaction,
|
||||||
SignalRepositoryWithLIDStore,
|
SignalRepositoryWithLIDStore,
|
||||||
|
SocketConfig,
|
||||||
WAMessage,
|
WAMessage,
|
||||||
WAMessageKey
|
WAMessageKey
|
||||||
} from '../Types'
|
} from '../Types'
|
||||||
@@ -23,12 +24,13 @@ import {
|
|||||||
isHostedPnUser,
|
isHostedPnUser,
|
||||||
isJidBroadcast,
|
isJidBroadcast,
|
||||||
isJidStatusBroadcast,
|
isJidStatusBroadcast,
|
||||||
|
isLidUser,
|
||||||
jidDecode,
|
jidDecode,
|
||||||
jidEncode,
|
jidEncode,
|
||||||
jidNormalizedUser
|
jidNormalizedUser
|
||||||
} from '../WABinary'
|
} from '../WABinary'
|
||||||
import { aesDecryptGCM, hmacSign } from './crypto'
|
import { aesDecryptGCM, hmacSign } from './crypto'
|
||||||
import { toNumber } from './generics'
|
import { getKeyAuthor, toNumber } from './generics'
|
||||||
import { downloadAndProcessHistorySyncNotification } from './history'
|
import { downloadAndProcessHistorySyncNotification } from './history'
|
||||||
import type { ILogger } from './logger'
|
import type { ILogger } from './logger'
|
||||||
|
|
||||||
@@ -41,6 +43,7 @@ type ProcessMessageContext = {
|
|||||||
logger?: ILogger
|
logger?: ILogger
|
||||||
options: RequestInit
|
options: RequestInit
|
||||||
signalRepository: SignalRepositoryWithLIDStore
|
signalRepository: SignalRepositoryWithLIDStore
|
||||||
|
getMessage: SocketConfig['getMessage']
|
||||||
}
|
}
|
||||||
|
|
||||||
const REAL_MSG_STUB_TYPES = new Set([
|
const REAL_MSG_STUB_TYPES = new Set([
|
||||||
@@ -144,6 +147,17 @@ type PollContext = {
|
|||||||
voterJid: string
|
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
|
* Decrypt a poll vote
|
||||||
* @param vote encrypted 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 (
|
const processMessage = async (
|
||||||
message: WAMessage,
|
message: WAMessage,
|
||||||
{
|
{
|
||||||
@@ -184,7 +228,8 @@ const processMessage = async (
|
|||||||
signalRepository,
|
signalRepository,
|
||||||
keyStore,
|
keyStore,
|
||||||
logger,
|
logger,
|
||||||
options
|
options,
|
||||||
|
getMessage
|
||||||
}: ProcessMessageContext
|
}: ProcessMessageContext
|
||||||
) => {
|
) => {
|
||||||
const meId = creds.me!.id
|
const meId = creds.me!.id
|
||||||
@@ -363,6 +408,60 @@ const processMessage = async (
|
|||||||
key: content.reactionMessage?.key!
|
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) {
|
} else if (message.messageStubType) {
|
||||||
const jid = message.key?.remoteJid!
|
const jid = message.key?.remoteJid!
|
||||||
//let actor = whatsappID (message.participant)
|
//let actor = whatsappID (message.participant)
|
||||||
|
|||||||
Reference in New Issue
Block a user