Path: blob/master/node_modules/@adiwajshing/baileys/lib/Utils/chat-utils.js
2593 views
"use strict";1Object.defineProperty(exports, "__esModule", { value: true });2exports.chatModificationToAppPatch = exports.decodePatches = exports.decodeSyncdSnapshot = exports.downloadExternalPatch = exports.downloadExternalBlob = exports.extractSyncdPatches = exports.decodeSyncdPatch = exports.decodeSyncdMutations = exports.encodeSyncdPatch = exports.newLTHashState = void 0;3const boom_1 = require("@hapi/boom");4const WAProto_1 = require("../../WAProto");5const WABinary_1 = require("../WABinary");6const crypto_1 = require("./crypto");7const generics_1 = require("./generics");8const lt_hash_1 = require("./lt-hash");9const messages_media_1 = require("./messages-media");10const mutationKeys = (keydata) => {11const expanded = crypto_1.hkdf(keydata, 160, { info: 'WhatsApp Mutation Keys' });12return {13indexKey: expanded.slice(0, 32),14valueEncryptionKey: expanded.slice(32, 64),15valueMacKey: expanded.slice(64, 96),16snapshotMacKey: expanded.slice(96, 128),17patchMacKey: expanded.slice(128, 160)18};19};20const generateMac = (operation, data, keyId, key) => {21const getKeyData = () => {22let r;23switch (operation) {24case WAProto_1.proto.SyncdMutation.SyncdMutationSyncdOperation.SET:25r = 0x01;26break;27case WAProto_1.proto.SyncdMutation.SyncdMutationSyncdOperation.REMOVE:28r = 0x02;29break;30}31const buff = Buffer.from([r]);32return Buffer.concat([buff, Buffer.from(keyId, 'base64')]);33};34const keyData = getKeyData();35const last = Buffer.alloc(8); // 8 bytes36last.set([keyData.length], last.length - 1);37const total = Buffer.concat([keyData, data, last]);38const hmac = crypto_1.hmacSign(total, key, 'sha512');39return hmac.slice(0, 32);40};41const to64BitNetworkOrder = (e) => {42const t = new ArrayBuffer(8);43new DataView(t).setUint32(4, e, !1);44return Buffer.from(t);45};46const makeLtHashGenerator = ({ indexValueMap, hash }) => {47indexValueMap = { ...indexValueMap };48const addBuffs = [];49const subBuffs = [];50return {51mix: ({ indexMac, valueMac, operation }) => {52const indexMacBase64 = Buffer.from(indexMac).toString('base64');53const prevOp = indexValueMap[indexMacBase64];54if (operation === WAProto_1.proto.SyncdMutation.SyncdMutationSyncdOperation.REMOVE) {55if (!prevOp) {56throw new boom_1.Boom('tried remove, but no previous op', { data: { indexMac, valueMac } });57}58// remove from index value mac, since this mutation is erased59delete indexValueMap[indexMacBase64];60}61else {62addBuffs.push(new Uint8Array(valueMac).buffer);63// add this index into the history map64indexValueMap[indexMacBase64] = { valueMac };65}66if (prevOp) {67subBuffs.push(new Uint8Array(prevOp.valueMac).buffer);68}69},70finish: () => {71const result = lt_hash_1.LT_HASH_ANTI_TAMPERING.subtractThenAdd(new Uint8Array(hash).buffer, addBuffs, subBuffs);72const buffer = Buffer.from(result);73return {74hash: buffer,75indexValueMap76};77}78};79};80const generateSnapshotMac = (lthash, version, name, key) => {81const total = Buffer.concat([82lthash,83to64BitNetworkOrder(version),84Buffer.from(name, 'utf-8')85]);86return crypto_1.hmacSign(total, key, 'sha256');87};88const generatePatchMac = (snapshotMac, valueMacs, version, type, key) => {89const total = Buffer.concat([90snapshotMac,91...valueMacs,92to64BitNetworkOrder(version),93Buffer.from(type, 'utf-8')94]);95return crypto_1.hmacSign(total, key);96};97const newLTHashState = () => ({ version: 0, hash: Buffer.alloc(128), indexValueMap: {} });98exports.newLTHashState = newLTHashState;99const encodeSyncdPatch = async ({ type, index, syncAction, apiVersion, operation }, myAppStateKeyId, state, getAppStateSyncKey) => {100const key = !!myAppStateKeyId ? await getAppStateSyncKey(myAppStateKeyId) : undefined;101if (!key) {102throw new boom_1.Boom(`myAppStateKey ("${myAppStateKeyId}") not present`, { statusCode: 404 });103}104const encKeyId = Buffer.from(myAppStateKeyId, 'base64');105state = { ...state, indexValueMap: { ...state.indexValueMap } };106const indexBuffer = Buffer.from(JSON.stringify(index));107const dataProto = WAProto_1.proto.SyncActionData.fromObject({108index: indexBuffer,109value: syncAction,110padding: new Uint8Array(0),111version: apiVersion112});113const encoded = WAProto_1.proto.SyncActionData.encode(dataProto).finish();114const keyValue = mutationKeys(key.keyData);115const encValue = crypto_1.aesEncrypt(encoded, keyValue.valueEncryptionKey);116const valueMac = generateMac(operation, encValue, encKeyId, keyValue.valueMacKey);117const indexMac = crypto_1.hmacSign(indexBuffer, keyValue.indexKey);118// update LT hash119const generator = makeLtHashGenerator(state);120generator.mix({ indexMac, valueMac, operation });121Object.assign(state, generator.finish());122state.version += 1;123const snapshotMac = generateSnapshotMac(state.hash, state.version, type, keyValue.snapshotMacKey);124const patch = {125patchMac: generatePatchMac(snapshotMac, [valueMac], state.version, type, keyValue.patchMacKey),126snapshotMac: snapshotMac,127keyId: { id: encKeyId },128mutations: [129{130operation: operation,131record: {132index: {133blob: indexMac134},135value: {136blob: Buffer.concat([encValue, valueMac])137},138keyId: { id: encKeyId }139}140}141]142};143const base64Index = indexMac.toString('base64');144state.indexValueMap[base64Index] = { valueMac };145return { patch, state };146};147exports.encodeSyncdPatch = encodeSyncdPatch;148const decodeSyncdMutations = async (msgMutations, initialState, getAppStateSyncKey, validateMacs) => {149const keyCache = {};150const getKey = async (keyId) => {151const base64Key = Buffer.from(keyId).toString('base64');152let key = keyCache[base64Key];153if (!key) {154const keyEnc = await getAppStateSyncKey(base64Key);155if (!keyEnc) {156throw new boom_1.Boom(`failed to find key "${base64Key}" to decode mutation`, { statusCode: 404, data: { msgMutations } });157}158const result = mutationKeys(keyEnc.keyData);159keyCache[base64Key] = result;160key = result;161}162return key;163};164const ltGenerator = makeLtHashGenerator(initialState);165const mutations = [];166// indexKey used to HMAC sign record.index.blob167// valueEncryptionKey used to AES-256-CBC encrypt record.value.blob[0:-32]168// the remaining record.value.blob[0:-32] is the mac, it the HMAC sign of key.keyId + decoded proto data + length of bytes in keyId169for (const msgMutation of msgMutations) {170// if it's a syncdmutation, get the operation property171// otherwise, if it's only a record -- it'll be a SET mutation172const operation = 'operation' in msgMutation ? msgMutation.operation : WAProto_1.proto.SyncdMutation.SyncdMutationSyncdOperation.SET;173const record = ('record' in msgMutation && !!msgMutation.record) ? msgMutation.record : msgMutation;174const key = await getKey(record.keyId.id);175const content = Buffer.from(record.value.blob);176const encContent = content.slice(0, -32);177const ogValueMac = content.slice(-32);178if (validateMacs) {179const contentHmac = generateMac(operation, encContent, record.keyId.id, key.valueMacKey);180if (Buffer.compare(contentHmac, ogValueMac) !== 0) {181throw new boom_1.Boom('HMAC content verification failed');182}183}184const result = crypto_1.aesDecrypt(encContent, key.valueEncryptionKey);185const syncAction = WAProto_1.proto.SyncActionData.decode(result);186if (validateMacs) {187const hmac = crypto_1.hmacSign(syncAction.index, key.indexKey);188if (Buffer.compare(hmac, record.index.blob) !== 0) {189throw new boom_1.Boom('HMAC index verification failed');190}191}192const indexStr = Buffer.from(syncAction.index).toString();193mutations.push({194syncAction,195index: JSON.parse(indexStr),196});197ltGenerator.mix({198indexMac: record.index.blob,199valueMac: ogValueMac,200operation: operation201});202}203return { mutations, ...ltGenerator.finish() };204};205exports.decodeSyncdMutations = decodeSyncdMutations;206const decodeSyncdPatch = async (msg, name, initialState, getAppStateSyncKey, validateMacs) => {207if (validateMacs) {208const base64Key = Buffer.from(msg.keyId.id).toString('base64');209const mainKeyObj = await getAppStateSyncKey(base64Key);210const mainKey = mutationKeys(mainKeyObj.keyData);211const mutationmacs = msg.mutations.map(mutation => mutation.record.value.blob.slice(-32));212const patchMac = generatePatchMac(msg.snapshotMac, mutationmacs, generics_1.toNumber(msg.version.version), name, mainKey.patchMacKey);213if (Buffer.compare(patchMac, msg.patchMac) !== 0) {214throw new boom_1.Boom('Invalid patch mac');215}216}217const result = await exports.decodeSyncdMutations(msg.mutations, initialState, getAppStateSyncKey, validateMacs);218return result;219};220exports.decodeSyncdPatch = decodeSyncdPatch;221const extractSyncdPatches = async (result) => {222const syncNode = WABinary_1.getBinaryNodeChild(result, 'sync');223const collectionNodes = WABinary_1.getBinaryNodeChildren(syncNode, 'collection');224const final = {};225await Promise.all(collectionNodes.map(async (collectionNode) => {226const patchesNode = WABinary_1.getBinaryNodeChild(collectionNode, 'patches');227const patches = WABinary_1.getBinaryNodeChildren(patchesNode || collectionNode, 'patch');228const snapshotNode = WABinary_1.getBinaryNodeChild(collectionNode, 'snapshot');229const syncds = [];230const name = collectionNode.attrs.name;231const hasMorePatches = collectionNode.attrs.has_more_patches === 'true';232let snapshot = undefined;233if (snapshotNode && !!snapshotNode.content) {234if (!Buffer.isBuffer(snapshotNode)) {235snapshotNode.content = Buffer.from(Object.values(snapshotNode.content));236}237const blobRef = WAProto_1.proto.ExternalBlobReference.decode(snapshotNode.content);238const data = await exports.downloadExternalBlob(blobRef);239snapshot = WAProto_1.proto.SyncdSnapshot.decode(data);240}241for (let { content } of patches) {242if (content) {243if (!Buffer.isBuffer(content)) {244content = Buffer.from(Object.values(content));245}246const syncd = WAProto_1.proto.SyncdPatch.decode(content);247if (!syncd.version) {248syncd.version = { version: +collectionNode.attrs.version + 1 };249}250syncds.push(syncd);251}252}253final[name] = { patches: syncds, hasMorePatches, snapshot };254}));255return final;256};257exports.extractSyncdPatches = extractSyncdPatches;258const downloadExternalBlob = async (blob) => {259const stream = await messages_media_1.downloadContentFromMessage(blob, 'md-app-state');260let buffer = Buffer.from([]);261for await (const chunk of stream) {262buffer = Buffer.concat([buffer, chunk]);263}264return buffer;265};266exports.downloadExternalBlob = downloadExternalBlob;267const downloadExternalPatch = async (blob) => {268const buffer = await exports.downloadExternalBlob(blob);269const syncData = WAProto_1.proto.SyncdMutations.decode(buffer);270return syncData;271};272exports.downloadExternalPatch = downloadExternalPatch;273const decodeSyncdSnapshot = async (name, snapshot, getAppStateSyncKey, minimumVersionNumber, validateMacs = true) => {274const newState = exports.newLTHashState();275newState.version = generics_1.toNumber(snapshot.version.version);276const { hash, indexValueMap, mutations } = await exports.decodeSyncdMutations(snapshot.records, newState, getAppStateSyncKey, validateMacs);277newState.hash = hash;278newState.indexValueMap = indexValueMap;279if (validateMacs) {280const base64Key = Buffer.from(snapshot.keyId.id).toString('base64');281const keyEnc = await getAppStateSyncKey(base64Key);282if (!keyEnc) {283throw new boom_1.Boom(`failed to find key "${base64Key}" to decode mutation`, { statusCode: 500 });284}285const result = mutationKeys(keyEnc.keyData);286const computedSnapshotMac = generateSnapshotMac(newState.hash, newState.version, name, result.snapshotMacKey);287if (Buffer.compare(snapshot.mac, computedSnapshotMac) !== 0) {288throw new boom_1.Boom(`failed to verify LTHash at ${newState.version} of ${name} from snapshot`, { statusCode: 500 });289}290}291const areMutationsRequired = typeof minimumVersionNumber === 'undefined' || newState.version > minimumVersionNumber;292if (!areMutationsRequired) {293// clear array294mutations.splice(0, mutations.length);295}296return {297state: newState,298mutations299};300};301exports.decodeSyncdSnapshot = decodeSyncdSnapshot;302const decodePatches = async (name, syncds, initial, getAppStateSyncKey, minimumVersionNumber, validateMacs = true) => {303const successfulMutations = [];304const newState = {305...initial,306indexValueMap: { ...initial.indexValueMap }307};308for (const syncd of syncds) {309const { version, keyId, snapshotMac } = syncd;310if (syncd.externalMutations) {311const ref = await exports.downloadExternalPatch(syncd.externalMutations);312syncd.mutations.push(...ref.mutations);313}314const patchVersion = generics_1.toNumber(version.version);315newState.version = patchVersion;316const decodeResult = await exports.decodeSyncdPatch(syncd, name, newState, getAppStateSyncKey, validateMacs);317newState.hash = decodeResult.hash;318newState.indexValueMap = decodeResult.indexValueMap;319if (typeof minimumVersionNumber === 'undefined' || patchVersion > minimumVersionNumber) {320successfulMutations.push(...decodeResult.mutations);321}322if (validateMacs) {323const base64Key = Buffer.from(keyId.id).toString('base64');324const keyEnc = await getAppStateSyncKey(base64Key);325if (!keyEnc) {326throw new boom_1.Boom(`failed to find key "${base64Key}" to decode mutation`);327}328const result = mutationKeys(keyEnc.keyData);329const computedSnapshotMac = generateSnapshotMac(newState.hash, newState.version, name, result.snapshotMacKey);330if (Buffer.compare(snapshotMac, computedSnapshotMac) !== 0) {331throw new boom_1.Boom(`failed to verify LTHash at ${newState.version} of ${name}`);332}333}334}335return {336newMutations: successfulMutations,337state: newState338};339};340exports.decodePatches = decodePatches;341const chatModificationToAppPatch = (mod, jid) => {342const OP = WAProto_1.proto.SyncdMutation.SyncdMutationSyncdOperation;343const getMessageRange = (lastMessages) => {344if (!(lastMessages === null || lastMessages === void 0 ? void 0 : lastMessages.length)) {345throw new boom_1.Boom('Expected last message to be not from me', { statusCode: 400 });346}347const lastMsg = lastMessages[lastMessages.length - 1];348if (lastMsg.key.fromMe) {349throw new boom_1.Boom('Expected last message in array to be not from me', { statusCode: 400 });350}351const messageRange = {352lastMessageTimestamp: lastMsg === null || lastMsg === void 0 ? void 0 : lastMsg.messageTimestamp,353messages: lastMessages354};355return messageRange;356};357let patch;358if ('mute' in mod) {359patch = {360syncAction: {361muteAction: {362muted: !!mod.mute,363muteEndTimestamp: mod.mute || undefined364}365},366index: ['mute', jid],367type: 'regular_high',368apiVersion: 2,369operation: OP.SET370};371}372else if ('archive' in mod) {373patch = {374syncAction: {375archiveChatAction: {376archived: !!mod.archive,377messageRange: getMessageRange(mod.lastMessages)378}379},380index: ['archive', jid],381type: 'regular_low',382apiVersion: 3,383operation: OP.SET384};385}386else if ('markRead' in mod) {387patch = {388syncAction: {389markChatAsReadAction: {390read: mod.markRead,391messageRange: getMessageRange(mod.lastMessages)392}393},394index: ['markChatAsRead', jid],395type: 'regular_low',396apiVersion: 3,397operation: OP.SET398};399}400else if ('clear' in mod) {401if (mod.clear === 'all') {402throw new boom_1.Boom('not supported');403}404else {405const key = mod.clear.messages[0];406patch = {407syncAction: {408deleteMessageForMeAction: {409deleteMedia: false410}411},412index: ['deleteMessageForMe', jid, key.id, key.fromMe ? '1' : '0', '0'],413type: 'regular_high',414apiVersion: 3,415operation: OP.SET416};417}418}419else if ('pin' in mod) {420patch = {421syncAction: {422pinAction: {423pinned: !!mod.pin424}425},426index: ['pin_v1', jid],427type: 'regular_low',428apiVersion: 5,429operation: OP.SET430};431}432else {433throw new boom_1.Boom('not supported');434}435patch.syncAction.timestamp = Date.now();436return patch;437};438exports.chatModificationToAppPatch = chatModificationToAppPatch;439440441