From 3c278b35f038365e05a0266926c7efbb4ebb8ee3 Mon Sep 17 00:00:00 2001 From: Adhiraj Singh Date: Sun, 24 Apr 2022 16:49:13 +0530 Subject: [PATCH] fix: add legacy constants for decoding --- src/Utils/validate-connection.ts | 2 +- src/WABinary/Legacy/constants.ts | 205 +++++++++++++++++ src/WABinary/Legacy/index.ts | 377 +------------------------------ src/WABinary/constants.ts | 7 +- src/WABinary/decode.ts | 15 +- src/WABinary/encode.ts | 13 +- src/WABinary/types.ts | 5 +- 7 files changed, 239 insertions(+), 385 deletions(-) create mode 100644 src/WABinary/Legacy/constants.ts diff --git a/src/Utils/validate-connection.ts b/src/Utils/validate-connection.ts index c759d4d..d637bc8 100644 --- a/src/Utils/validate-connection.ts +++ b/src/Utils/validate-connection.ts @@ -10,7 +10,7 @@ import { createSignalIdentity } from './signal' type ClientPayloadConfig = Pick -const getUserAgent = ({ version, browser }: ClientPayloadConfig): proto.IUserAgent => { +const getUserAgent = ({ version }: ClientPayloadConfig): proto.IUserAgent => { const osVersion = '0.1' return { appVersion: { diff --git a/src/WABinary/Legacy/constants.ts b/src/WABinary/Legacy/constants.ts new file mode 100644 index 0000000..ad9e943 --- /dev/null +++ b/src/WABinary/Legacy/constants.ts @@ -0,0 +1,205 @@ + +export const TAGS = { + LIST_EMPTY: 0, + STREAM_END: 2, + DICTIONARY_0: 236, + DICTIONARY_1: 237, + DICTIONARY_2: 238, + DICTIONARY_3: 239, + LIST_8: 248, + LIST_16: 249, + JID_PAIR: 250, + HEX_8: 251, + BINARY_8: 252, + BINARY_20: 253, + BINARY_32: 254, + NIBBLE_8: 255, + SINGLE_BYTE_MAX: 256, + PACKED_MAX: 254, + AD_JID: 247, +} +export const DOUBLE_BYTE_TOKENS = [] +export const SINGLE_BYTE_TOKENS = [ + null, + null, + null, + '200', + '400', + '404', + '500', + '501', + '502', + 'action', + 'add', + 'after', + 'archive', + 'author', + 'available', + 'battery', + 'before', + 'body', + 'broadcast', + 'chat', + 'clear', + 'code', + 'composing', + 'contacts', + 'count', + 'create', + 'debug', + 'delete', + 'demote', + 'duplicate', + 'encoding', + 'error', + 'false', + 'filehash', + 'from', + 'g.us', + 'group', + 'groups_v2', + 'height', + 'id', + 'image', + 'in', + 'index', + 'invis', + 'item', + 'jid', + 'kind', + 'last', + 'leave', + 'live', + 'log', + 'media', + 'message', + 'mimetype', + 'missing', + 'modify', + 'name', + 'notification', + 'notify', + 'out', + 'owner', + 'participant', + 'paused', + 'picture', + 'played', + 'presence', + 'preview', + 'promote', + 'query', + 'raw', + 'read', + 'receipt', + 'received', + 'recipient', + 'recording', + 'relay', + 'remove', + 'response', + 'resume', + 'retry', + 's.whatsapp.net', + 'seconds', + 'set', + 'size', + 'status', + 'subject', + 'subscribe', + 't', + 'text', + 'to', + 'true', + 'type', + 'unarchive', + 'unavailable', + 'url', + 'user', + 'value', + 'web', + 'width', + 'mute', + 'read_only', + 'admin', + 'creator', + 'short', + 'update', + 'powersave', + 'checksum', + 'epoch', + 'block', + 'previous', + '409', + 'replaced', + 'reason', + 'spam', + 'modify_tag', + 'message_info', + 'delivery', + 'emoji', + 'title', + 'description', + 'canonical-url', + 'matched-text', + 'star', + 'unstar', + 'media_key', + 'filename', + 'identity', + 'unread', + 'page', + 'page_count', + 'search', + 'media_message', + 'security', + 'call_log', + 'profile', + 'ciphertext', + 'invite', + 'gif', + 'vcard', + 'frequent', + 'privacy', + 'blacklist', + 'whitelist', + 'verify', + 'location', + 'document', + 'elapsed', + 'revoke_invite', + 'expiration', + 'unsubscribe', + 'disable', + 'vname', + 'old_jid', + 'new_jid', + 'announcement', + 'locked', + 'prop', + 'label', + 'color', + 'call', + 'offer', + 'call-id', + 'quick_reply', + 'sticker', + 'pay_t', + 'accept', + 'reject', + 'sticker_pack', + 'invalid', + 'canceled', + 'missed', + 'connected', + 'result', + 'audio', + 'video', + 'recent', +] + +export const TOKEN_MAP: { [token: string]: { dict?: number, index: number } } = { } + +for(let i = 0;i < SINGLE_BYTE_TOKENS.length;i++) { + TOKEN_MAP[SINGLE_BYTE_TOKENS[i]] = { index: i } +} \ No newline at end of file diff --git a/src/WABinary/Legacy/index.ts b/src/WABinary/Legacy/index.ts index 5c4c478..a2a6f4a 100644 --- a/src/WABinary/Legacy/index.ts +++ b/src/WABinary/Legacy/index.ts @@ -1,376 +1,13 @@ -import { DOUBLE_BYTE_TOKENS, SINGLE_BYTE_TOKENS, TAGS } from '../constants' +import { decodeDecompressedBinaryNode } from '../decode' +import { encodeBinaryNode } from '../encode' import type { BinaryNode } from '../types' +import * as constants from './constants' -export const isLegacyBinaryNode = (buffer: Buffer) => { - switch (buffer[0]) { - case TAGS.LIST_EMPTY: - case TAGS.LIST_8: - case TAGS.LIST_16: - return true - default: - return false - } +export const encodeBinaryNodeLegacy = (node: BinaryNode) => { + return encodeBinaryNode(node, constants, []) } -function decode(buffer: Buffer, indexRef: { index: number }): BinaryNode { - - const checkEOS = (length: number) => { - if(indexRef.index + length > buffer.length) { - throw new Error('end of stream') - } - } - - const next = () => { - const value = buffer[indexRef.index] - indexRef.index += 1 - return value - } - - const readByte = () => { - checkEOS(1) - return next() - } - - const readStringFromChars = (length: number) => { - checkEOS(length) - const value = buffer.slice(indexRef.index, indexRef.index + length) - - indexRef.index += length - return value.toString('utf-8') - } - - const readBytes = (n: number) => { - checkEOS(n) - const value = buffer.slice(indexRef.index, indexRef.index + n) - indexRef.index += n - return value - } - - const readInt = (n: number, littleEndian = false) => { - checkEOS(n) - let val = 0 - for(let i = 0; i < n; i++) { - const shift = littleEndian ? i : n - 1 - i - val |= next() << (shift * 8) - } - - return val - } - - const readInt20 = () => { - checkEOS(3) - return ((next() & 15) << 16) + (next() << 8) + next() - } - - const unpackHex = (value: number) => { - if(value >= 0 && value < 16) { - return value < 10 ? '0'.charCodeAt(0) + value : 'A'.charCodeAt(0) + value - 10 - } - - throw new Error('invalid hex: ' + value) - } - - const unpackNibble = (value: number) => { - if(value >= 0 && value <= 9) { - return '0'.charCodeAt(0) + value - } - - switch (value) { - case 10: - return '-'.charCodeAt(0) - case 11: - return '.'.charCodeAt(0) - case 15: - return '\0'.charCodeAt(0) - default: - throw new Error('invalid nibble: ' + value) - } - } - - const unpackByte = (tag: number, value: number) => { - if(tag === TAGS.NIBBLE_8) { - return unpackNibble(value) - } else if(tag === TAGS.HEX_8) { - return unpackHex(value) - } else { - throw new Error('unknown tag: ' + tag) - } - } - - const readPacked8 = (tag: number) => { - const startByte = readByte() - let value = '' - - for(let i = 0; i < (startByte & 127); i++) { - const curByte = readByte() - value += String.fromCharCode(unpackByte(tag, (curByte & 0xf0) >> 4)) - value += String.fromCharCode(unpackByte(tag, curByte & 0x0f)) - } - - if(startByte >> 7 !== 0) { - value = value.slice(0, -1) - } - - return value - } - - const isListTag = (tag: number) => { - return tag === TAGS.LIST_EMPTY || tag === TAGS.LIST_8 || tag === TAGS.LIST_16 - } - - const readListSize = (tag: number) => { - switch (tag) { - case TAGS.LIST_EMPTY: - return 0 - case TAGS.LIST_8: - return readByte() - case TAGS.LIST_16: - return readInt(2) - default: - throw new Error('invalid tag for list size: ' + tag) - } - } - - const getToken = (index: number) => { - if(index < 3 || index >= SINGLE_BYTE_TOKENS.length) { - throw new Error('invalid token index: ' + index) - } - - return SINGLE_BYTE_TOKENS[index] - } - - const readString = (tag: number) => { - if(tag >= 3 && tag <= 235) { - const token = getToken(tag) - return token// === 's.whatsapp.net' ? 'c.us' : token - } - - switch (tag) { - case TAGS.DICTIONARY_0: - case TAGS.DICTIONARY_1: - case TAGS.DICTIONARY_2: - case TAGS.DICTIONARY_3: - return getTokenDouble(tag - TAGS.DICTIONARY_0, readByte()) - case TAGS.LIST_EMPTY: - return null - case TAGS.BINARY_8: - return readStringFromChars(readByte()) - case TAGS.BINARY_20: - return readStringFromChars(readInt20()) - case TAGS.BINARY_32: - return readStringFromChars(readInt(4)) - case TAGS.JID_PAIR: - const i = readString(readByte()) - const j = readString(readByte()) - if(typeof i === 'string' && j) { - return i + '@' + j - } - - throw new Error('invalid jid pair: ' + i + ', ' + j) - case TAGS.HEX_8: - case TAGS.NIBBLE_8: - return readPacked8(tag) - default: - throw new Error('invalid string with tag: ' + tag) - } - } - - const readList = (tag: number) => ( - [...new Array(readListSize(tag))].map(() => decode(buffer, indexRef)) - ) - const getTokenDouble = (index1: number, index2: number) => { - const n = 256 * index1 + index2 - if(n < 0 || n > DOUBLE_BYTE_TOKENS.length) { - throw new Error('invalid double token index: ' + n) - } - - return DOUBLE_BYTE_TOKENS[n] - } - - const listSize = readListSize(readByte()) - const descrTag = readByte() - if(descrTag === TAGS.STREAM_END) { - throw new Error('unexpected stream end') - } - - const header = readString(descrTag) - const attrs: BinaryNode['attrs'] = { } - let data: BinaryNode['content'] - if(listSize === 0 || !header) { - throw new Error('invalid node') - } - // read the attributes in - - const attributesLength = (listSize - 1) >> 1 - for(let i = 0; i < attributesLength; i++) { - const key = readString(readByte()) - const b = readByte() - - attrs[key] = readString(b) - } - - if(listSize % 2 === 0) { - const tag = readByte() - if(isListTag(tag)) { - data = readList(tag) - } else { - let decoded: Buffer | string - switch (tag) { - case TAGS.BINARY_8: - decoded = readBytes(readByte()) - break - case TAGS.BINARY_20: - decoded = readBytes(readInt20()) - break - case TAGS.BINARY_32: - decoded = readBytes(readInt(4)) - break - default: - decoded = readString(tag) - break - } - - data = decoded - } - } - - return { - tag: header, - attrs, - content: data - } +export const decodeBinaryNodeLegacy = (data: Buffer, indexRef: { index: number }) => { + return decodeDecompressedBinaryNode(data, constants, indexRef) } - -const encode = ({ tag, attrs, content }: BinaryNode, buffer: number[] = []) => { - - const pushByte = (value: number) => buffer.push(value & 0xff) - - const pushInt = (value: number, n: number, littleEndian = false) => { - for(let i = 0; i < n; i++) { - const curShift = littleEndian ? i : n - 1 - i - buffer.push((value >> (curShift * 8)) & 0xff) - } - } - - const pushBytes = (bytes: Uint8Array | Buffer | number[]) => ( - bytes.forEach (b => buffer.push(b)) - ) - const pushInt20 = (value: number) => ( - pushBytes([(value >> 16) & 0x0f, (value >> 8) & 0xff, value & 0xff]) - ) - const writeByteLength = (length: number) => { - if(length >= 4294967296) { - throw new Error('string too large to encode: ' + length) - } - - if(length >= 1 << 20) { - pushByte(TAGS.BINARY_32) - pushInt(length, 4) // 32 bit integer - } else if(length >= 256) { - pushByte(TAGS.BINARY_20) - pushInt20(length) - } else { - pushByte(TAGS.BINARY_8) - pushByte(length) - } - } - - const writeStringRaw = (str: string) => { - const bytes = Buffer.from (str, 'utf-8') - writeByteLength(bytes.length) - pushBytes(bytes) - } - - const writeToken = (token: number) => { - if(token < 245) { - pushByte(token) - } else if(token <= 500) { - throw new Error('invalid token') - } - } - - const writeString = (token: string, i?: boolean) => { - if(token === 'c.us') { - token = 's.whatsapp.net' - } - - const tokenIndex = SINGLE_BYTE_TOKENS.indexOf(token) - if(!i && token === 's.whatsapp.net') { - writeToken(tokenIndex) - } else if(tokenIndex >= 0) { - if(tokenIndex < TAGS.SINGLE_BYTE_MAX) { - writeToken(tokenIndex) - } else { - const overflow = tokenIndex - TAGS.SINGLE_BYTE_MAX - const dictionaryIndex = overflow >> 8 - if(dictionaryIndex < 0 || dictionaryIndex > 3) { - throw new Error('double byte dict token out of range: ' + token + ', ' + tokenIndex) - } - - writeToken(TAGS.DICTIONARY_0 + dictionaryIndex) - writeToken(overflow % 256) - } - } else if(token) { - const jidSepIndex = token.indexOf('@') - if(jidSepIndex <= 0) { - writeStringRaw(token) - } else { - writeJid(token.slice(0, jidSepIndex), token.slice(jidSepIndex + 1, token.length)) - } - } - } - - const writeJid = (left: string, right: string) => { - pushByte(TAGS.JID_PAIR) - left && left.length > 0 ? writeString(left) : writeToken(TAGS.LIST_EMPTY) - writeString(right) - } - - const writeListStart = (listSize: number) => { - if(listSize === 0) { - pushByte(TAGS.LIST_EMPTY) - } else if(listSize < 256) { - pushBytes([TAGS.LIST_8, listSize]) - } else { - pushBytes([TAGS.LIST_16, listSize]) - } - } - - const validAttributes = Object.keys(attrs).filter(k => ( - typeof attrs[k] !== 'undefined' && attrs[k] !== null - )) - - writeListStart(2 * validAttributes.length + 1 + (typeof content !== 'undefined' && content !== null ? 1 : 0)) - writeString(tag) - - validAttributes.forEach((key) => { - if(typeof attrs[key] === 'string') { - writeString(key) - writeString(attrs[key]) - } - }) - - if(typeof content === 'string') { - writeString(content, true) - } else if(Buffer.isBuffer(content)) { - writeByteLength(content.length) - pushBytes(content) - } else if(Array.isArray(content)) { - writeListStart(content.length) - for(const item of content) { - if(item) { - encode(item, buffer) - } - } - } else if(typeof content === 'undefined' || content === null) { - - } else { - throw new Error(`invalid children for header "${tag}": ${content} (${typeof content})`) - } - - return Buffer.from(buffer) -} - -export const encodeBinaryNodeLegacy = encode -export const decodeBinaryNodeLegacy = decode diff --git a/src/WABinary/constants.ts b/src/WABinary/constants.ts index d5806f5..d6028ae 100644 --- a/src/WABinary/constants.ts +++ b/src/WABinary/constants.ts @@ -29,7 +29,8 @@ export const SINGLE_BYTE_TOKENS = [ '', 'xmlstreamstart', 'xmlstreamend', 's.whatsapp.net', 'type', 'participant', 'from', 'receipt', 'id', 'broadcast', 'status', 'message', 'notification', 'notify', 'to', 'jid', 'user', 'class', 'offline', 'g.us', 'result', 'mediatype', 'enc', 'skmsg', 'off_cnt', 'xmlns', 'presence', 'participants', 'ack', 't', 'iq', 'device_hash', 'read', 'value', 'media', 'picture', 'chatstate', 'unavailable', 'text', 'urn:xmpp:whatsapp:push', 'devices', 'verified_name', 'contact', 'composing', 'edge_routing', 'routing_info', 'item', 'image', 'verified_level', 'get', 'fallback_hostname', '2', 'media_conn', '1', 'v', 'handshake', 'fallback_class', 'count', 'config', 'offline_preview', 'download_buckets', 'w:profile:picture', 'set', 'creation', 'location', 'fallback_ip4', 'msg', 'urn:xmpp:ping', 'fallback_ip6', 'call-creator', 'relaylatency', 'success', 'subscribe', 'video', 'business_hours_config', 'platform', 'hostname', 'version', 'unknown', '0', 'ping', 'hash', 'edit', 'subject', 'max_buckets', 'download', 'delivery', 'props', 'sticker', 'name', 'last', 'contacts', 'business', 'primary', 'preview', 'w:p', 'pkmsg', 'call-id', 'retry', 'prop', 'call', 'auth_ttl', 'available', 'relay_id', 'last_id', 'day_of_week', 'w', 'host', 'seen', 'bits', 'list', 'atn', 'upload', 'is_new', 'w:stats', 'key', 'paused', 'specific_hours', 'multicast', 'stream:error', 'mmg.whatsapp.net', 'code', 'deny', 'played', 'profile', 'fna', 'device-list', 'close_time', 'latency', 'gcm', 'pop', 'audio', '26', 'w:web', 'open_time', 'error', 'auth', 'ip4', 'update', 'profile_options', 'config_value', 'category', 'catalog_not_created', '00', 'config_code', 'mode', 'catalog_status', 'ip6', 'blocklist', 'registration', '7', 'web', 'fail', 'w:m', 'cart_enabled', 'ttl', 'gif', '300', 'device_orientation', 'identity', 'query', '401', 'media-gig2-1.cdn.whatsapp.net', 'in', '3', 'te2', 'add', 'fallback', 'categories', 'ptt', 'encrypt', 'notice', 'thumbnail-document', 'item-not-found', '12', 'thumbnail-image', 'stage', 'thumbnail-link', 'usync', 'out', 'thumbnail-video', '8', '01', 'context', 'sidelist', 'thumbnail-gif', 'terminate', 'not-authorized', 'orientation', 'dhash', 'capability', 'side_list', 'md-app-state', 'description', 'serial', 'readreceipts', 'te', 'business_hours', 'md-msg-hist', 'tag', 'attribute_padding', 'document', 'open_24h', 'delete', 'expiration', 'active', 'prev_v_id', 'true', 'passive', 'index', '4', 'conflict', 'remove', 'w:gp2', 'config_expo_key', 'screen_height', 'replaced', '02', 'screen_width', 'uploadfieldstat', '2:47DEQpj8', 'media-bog1-1.cdn.whatsapp.net', 'encopt', 'url', 'catalog_exists', 'keygen', 'rate', 'offer', 'opus', 'media-mia3-1.cdn.whatsapp.net', 'privacy', 'media-mia3-2.cdn.whatsapp.net', 'signature', 'preaccept', 'token_id', 'media-eze1-1.cdn.whatsapp.net' ] -const TOKEN_MAP: { [token: string]: { dict?: number, index: number } } = { } +export const TOKEN_MAP: { [token: string]: { dict?: number, index: number } } = { } + for(let i = 0;i < SINGLE_BYTE_TOKENS.length;i++) { TOKEN_MAP[SINGLE_BYTE_TOKENS[i]] = { index: i } } @@ -38,6 +39,4 @@ for(let i = 0;i < DOUBLE_BYTE_TOKENS.length;i++) { for(let j = 0;j < DOUBLE_BYTE_TOKENS[i].length;j++) { TOKEN_MAP[DOUBLE_BYTE_TOKENS[i][j]] = { dict: i, index: j } } -} - -export { TOKEN_MAP } \ No newline at end of file +} \ No newline at end of file diff --git a/src/WABinary/decode.ts b/src/WABinary/decode.ts index 888641e..a119af4 100644 --- a/src/WABinary/decode.ts +++ b/src/WABinary/decode.ts @@ -1,7 +1,7 @@ import { inflateSync } from 'zlib' -import { DOUBLE_BYTE_TOKENS, SINGLE_BYTE_TOKENS, TAGS } from './constants' +import * as constants from './constants' import { jidEncode } from './jid-utils' -import type { BinaryNode } from './types' +import type { BinaryNode, BinaryNodeCodingOptions } from './types' export const decompressingIfRequired = (buffer: Buffer) => { if(2 & buffer.readUInt8()) { @@ -13,7 +13,12 @@ export const decompressingIfRequired = (buffer: Buffer) => { return buffer } -export const decodeDecompressedBinaryNode = (buffer: Buffer, indexRef: { index: number } = { index: 0 }): BinaryNode => { +export const decodeDecompressedBinaryNode = ( + buffer: Buffer, + opts: Pick, + indexRef: { index: number } = { index: 0 } +): BinaryNode => { + const { DOUBLE_BYTE_TOKENS, SINGLE_BYTE_TOKENS, TAGS } = opts const checkEOS = (length: number) => { if(indexRef.index + length > buffer.length) { @@ -181,7 +186,7 @@ export const decodeDecompressedBinaryNode = (buffer: Buffer, indexRef: { index: const items: BinaryNode[] = [] const size = readListSize(tag) for(let i = 0;i < size;i++) { - items.push(decodeDecompressedBinaryNode(buffer, indexRef)) + items.push(decodeDecompressedBinaryNode(buffer, opts, indexRef)) } return items @@ -256,5 +261,5 @@ export const decodeDecompressedBinaryNode = (buffer: Buffer, indexRef: { index: export const decodeBinaryNode = (buff: Buffer): BinaryNode => { const decompBuff = decompressingIfRequired(buff) - return decodeDecompressedBinaryNode(decompBuff) + return decodeDecompressedBinaryNode(decompBuff, constants) } \ No newline at end of file diff --git a/src/WABinary/encode.ts b/src/WABinary/encode.ts index 9dfb912..d578027 100644 --- a/src/WABinary/encode.ts +++ b/src/WABinary/encode.ts @@ -1,9 +1,14 @@ -import { TAGS, TOKEN_MAP } from './constants' +import * as constants from './constants' import { jidDecode } from './jid-utils' -import type { BinaryNode } from './types' +import type { BinaryNode, BinaryNodeCodingOptions } from './types' -export const encodeBinaryNode = ({ tag, attrs, content }: BinaryNode, buffer: number[] = [0]) => { +export const encodeBinaryNode = ( + { tag, attrs, content }: BinaryNode, + opts: Pick = constants, + buffer: number[] = [] +) => { + const { TAGS, TOKEN_MAP } = opts const pushByte = (value: number) => buffer.push(value & 0xff) @@ -216,7 +221,7 @@ export const encodeBinaryNode = ({ tag, attrs, content }: BinaryNode, buffer: nu writeListStart(content.length) for(const item of content) { if(item) { - encodeBinaryNode(item, buffer) + encodeBinaryNode(item, opts, buffer) } } } else if(typeof content === 'undefined' || content === null) { diff --git a/src/WABinary/types.ts b/src/WABinary/types.ts index a617bdf..dcc0ea2 100644 --- a/src/WABinary/types.ts +++ b/src/WABinary/types.ts @@ -1,3 +1,4 @@ +import * as constants from './constants' /** * the binary node WA uses internally for communication * @@ -11,4 +12,6 @@ export type BinaryNode = { content?: BinaryNode[] | string | Uint8Array } export type BinaryNodeAttributes = BinaryNode['attrs'] -export type BinaryNodeData = BinaryNode['content'] \ No newline at end of file +export type BinaryNodeData = BinaryNode['content'] + +export type BinaryNodeCodingOptions = typeof constants \ No newline at end of file