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
@@ -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,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]
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 || [])
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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({})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user