Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: added a decrypt and save function #2314

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 5 additions & 23 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

62 changes: 59 additions & 3 deletions src/api/helpers/decrypt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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');
105 changes: 102 additions & 3 deletions src/api/whatsapp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
Expand Down Expand Up @@ -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<void> {
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<void>((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
Expand Down