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
+3
View File
@@ -333,6 +333,9 @@ sock.ev.on('messages.upsert', ({ messages }) => {
> [!NOTE]
> This example includes basic auth storage too
> [!NOTE]
> For reliable serialization of the authentication state, especially when storing as JSON, always use the BufferJSON utility.
```ts
import makeWASocket, { DisconnectReason, useMultiFileAuthState } from '@whiskeysockets/baileys'
import { Boom } from '@hapi/boom'
+1 -1
View File
@@ -1,7 +1,7 @@
import { proto } from '../../WAProto/index.js'
import { makeLibSignalRepository } from '../Signal/libsignal'
import type { AuthenticationState, SocketConfig, WAVersion } from '../Types'
import { Browsers } from '../Utils'
import { Browsers } from '../Utils/browser-utils'
import logger from '../Utils/logger'
const version = [2, 3000, 1023223821]
+3 -10
View File
@@ -19,7 +19,7 @@ export class GroupCipher {
this.senderKeyName = senderKeyName
}
public async encrypt(paddedPlaintext: Uint8Array | string): Promise<Uint8Array> {
public async encrypt(paddedPlaintext: Uint8Array): Promise<Uint8Array> {
const record = await this.senderKeyStore.loadSenderKey(this.senderKeyName)
if (!record) {
throw new Error('No SenderKeyRecord found for encryption')
@@ -107,16 +107,9 @@ export class GroupCipher {
}
}
private async getCipherText(
iv: Uint8Array | string,
key: Uint8Array | string,
plaintext: Uint8Array | string
): Promise<Buffer> {
private async getCipherText(iv: Uint8Array, key: Uint8Array, plaintext: Uint8Array): Promise<Buffer> {
try {
const ivBuffer = typeof iv === 'string' ? Buffer.from(iv, 'base64') : iv
const keyBuffer = typeof key === 'string' ? Buffer.from(key, 'base64') : key
const plaintextBuffer = typeof plaintext === 'string' ? Buffer.from(plaintext) : plaintext
return encrypt(keyBuffer, plaintextBuffer, ivBuffer)
return encrypt(key, plaintext, iv)
} catch (e) {
throw new Error('InvalidMessageException')
}
+1 -10
View File
@@ -9,16 +9,7 @@ export class SenderChainKey {
constructor(iteration: number, chainKey: Uint8Array | Buffer) {
this.iteration = iteration
if (Buffer.isBuffer(chainKey)) {
this.chainKey = chainKey
} else if (chainKey instanceof Uint8Array) {
this.chainKey = Buffer.from(chainKey)
} else if (chainKey && typeof chainKey === 'object') {
// backported from @MartinSchere (#1741)
this.chainKey = Buffer.from(Object.values(chainKey)) // temp fix // backported from @MartinSchere (#1741)
} else {
this.chainKey = Buffer.alloc(0)
}
this.chainKey = Buffer.from(chainKey)
}
public getIteration(): number {
@@ -1,4 +1,5 @@
import { proto } from '../../../WAProto/index.js'
import { decodeAndHydrate } from '../../Utils/proto-utils'
import { CiphertextMessage } from './ciphertext-message'
export class SenderKeyDistributionMessage extends CiphertextMessage {
@@ -20,19 +21,13 @@ export class SenderKeyDistributionMessage extends CiphertextMessage {
if (serialized) {
try {
const message = serialized.slice(1)
const distributionMessage = proto.SenderKeyDistributionMessage.decode(message)
const distributionMessage = decodeAndHydrate(proto.SenderKeyDistributionMessage, message)
this.serialized = serialized
this.id = distributionMessage.id
this.iteration = distributionMessage.iteration
this.chainKey =
typeof distributionMessage.chainKey === 'string'
? Buffer.from(distributionMessage.chainKey, 'base64')
: distributionMessage.chainKey
this.signatureKey =
typeof distributionMessage.signingKey === 'string'
? Buffer.from(distributionMessage.signingKey, 'base64')
: distributionMessage.signingKey
this.chainKey = distributionMessage.chainKey
this.signatureKey = distributionMessage.signingKey
} catch (e) {
throw new Error(String(e))
}
@@ -73,11 +68,11 @@ export class SenderKeyDistributionMessage extends CiphertextMessage {
}
public getChainKey(): Uint8Array {
return typeof this.chainKey === 'string' ? Buffer.from(this.chainKey, 'base64') : this.chainKey
return this.chainKey
}
public getSignatureKey(): Uint8Array {
return typeof this.signatureKey === 'string' ? Buffer.from(this.signatureKey, 'base64') : this.signatureKey
return this.signatureKey
}
public getId(): number {
+3 -5
View File
@@ -1,5 +1,6 @@
import { calculateSignature, verifySignature } from 'libsignal/src/curve'
import { proto } from '../../../WAProto/index.js'
import { decodeAndHydrate } from '../../Utils/proto-utils'
import { CiphertextMessage } from './ciphertext-message'
export class SenderKeyMessage extends CiphertextMessage {
@@ -24,16 +25,13 @@ export class SenderKeyMessage extends CiphertextMessage {
const version = serialized[0]!
const message = serialized.slice(1, serialized.length - this.SIGNATURE_LENGTH)
const signature = serialized.slice(-1 * this.SIGNATURE_LENGTH)
const senderKeyMessage = proto.SenderKeyMessage.decode(message)
const senderKeyMessage = decodeAndHydrate(proto.SenderKeyMessage, message)
this.serialized = serialized
this.messageVersion = (version & 0xff) >> 4
this.keyId = senderKeyMessage.id
this.iteration = senderKeyMessage.iteration
this.ciphertext =
typeof senderKeyMessage.ciphertext === 'string'
? Buffer.from(senderKeyMessage.ciphertext, 'base64')
: senderKeyMessage.ciphertext
this.ciphertext = senderKeyMessage.ciphertext
this.signature = signature
} else {
const version = (((this.CURRENT_VERSION << 4) | this.CURRENT_VERSION) & 0xff) % 256
+3 -11
View File
@@ -61,17 +61,9 @@ export class SenderKeyRecord {
public serialize(): SenderKeyStateStructure[] {
return this.senderKeyStates.map(state => state.getStructure())
}
static deserialize(data: Uint8Array | string | SenderKeyStateStructure[]): SenderKeyRecord {
let parsed: SenderKeyStateStructure[]
if (typeof data === 'string') {
parsed = JSON.parse(data, BufferJSON.reviver)
} else if (data instanceof Uint8Array) {
const str = Buffer.from(data).toString('utf-8')
parsed = JSON.parse(str, BufferJSON.reviver)
} else {
parsed = data
}
static deserialize(data: Uint8Array): SenderKeyRecord {
const str = Buffer.from(data).toString('utf-8')
const parsed = JSON.parse(str, BufferJSON.reviver)
return new SenderKeyRecord(parsed)
}
}
+9 -50
View File
@@ -36,13 +36,6 @@ export class SenderKeyState {
signatureKeyPrivate?: Uint8Array | string | null,
senderKeyStateStructure?: SenderKeyStateStructure | null
) {
const toBuffer = (val: any) => {
if (!val) return Buffer.alloc(0)
if (typeof val === 'string') return Buffer.from(val, 'base64')
if (val instanceof Uint8Array || Array.isArray(val)) return Buffer.from(val)
return Buffer.alloc(0)
}
if (senderKeyStateStructure) {
this.senderKeyStateStructure = {
...senderKeyStateStructure,
@@ -56,22 +49,16 @@ export class SenderKeyState {
signatureKeyPrivate = signatureKeyPair.private
}
chainKey = typeof chainKey === 'string' ? Buffer.from(chainKey, 'base64') : chainKey
const senderChainKeyStructure: SenderChainKeyStructure = {
iteration: iteration || 0,
seed: chainKey ? toBuffer(chainKey) : Buffer.alloc(0)
}
const signingKeyStructure: SenderSigningKeyStructure = {
public: toBuffer(signatureKeyPublic),
private: signatureKeyPrivate ? toBuffer(signatureKeyPrivate) : undefined
}
this.senderKeyStateStructure = {
senderKeyId: id || 0,
senderChainKey: senderChainKeyStructure,
senderSigningKey: signingKeyStructure,
senderChainKey: {
iteration: iteration || 0,
seed: Buffer.from(chainKey || [])
},
senderSigningKey: {
public: Buffer.from(signatureKeyPublic || []),
private: Buffer.from(signatureKeyPrivate || [])
},
senderMessageKeys: []
}
}
@@ -96,22 +83,7 @@ export class SenderKeyState {
}
public getSigningKeyPublic(): Buffer {
let key = this.senderKeyStateStructure.senderSigningKey.public
// normalize into Buffer
if (!Buffer.isBuffer(key)) {
if (key instanceof Uint8Array) {
key = Buffer.from(key)
} else if (typeof key === 'string') {
key = Buffer.from(key, 'base64')
} else if (key && typeof key === 'object') {
return Buffer.from(Object.values(key)) // temp fix // inspired by @MartinSchere 's #1741
} else {
key = Buffer.from(key || [])
}
}
const publicKey = key as Buffer
const publicKey = Buffer.from(this.senderKeyStateStructure.senderSigningKey.public)
if (publicKey.length === 32) {
const fixed = Buffer.alloc(33)
@@ -125,19 +97,6 @@ export class SenderKeyState {
public getSigningKeyPrivate(): Buffer | undefined {
const privateKey = this.senderKeyStateStructure.senderSigningKey.private
if (!privateKey) {
return undefined
}
if (Buffer.isBuffer(privateKey)) {
return privateKey
} else if (privateKey instanceof Uint8Array) {
return Buffer.from(privateKey)
} else if (privateKey && typeof privateKey === 'object') {
return Buffer.from(Object.values(privateKey)) // temp fix // inspired by @MartinSchere 's #1741
} else if (typeof privateKey === 'string') {
return Buffer.from(privateKey, 'base64')
}
return Buffer.from(privateKey || [])
}
+2 -1
View File
@@ -39,6 +39,7 @@ import {
xmppSignedPreKey
} from '../Utils'
import { makeMutex } from '../Utils/make-mutex'
import { decodeAndHydrate } from '../Utils/proto-utils'
import {
areJidsSameUser,
type BinaryNode,
@@ -293,7 +294,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
typeof plaintextNode.content === 'string'
? Buffer.from(plaintextNode.content, 'binary')
: Buffer.from(plaintextNode.content as Uint8Array)
const messageProto = proto.Message.decode(contentBuf)
const messageProto = decodeAndHydrate(proto.Message, contentBuf)
const fullMessage = proto.WebMessageInfo.create({
key: {
remoteJid: from,
+3 -2
View File
@@ -28,11 +28,12 @@ import {
getCodeFromWSError,
getErrorCodeFromStreamError,
getNextPreKeysNode,
getPlatformId,
makeEventBuffer,
makeNoiseHandler,
promiseTimeout
} from '../Utils'
import { getPlatformId } from '../Utils/browser-utils'
import { decodeAndHydrate } from '../Utils/proto-utils'
import {
assertNodeErrorFree,
type BinaryNode,
@@ -342,7 +343,7 @@ export const makeSocket = (config: SocketConfig) => {
const init = proto.HandshakeMessage.encode(helloMsg).finish()
const result = await awaitNextMessage<Uint8Array>(init)
const handshake = proto.HandshakeMessage.decode(result)
const handshake = decodeAndHydrate(proto.HandshakeMessage, result)
logger.trace({ handshake }, 'handshake recv from WA')
+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',
+2 -1
View File
@@ -1,5 +1,6 @@
import { Boom } from '@hapi/boom'
import { proto } from '../../WAProto/index.js'
import { decodeAndHydrate } from '../Utils/proto-utils'
import { type BinaryNode } from './types'
// some extra useful utilities
@@ -78,7 +79,7 @@ export const getBinaryNodeMessages = ({ content }: BinaryNode) => {
if (Array.isArray(content)) {
for (const item of content) {
if (item.tag === 'message') {
msgs.push(proto.WebMessageInfo.decode(item.content as Buffer))
msgs.push(decodeAndHydrate(proto.WebMessageInfo, item.content as Buffer))
}
}
}
+70
View File
@@ -0,0 +1,70 @@
import { BufferJSON } from '../../Utils/generics'
describe('BufferJSON', () => {
const originalObject = {
id: 1,
key: Buffer.from([1, 2, 3, 4, 5]),
nested: {
data: Buffer.from([6, 7, 8])
}
}
const legacyJsonString = '{"id":1,"key":{"0":1,"1":2,"2":3,"3":4,"4":5},"nested":{"data":{"0":6,"1":7,"2":8}}}'
it('should correctly perform a round trip (stringify with replacer, parse with reviver)', () => {
const serialized = JSON.stringify(originalObject, BufferJSON.replacer)
expect(serialized).toContain('"type":"Buffer"')
expect(serialized).not.toContain('"data":[1,2,3,4,5]')
const revived = JSON.parse(serialized, BufferJSON.reviver)
expect(Buffer.isBuffer(revived.key)).toBe(true)
expect(Buffer.isBuffer(revived.nested.data)).toBe(true)
expect(revived.key).toEqual(originalObject.key)
expect(revived).toEqual(originalObject)
})
it('should correctly revive a legacy JSON string with object-like buffers', () => {
const revived = JSON.parse(legacyJsonString, BufferJSON.reviver)
expect(Buffer.isBuffer(revived.key)).toBe(true)
expect(Buffer.isBuffer(revived.nested.data)).toBe(true)
expect(revived.key).toEqual(originalObject.key)
expect(revived.nested.data).toEqual(originalObject.nested.data)
expect(revived.id).toEqual(originalObject.id)
})
it('should not corrupt legitimate objects that are not buffers', () => {
const legitimateProtoObject = {
'0': 'some-value',
'1': 'another-value'
}
const jsonString = JSON.stringify(legitimateProtoObject)
const revived = JSON.parse(jsonString, BufferJSON.reviver)
expect(Buffer.isBuffer(revived)).toBe(false)
expect(revived).toEqual(legitimateProtoObject)
})
it('should not convert other objects or null values', () => {
const otherObject = {
a: 1,
b: 2,
c: null,
d: { '0': 1, foo: 'bar' }
}
const jsonString = JSON.stringify(otherObject)
const revived = JSON.parse(jsonString, BufferJSON.reviver)
expect(Buffer.isBuffer(revived.a)).toBe(false)
expect(revived.c).toBeNull()
expect(Buffer.isBuffer(revived.d)).toBe(false)
expect(revived).toEqual(otherObject)
})
it('should correctly handle an empty object', () => {
const revived = JSON.parse('{}', BufferJSON.reviver)
expect(revived).toEqual({})
})
})