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:
committed by
GitHub
parent
202c39f3fe
commit
3b46d43beb
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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!)
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user