📄 media.ts

import crypto from "node:crypto";
import { decodeEncodingAESKey, pkcs7Unpad, WECOM_PKCS7_BLOCK_SIZE } from "./crypto.js";
import { readResponseBodyAsBuffer, wecomFetch, type WecomHttpOptions } from "./http.js";

/**
 * **decryptWecomMedia (解密企业微信媒体文件)**
 * 
 * 简易封装:直接传入 URL 和 AES Key 下载并解密。
 * 企业微信媒体文件使用与消息体相同的 AES-256-CBC 加密,IV 为 AES Key 前16字节。
 * 解密后需移除 PKCS#7 填充。
 */
export async function decryptWecomMedia(url: string, encodingAESKey: string, maxBytes?: number): Promise<Buffer> {
    return decryptWecomMediaWithHttp(url, encodingAESKey, { maxBytes });
}

/**
 * **decryptWecomMediaWithHttp (解密企业微信媒体 - 高级)**
 * 
 * 支持传递 HTTP 选项(如 Proxy、Timeout)。
 * 流程:
 * 1. 下载加密内容。
 * 2. 准备 AES Key 和 IV。
 * 3. AES-CBC 解密。
 * 4. PKCS#7 去除填充。
 */
export async function decryptWecomMediaWithHttp(
    url: string,
    encodingAESKey: string,
    params?: { maxBytes?: number; http?: WecomHttpOptions },
): Promise<Buffer> {
    // 1. Download encrypted content
    const res = await wecomFetch(url, undefined, { ...params?.http, timeoutMs: params?.http?.timeoutMs ?? 15_000 });
    if (!res.ok) {
        throw new Error(`failed to download media: ${res.status}`);
    }
    const encryptedData = await readResponseBodyAsBuffer(res, params?.maxBytes);

    // 2. Prepare Key and IV
    const aesKey = decodeEncodingAESKey(encodingAESKey);
    const iv = aesKey.subarray(0, 16);

    // 3. Decrypt
    const decipher = crypto.createDecipheriv("aes-256-cbc", aesKey, iv);
    decipher.setAutoPadding(false); // We handle padding manually
    const decryptedPadded = Buffer.concat([
        decipher.update(encryptedData),
        decipher.final(),
    ]);

    // 4. Unpad
    // Note: Unlike msg bodies, usually removing PKCS#7 padding is enough for media files.
    // The Python SDK logic: pad_len = decrypted_data[-1]; decrypted_data = decrypted_data[:-pad_len]
    // Our pkcs7Unpad function does exactly this + validation.
    return pkcs7Unpad(decryptedPadded, WECOM_PKCS7_BLOCK_SIZE);
}