fix: harden Protobuf Deserialization and Refactor Utilities (#1820)

* refactor: reorganize browser utility functions and improve buffer handling

* fix: harden protobuf deserialization and clean up code

* refactor: simplify data handling in GroupCipher and SenderKey classes

* fix: Ensure consistent Buffer hydration and remove redundant code

* refactor: update decodeAndHydrate calls to use proto types for improved type safety
This commit is contained in:
João Lucas de Oliveira Lopes
2025-09-25 22:45:30 -03:00
committed by GitHub
parent 202c39f3fe
commit 3b46d43beb
23 changed files with 194 additions and 160 deletions
+31
View File
@@ -0,0 +1,31 @@
import { platform, release } from 'os'
import { proto } from '../../WAProto/index.js'
import type { BrowsersMap } from '../Types'
const PLATFORM_MAP = {
aix: 'AIX',
darwin: 'Mac OS',
win32: 'Windows',
android: 'Android',
freebsd: 'FreeBSD',
openbsd: 'OpenBSD',
sunos: 'Solaris',
linux: undefined,
haiku: undefined,
cygwin: undefined,
netbsd: undefined
}
export const Browsers: BrowsersMap = {
ubuntu: browser => ['Ubuntu', browser, '22.04.4'],
macOS: browser => ['Mac OS', browser, '14.4.1'],
baileys: browser => ['Baileys', browser, '6.5.0'],
windows: browser => ['Windows', browser, '10.0.22631'],
/** The appropriate browser based on your OS & release */
appropriate: browser => [PLATFORM_MAP[platform()] || 'Ubuntu', browser, release()]
}
export const getPlatformId = (browser: string) => {
const platformType = proto.DeviceProps.PlatformType[browser.toUpperCase() as any]
return platformType ? platformType.toString() : '1' //chrome
}
+6 -5
View File
@@ -25,6 +25,7 @@ import { toNumber } from './generics'
import type { ILogger } from './logger'
import { LT_HASH_ANTI_TAMPERING } from './lt-hash'
import { downloadContentFromMessage } from './messages-media'
import { decodeAndHydrate } from './proto-utils'
type FetchAppStateSyncKey = (keyId: string) => Promise<proto.Message.IAppStateSyncKeyData | null | undefined>
@@ -233,7 +234,7 @@ export const decodeSyncdMutations = async (
}
const result = aesDecrypt(encContent, key.valueEncryptionKey)
const syncAction = proto.SyncActionData.decode(result)
const syncAction = decodeAndHydrate(proto.SyncActionData, result)
if (validateMacs) {
const hmac = hmacSign(syncAction.index, key.indexKey)
@@ -327,9 +328,9 @@ export const extractSyncdPatches = async (result: BinaryNode, options: AxiosRequ
snapshotNode.content = Buffer.from(Object.values(snapshotNode.content))
}
const blobRef = proto.ExternalBlobReference.decode(snapshotNode.content as Buffer)
const blobRef = decodeAndHydrate(proto.ExternalBlobReference, snapshotNode.content as Buffer)
const data = await downloadExternalBlob(blobRef, options)
snapshot = proto.SyncdSnapshot.decode(data)
snapshot = decodeAndHydrate(proto.SyncdSnapshot, data)
}
for (let { content } of patches) {
@@ -338,7 +339,7 @@ export const extractSyncdPatches = async (result: BinaryNode, options: AxiosRequ
content = Buffer.from(Object.values(content))
}
const syncd = proto.SyncdPatch.decode(content as Uint8Array)
const syncd = decodeAndHydrate(proto.SyncdPatch, content as Uint8Array)
if (!syncd.version) {
syncd.version = { version: +collectionNode.attrs.version! + 1 }
}
@@ -366,7 +367,7 @@ export const downloadExternalBlob = async (blob: proto.IExternalBlobReference, o
export const downloadExternalPatch = async (blob: proto.IExternalBlobReference, options: AxiosRequestConfig<{}>) => {
const buffer = await downloadExternalBlob(blob, options)
const syncData = proto.SyncdMutations.decode(buffer)
const syncData = decodeAndHydrate(proto.SyncdMutations, buffer)
return syncData
}
+4 -2
View File
@@ -16,6 +16,7 @@ import {
} from '../WABinary'
import { unpadRandomMax16 } from './generics'
import type { ILogger } from './logger'
import { decodeAndHydrate } from './proto-utils'
const getDecryptionJid = async (sender: string, repository: SignalRepositoryWithLIDStore): Promise<string> => {
if (!sender.includes('@s.whatsapp.net')) {
@@ -220,7 +221,7 @@ export const decryptMessageNode = (
if (Array.isArray(stanza.content)) {
for (const { tag, attrs, content } of stanza.content) {
if (tag === 'verified_name' && content instanceof Uint8Array) {
const cert = proto.VerifiedNameCertificate.decode(content)
const cert = decodeAndHydrate(proto.VerifiedNameCertificate, content)
const details = proto.VerifiedNameCertificate.Details.decode(cert.details)
fullMessage.verifiedBizName = details.verifiedName
}
@@ -273,7 +274,8 @@ export const decryptMessageNode = (
throw new Error(`Unknown e2e type: ${e2eType}`)
}
let msg: proto.IMessage = proto.Message.decode(
let msg: proto.IMessage = decodeAndHydrate(
proto.Message,
e2eType !== 'plaintext' ? unpadRandomMax16(msgBuffer) : msgBuffer
)
msg = msg.deviceSentMessage?.message || msg
+13 -40
View File
@@ -1,49 +1,13 @@
import { Boom } from '@hapi/boom'
import axios, { type AxiosRequestConfig } from 'axios'
import { createHash, randomBytes } from 'crypto'
import { platform, release } from 'os'
import { proto } from '../../WAProto/index.js'
const baileysVersion = [2, 3000, 1023223821]
import type {
BaileysEventEmitter,
BaileysEventMap,
BrowsersMap,
ConnectionState,
WACallUpdateType,
WAVersion
} from '../Types'
import type { BaileysEventEmitter, BaileysEventMap, ConnectionState, WACallUpdateType, WAVersion } from '../Types'
import { DisconnectReason } from '../Types'
import { type BinaryNode, getAllBinaryNodeChildren, jidDecode } from '../WABinary'
import { sha256 } from './crypto'
const PLATFORM_MAP = {
aix: 'AIX',
darwin: 'Mac OS',
win32: 'Windows',
android: 'Android',
freebsd: 'FreeBSD',
openbsd: 'OpenBSD',
sunos: 'Solaris',
linux: undefined,
haiku: undefined,
cygwin: undefined,
netbsd: undefined
}
export const Browsers: BrowsersMap = {
ubuntu: browser => ['Ubuntu', browser, '22.04.4'],
macOS: browser => ['Mac OS', browser, '14.4.1'],
baileys: browser => ['Baileys', browser, '6.5.0'],
windows: browser => ['Windows', browser, '10.0.22631'],
/** The appropriate browser based on your OS & release */
appropriate: browser => [PLATFORM_MAP[platform()] || 'Ubuntu', browser, release()]
}
export const getPlatformId = (browser: string) => {
const platformType = proto.DeviceProps.PlatformType[browser.toUpperCase() as any]
return platformType ? platformType.toString() : '1' //chrome
}
export const BufferJSON = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
replacer: (k: any, value: any) => {
@@ -56,9 +20,18 @@ export const BufferJSON = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
reviver: (_: any, value: any) => {
if (typeof value === 'object' && !!value && (value.buffer === true || value.type === 'Buffer')) {
const val = value.data || value.value
return typeof val === 'string' ? Buffer.from(val, 'base64') : Buffer.from(val || [])
if (typeof value === 'object' && value !== null && value.type === 'Buffer' && typeof value.data === 'string') {
return Buffer.from(value.data, 'base64')
}
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
const keys = Object.keys(value)
if (keys.length > 0 && keys.every(k => !isNaN(parseInt(k, 10)))) {
const values = Object.values(value)
if (values.every(v => typeof v === 'number')) {
return Buffer.from(values)
}
}
}
return value
+2 -1
View File
@@ -7,6 +7,7 @@ import { WAMessageStubType } from '../Types'
import { toNumber } from './generics'
import { normalizeMessageContent } from './messages'
import { downloadContentFromMessage } from './messages-media'
import { decodeAndHydrate } from './proto-utils'
const inflatePromise = promisify(inflate)
@@ -22,7 +23,7 @@ export const downloadHistory = async (msg: proto.Message.IHistorySyncNotificatio
// decompress buffer
buffer = await inflatePromise(buffer)
const syncData = proto.HistorySync.decode(buffer)
const syncData = decodeAndHydrate(proto.HistorySync, buffer)
return syncData
}
+2 -1
View File
@@ -27,6 +27,7 @@ import { type BinaryNode, getBinaryNodeChild, getBinaryNodeChildBuffer, jidNorma
import { aesDecryptGCM, aesEncryptGCM, hkdf } from './crypto'
import { generateMessageIDV2 } from './generics'
import type { ILogger } from './logger'
import { decodeAndHydrate } from './proto-utils'
const getTmpFilesDirectory = () => tmpdir()
@@ -783,7 +784,7 @@ export const decryptMediaRetryData = async (
) => {
const retryKey = await getMediaRetryKey(mediaKey)
const plaintext = aesDecryptGCM(ciphertext, retryKey, iv, Buffer.from(msgId))
return proto.MediaRetryNotification.decode(plaintext)
return decodeAndHydrate(proto.MediaRetryNotification, plaintext)
}
export const getStatusCodeForMediaRetry = (code: number) =>
+3 -2
View File
@@ -40,6 +40,7 @@ import {
getRawMediaUploadData,
type MediaDownloadOptions
} from './messages-media'
import { decodeAndHydrate } from './proto-utils'
type MediaUploadData = {
media: WAMediaUpload
@@ -156,7 +157,7 @@ export const prepareWAMessageMedia = async (
if (mediaBuff) {
logger?.debug({ cacheableKey }, 'got media cache hit')
const obj = WAProto.Message.decode(mediaBuff)
const obj = decodeAndHydrate(proto.Message, mediaBuff)
const key = `${mediaType}Message`
Object.assign(obj[key as keyof proto.Message]!, { ...uploadData, media: undefined })
@@ -339,7 +340,7 @@ export const generateForwardMessageContent = (message: WAMessage, forceForward?:
// hacky copy
content = normalizeMessageContent(content)
content = proto.Message.decode(proto.Message.encode(content!).finish())
content = decodeAndHydrate(proto.Message, proto.Message.encode(content!).finish())
let key = Object.keys(content)[0] as keyof proto.IMessage
+2 -1
View File
@@ -6,6 +6,7 @@ import type { BinaryNode } from '../WABinary'
import { decodeBinaryNode } from '../WABinary'
import { aesDecryptGCM, aesEncryptGCM, Curve, hkdf, sha256 } from './crypto'
import type { ILogger } from './logger'
import { decodeAndHydrate } from './proto-utils'
const generateIV = (counter: number) => {
const iv = new ArrayBuffer(12)
@@ -112,7 +113,7 @@ export const makeNoiseHandler = ({
const certDecoded = decrypt(serverHello!.payload!)
const { intermediate: certIntermediate } = proto.CertChain.decode(certDecoded)
const { intermediate: certIntermediate } = decodeAndHydrate(proto.CertChain, certDecoded)
const { issuerSerial } = proto.CertChain.NoiseCertificate.Details.decode(certIntermediate!.details!)
+6 -3
View File
@@ -19,6 +19,7 @@ import { aesDecryptGCM, hmacSign } from './crypto'
import { toNumber } from './generics'
import { downloadAndProcessHistorySyncNotification } from './history'
import type { ILogger } from './logger'
import { decodeAndHydrate } from './proto-utils'
type ProcessMessageContext = {
shouldProcessHistoryMsg: boolean
@@ -271,7 +272,7 @@ const processMessage = async (
const { placeholderMessageResendResponse: retryResponse } = result
//eslint-disable-next-line max-depth
if (retryResponse) {
const webMessageInfo = proto.WebMessageInfo.decode(retryResponse.webMessageInfoBytes!)
const webMessageInfo = decodeAndHydrate(proto.WebMessageInfo, retryResponse.webMessageInfoBytes!)
// wait till another upsert event is available, don't want it to be part of the PDO response message
setTimeout(() => {
ev.emit('messages.upsert', {
@@ -305,8 +306,10 @@ const processMessage = async (
break
case proto.Message.ProtocolMessage.Type.LID_MIGRATION_MAPPING_SYNC:
const encodedPayload = protocolMsg.lidMigrationMappingSyncMessage?.encodedMappingPayload!
const { pnToLidMappings, chatDbMigrationTimestamp } =
proto.LIDMigrationMappingSyncPayload.decode(encodedPayload)
const { pnToLidMappings, chatDbMigrationTimestamp } = decodeAndHydrate(
proto.LIDMigrationMappingSyncPayload,
encodedPayload
)
logger?.debug({ pnToLidMappings, chatDbMigrationTimestamp }, 'got lid mappings and chat db migration timestamp')
const pairs = []
for (const { pn, latestLid, assignedLid } of pnToLidMappings) {
+12
View File
@@ -0,0 +1,12 @@
import { BufferJSON } from './generics'
type ProtoMessageClass<T> = {
new (...args: any[]): T
decode(buffer: Uint8Array | Buffer): T
}
export function decodeAndHydrate<T>(MessageType: ProtoMessageClass<T>, buffer: Uint8Array | Buffer): T {
const decoded = MessageType.decode(buffer)
const hydrated = JSON.parse(JSON.stringify(decoded, BufferJSON.replacer), BufferJSON.reviver)
return hydrated as T
}
+7 -3
View File
@@ -6,6 +6,7 @@ import type { AuthenticationCreds, SignalCreds, SocketConfig } from '../Types'
import { type BinaryNode, getBinaryNodeChild, jidDecode, S_WHATSAPP_NET } from '../WABinary'
import { Curve, hmacSign } from './crypto'
import { encodeBigEndian } from './generics'
import { decodeAndHydrate } from './proto-utils'
import { createSignalIdentity } from './signal'
const getUserAgent = (config: SocketConfig): proto.ClientPayload.IUserAgent => {
@@ -135,7 +136,10 @@ export const configureSuccessfulPairing = (
const jid = deviceNode.attrs.jid
const lid = deviceNode.attrs.lid
const { details, hmac, accountType } = proto.ADVSignedDeviceIdentityHMAC.decode(deviceIdentityNode.content as Buffer)
const { details, hmac, accountType } = decodeAndHydrate(
proto.ADVSignedDeviceIdentityHMAC,
deviceIdentityNode.content as Buffer
)
const isHostedAccount = accountType !== undefined && accountType === proto.ADVEncryptionType.HOSTED
const hmacPrefix = isHostedAccount ? Buffer.from([6, 5]) : Buffer.alloc(0)
@@ -144,7 +148,7 @@ export const configureSuccessfulPairing = (
throw new Boom('Invalid account signature')
}
const account = proto.ADVSignedDeviceIdentity.decode(details)
const account = decodeAndHydrate(proto.ADVSignedDeviceIdentity, details)
const { accountSignatureKey, accountSignature, details: deviceDetails } = account
const accountMsg = Buffer.concat([Buffer.from([6, 0]), deviceDetails, signedIdentityKey.public])
if (!Curve.verify(accountSignatureKey, accountMsg, accountSignature)) {
@@ -158,7 +162,7 @@ export const configureSuccessfulPairing = (
const identity = createSignalIdentity(lid!, accountSignatureKey)
const accountEnc = encodeSignedDeviceIdentity(account, false)
const deviceIdentity = proto.ADVDeviceIdentity.decode(account.details)
const deviceIdentity = decodeAndHydrate(proto.ADVDeviceIdentity, account.details)
const reply: BinaryNode = {
tag: 'iq',