Skip to content

Commit

Permalink
feat: Added client.decryptAndSaveFile function
Browse files Browse the repository at this point in the history
  • Loading branch information
pfeux authored Jan 31, 2025
1 parent 11bd50e commit f42ffd7
Show file tree
Hide file tree
Showing 3 changed files with 165 additions and 27 deletions.
25 changes: 4 additions & 21 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

0 comments on commit f42ffd7

Please sign in to comment.