Node+TypeScript开发WhatsApp Bot:音频解密后OGG文件损坏问题
WhatsApp音频解密后文件损坏问题
我使用Node.js和TypeScript开发WhatsApp聊天机器人,整体功能运行正常,但接收的音频为加密格式。消息中包含url、mimetype、mediaKey等可用于解密的信息,我已实现主处理、加密文件下载、解密三个方法。执行后虽能生成对应的文件,但得到的OGG音频文件已损坏无法播放。
现有代码实现
音频处理代码
import fs from 'fs/promises'; import fetch from 'node-fetch'; import crypto from 'crypto'; import MessageBotAudio from '../interfaces/messageBotAudio'; export async function downloadFile(url: string, filePath: string): Promise<string> { const response = await fetch(url); if (!response.ok) { throw new Error(`Failed to fetch ${url}: ${response.statusText}`); } const buffer = await response.buffer(); await fs.writeFile(filePath, buffer); return filePath; } const decryptAudio = async (encryptedFilePath: string, decryptedFilePath: string, audioMessage: MessageBotAudio) => { try { const key = Buffer.from(audioMessage.audioMessage.mediaKey); const iv = Buffer.alloc(16, 0); const decipher = crypto.createDecipheriv('aes-256-ctr', key, iv); const encryptedData = await fs.readFile(encryptedFilePath); const decryptedData = Buffer.concat([decipher.update(encryptedData), decipher.final()]); await fs.writeFile(decryptedFilePath, decryptedData); return decryptedFilePath; } catch (error: any) { console.error('Error al desencriptar el archivo:', error.message); throw error; } }; export const handleAudioMessage = async (audioMessage: MessageBotAudio): Promise<string> => { const encryptedFilePath = './audio.enc'; const decryptedFilePath = './audio.ogg'; await downloadFile(audioMessage.audioMessage.url, encryptedFilePath); return await decryptAudio(encryptedFilePath, decryptedFilePath, audioMessage); };
消息结构定义
export default interface MessageBotAudio { audioMessage: AudioMessage; messageContextInfo: MessageContextInfo; } interface AudioMessage { url: string; mimetype: string; fileEncSha256: Uint8Array; fileLength: Long; seconds: number; ptt: boolean; mediaKey: Uint8Array; fileSha256: Uint8Array; directPath: string; mediaKeyTimestamp: Long; streamingSidecar: Uint8Array; waveform: Uint8Array; } interface MessageContextInfo { deviceListMetadata: DeviceListMetadata; deviceListMetadataVersion: number; } interface DeviceListMetadata { senderKeyIndexes: number[]; recipientKeyIndexes: number[]; senderKeyHash: Uint8Array[]; recipientKeyHash: Uint8Array[]; recipientTimestamp: Long[]; }
问题原因及修复方案
核心问题
- MediaKey未正确衍生:WhatsApp的
mediaKey不能直接作为AES密钥使用,需要通过HKDF算法衍生出实际的加密密钥。 - IV生成错误:不能使用全0的IV,需要结合
mediaKeyTimestamp生成符合要求的初始化向量。
修正后的解密代码
const decryptAudio = async (encryptedFilePath: string, decryptedFilePath: string, audioMessage: MessageBotAudio) => { try { const mediaKey = Buffer.from(audioMessage.audioMessage.mediaKey); // 1. 通过HKDF衍生AES密钥 const hkdfSalt = Buffer.from('WhatsApp Audio', 'utf8'); const hkdfInfo = Buffer.from('WhatsApp Keys', 'utf8'); const derivedKey = crypto.hkdfSync('sha256', mediaKey, hkdfSalt, hkdfInfo, 32); // 2. 生成正确的IV:mediaKeyTimestamp转8字节大端,前补8个0凑16字节 const timestampBuffer = Buffer.alloc(8); timestampBuffer.writeBigInt64BE(BigInt(audioMessage.audioMessage.mediaKeyTimestamp.toString()), 0); const iv = Buffer.concat([Buffer.alloc(8), timestampBuffer]); // 3. 执行解密 const decipher = crypto.createDecipheriv('aes-256-ctr', derivedKey, iv); const encryptedData = await fs.readFile(encryptedFilePath); // CTR模式不需要自动填充,显式关闭避免数据冗余 decipher.setAutoPadding(false); const decryptedData = Buffer.concat([decipher.update(encryptedData), decipher.final()]); await fs.writeFile(decryptedFilePath, decryptedData); return decryptedFilePath; } catch (error: any) { console.error('Error al desencriptar el archivo:', error.message); throw error; } };
关键说明
- HKDF参数:WhatsApp对音频媒体固定使用
salt=WhatsApp Audio、info=WhatsApp Keys,输出32字节对应AES-256密钥。 - IV构造:将
mediaKeyTimestamp转为8字节大端数组后,拼接8个字节的0,组成符合要求的16字节IV。 - 自动填充:CTR模式不需要自动填充,关闭后可避免解密数据出现多余填充字节,导致音频文件结构损坏。
内容的提问来源于stack exchange,提问作者Juan Antonio Marquez
相关产品推荐
相关产品推荐

