diff --git a/package-lock.json b/package-lock.json index 0a2d3c949..532cb86e4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3089,7 +3089,6 @@ "version": "7.6.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -3393,7 +3392,6 @@ "version": "2.10.3", "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", - "license": "MIT", "optional": true, "dependencies": { "@types/node": "*" @@ -4333,7 +4331,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-2.1.3.tgz", "integrity": "sha512-lh/eITfU8hrj9Ru5quUp0Io1kJWIk1bTjzo7JH1P5dWmQ2EL4hFUlfI8FonAhSlgIfhn63p84CDY/x+PisgcXA==", - "license": "Apache-2.0", "optional": true, "dependencies": { "bare-os": "^2.1.0" @@ -4524,7 +4521,6 @@ "version": "0.2.13", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "license": "MIT", "engines": { "node": "*" } @@ -6664,7 +6660,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", - "license": "BSD-2-Clause", "dependencies": { "debug": "^4.1.1", "get-stream": "^5.1.0", @@ -6684,7 +6679,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "license": "MIT", "dependencies": { "pump": "^3.0.0" }, @@ -6712,8 +6706,7 @@ "node_modules/fast-fifo": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", - "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", - "license": "MIT" + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==" }, "node_modules/fast-glob": { "version": "3.3.2", @@ -6790,7 +6783,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", - "license": "MIT", "dependencies": { "pend": "~1.2.0" } @@ -9063,8 +9055,7 @@ "node_modules/mitt": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", - "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", - "license": "MIT" + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==" }, "node_modules/mixin-object": { "version": "2.0.1", @@ -9840,8 +9831,7 @@ "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "license": "MIT" + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==" }, "node_modules/picocolors": { "version": "1.1.1", @@ -10082,7 +10072,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "license": "MIT", "engines": { "node": ">=0.4.0" } @@ -10414,8 +10403,7 @@ "node_modules/queue-tick": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", - "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==", - "license": "MIT" + "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==" }, "node_modules/randombytes": { "version": "2.1.0", @@ -12051,7 +12039,6 @@ "version": "3.0.6", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.6.tgz", "integrity": "sha512-iokBDQQkUyeXhgPYaZxmczGPhnhXZ0CmrqI+MOb/WFGS9DW5wnfrLgtjUJBvz50vQ3qfRwJ62QVoCFu8mPVu5w==", - "license": "MIT", "dependencies": { "pump": "^3.0.0", "tar-stream": "^3.1.5" @@ -12065,7 +12052,6 @@ "version": "3.1.7", "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", - "license": "MIT", "dependencies": { "b4a": "^1.6.4", "fast-fifo": "^1.2.0", @@ -12557,7 +12543,6 @@ "version": "1.4.3", "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", - "license": "MIT", "dependencies": { "buffer": "^5.2.1", "through": "^2.3.8" @@ -13704,7 +13689,6 @@ "version": "2.10.0", "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", - "license": "MIT", "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" @@ -13750,7 +13734,6 @@ "version": "3.23.8", "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", - "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/src/api/helpers/decrypt.ts b/src/api/helpers/decrypt.ts index e80f210cd..adb464f4d 100644 --- a/src/api/helpers/decrypt.ts +++ b/src/api/helpers/decrypt.ts @@ -17,11 +17,15 @@ import * as crypto from 'crypto'; import hkdf from 'futoin-hkdf'; -import atob = require('atob'); +import atob from 'atob'; import { ResponseType } from 'axios'; +import { Transform } from 'stream'; -export const makeOptions = (useragentOverride: string) => ({ - responseType: 'arraybuffer' as ResponseType, +export const makeOptions = ( + useragentOverride: string, + responseType: ResponseType = 'arraybuffer' +) => ({ + responseType: responseType, headers: { 'User-Agent': processUA(useragentOverride), DNT: '1', @@ -110,3 +114,55 @@ const base64ToBytes = (base64Str: any) => { } return byteArray; }; + +export const newMagix = ( + mediaKeyBase64: string, + mediaType: string, + expectedSize: number +) => { + const mediaKeyBytes = newBase64ToBytes(mediaKeyBase64); + const info = `WhatsApp ${mediaTypes[mediaType.toUpperCase()]} Keys`; + const hash = 'sha256'; + const salt = Buffer.alloc(32); + const expandedSize = 112; + const mediaKeyExpanded = hkdf(mediaKeyBytes, expandedSize, { + salt, + info, + hash, + }); + const iv = mediaKeyExpanded.slice(0, 16); + const cipherKey = mediaKeyExpanded.slice(16, 48); + + const decipher = crypto.createDecipheriv('aes-256-cbc', cipherKey, iv); + let processedBytes: number = 0; + let buffer = Buffer.alloc(0); + + const transformStream = new Transform({ + transform(chunk, encoding, callback) { + try { + const decryptedChunk = decipher.update(chunk); + processedBytes += decryptedChunk.length; + if (processedBytes > expectedSize) { + const paddedChunk = Buffer.from(decryptedChunk).slice( + 0, + buffer.length - (processedBytes - expectedSize) + ); + callback(null, paddedChunk); + } else { + callback(null, decryptedChunk); + } + } catch (error: any) { + callback(error); + } + }, + }); + + transformStream.on('error', (error) => { + console.error('Error during decryption:', error); + }); + + return transformStream; +}; + +const newBase64ToBytes = (base64Str: string) => + Buffer.from(base64Str, 'base64'); diff --git a/src/api/whatsapp.ts b/src/api/whatsapp.ts index 962ff1d5c..9a57ed178 100644 --- a/src/api/whatsapp.ts +++ b/src/api/whatsapp.ts @@ -20,10 +20,11 @@ import { Page } from 'puppeteer'; import { CreateConfig } from '../config/create-config'; import { useragentOverride } from '../config/WAuserAgente'; import { evaluateAndReturn } from './helpers'; -import { magix, makeOptions, timeout } from './helpers/decrypt'; +import { magix, makeOptions, newMagix, timeout } from './helpers/decrypt'; import { BusinessLayer } from './layers/business.layer'; import { GetMessagesParam, Message } from './model'; -import treekill = require('tree-kill'); +import * as fs from 'fs'; +import { sleep } from '../utils/sleep'; export class Whatsapp extends BusinessLayer { private connected: boolean | null = null; @@ -39,7 +40,7 @@ export class Whatsapp extends BusinessLayer { }); } - interval = setInterval(async (state) => { + interval = setInterval(async () => { const newConnected = await page .evaluate(() => WPP.conn.isRegistered()) .catch(() => null); @@ -232,6 +233,104 @@ export class Whatsapp extends BusinessLayer { return magix(buff, message.mediaKey, message.type, message.size); } + public async decryptAndSaveFile( + message: Message, + savePath: string + ): Promise { + const mediaUrl = message.clientUrl || message.deprecatedMms3Url; + + if (!mediaUrl) { + throw new Error( + 'Message is missing critical data needed to download the file.' + ); + } + + try { + const tempSavePath: string = savePath + '.encrypted'; + await this.downloadEncryptedFile(mediaUrl.trim(), tempSavePath); + + const inputReadStream = fs.createReadStream(tempSavePath); + const outputWriteStream = fs.createWriteStream(savePath); + const decryptedStream = newMagix( + message.mediaKey, + message.type, + message.size + ); + + inputReadStream.pipe(decryptedStream).pipe(outputWriteStream); + + await new Promise((resolve, reject) => { + outputWriteStream.on('finish', () => { + console.log( + `Deciphering complete. Deleting the encrypted file: ${tempSavePath}` + ); + fs.unlink(tempSavePath, (error) => { + if (error) { + console.error( + `Error deleting the input file: ${tempSavePath}`, + error + ); + reject(error); + } else { + console.log('Encrypted file deleted successfully'); + resolve(); + } + }); + }); + + outputWriteStream.on('error', (error) => { + console.error(`Error during writing file: ${savePath}`, error); + reject(error); + }); + + decryptedStream.on('error', (error) => { + console.error('An error occurred while decrypting the file', error); + reject(error); + }); + }); + } catch (error) { + throw error; + } + } + + downloadEncryptedFile = async ( + url: string, + outputPath: string, + retries: number = 3 + ) => { + for (let attempt = 1; attempt <= retries; attempt++) { + try { + const response = await axios.get( + url, + makeOptions(useragentOverride, 'stream') + ); + + await new Promise((resolve, reject) => { + const writer = fs.createWriteStream(outputPath); + response.data.pipe(writer); + writer.on('finish', resolve); + writer.on('error', reject); + }); + + console.log(`Encrypted file downloaded at ${outputPath}`); + return; + } catch (error) { + console.error(`Attempt ${attempt} failed: `, error.message); + if (attempt === retries) { + console.error( + `${outputPath} - All attempt failed to download the file: ${url}` + ); + throw error; + } + + console.log( + `${outputPath} - Retrying to download the file: ${url} in 5 seconds...` + ); + await sleep(5000); + } + } + }; + /** * Rejects a call received via WhatsApp * @param callId string Call ID, if not passed, all calls will be rejected