Files
weixin-plugin-test/weixin-plugin/weixin/cdn/pic-decrypt.ts
zyh 77019ec050 feat: 初始化微信插件独立测试环境
添加完整的微信插件测试框架,包含:
- 微信插件源码(weixin-plugin/)
- TypeScript 类型定义(types.ts)
- 插件编译脚本(scripts/build-weixin-plugin.sh)
- 简化测试脚本(scripts/test-weixin-simple.js)
- 完整测试脚本(scripts/test-weixin-plugin.js)
- 项目配置和依赖管理(package.json)
- 使用说明文档(README.md)

主要功能:
- 支持二维码登录认证
- HTTP 长轮询消息接收
- 文本、图片、视频、文件消息收发
- 自动回复测试
- 运行状态统计

Generated with [Claude Code](https://claude.com/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
2026-03-30 07:14:00 +00:00

86 lines
3.0 KiB
TypeScript

import { decryptAesEcb } from "./aes-ecb.js";
import { buildCdnDownloadUrl } from "./cdn-url.js";
import { logger } from "../util/logger.js";
/**
* Download raw bytes from the CDN (no decryption).
*/
async function fetchCdnBytes(url: string, label: string): Promise<Buffer> {
let res: Response;
try {
res = await fetch(url);
} catch (err) {
const cause =
(err as NodeJS.ErrnoException).cause ?? (err as NodeJS.ErrnoException).code ?? "(no cause)";
logger.error(
`${label}: fetch network error url=${url} err=${String(err)} cause=${String(cause)}`,
);
throw err;
}
logger.debug(`${label}: response status=${res.status} ok=${res.ok}`);
if (!res.ok) {
const body = await res.text().catch(() => "(unreadable)");
const msg = `${label}: CDN download ${res.status} ${res.statusText} body=${body}`;
logger.error(msg);
throw new Error(msg);
}
return Buffer.from(await res.arrayBuffer());
}
/**
* Parse CDNMedia.aes_key into a raw 16-byte AES key.
*
* Two encodings are seen in the wild:
* - base64(raw 16 bytes) → images (aes_key from media field)
* - base64(hex string of 16 bytes) → file / voice / video
*
* In the second case, base64-decoding yields 32 ASCII hex chars which must
* then be parsed as hex to recover the actual 16-byte key.
*/
function parseAesKey(aesKeyBase64: string, label: string): Buffer {
const decoded = Buffer.from(aesKeyBase64, "base64");
if (decoded.length === 16) {
return decoded;
}
if (decoded.length === 32 && /^[0-9a-fA-F]{32}$/.test(decoded.toString("ascii"))) {
// hex-encoded key: base64 → hex string → raw bytes
return Buffer.from(decoded.toString("ascii"), "hex");
}
const msg = `${label}: aes_key must decode to 16 raw bytes or 32-char hex string, got ${decoded.length} bytes (base64="${aesKeyBase64}")`;
logger.error(msg);
throw new Error(msg);
}
/**
* Download and AES-128-ECB decrypt a CDN media file. Returns plaintext Buffer.
* aesKeyBase64: CDNMedia.aes_key JSON field (see parseAesKey for supported formats).
*/
export async function downloadAndDecryptBuffer(
encryptedQueryParam: string,
aesKeyBase64: string,
cdnBaseUrl: string,
label: string,
): Promise<Buffer> {
const key = parseAesKey(aesKeyBase64, label);
const url = buildCdnDownloadUrl(encryptedQueryParam, cdnBaseUrl);
logger.debug(`${label}: fetching url=${url}`);
const encrypted = await fetchCdnBytes(url, label);
logger.debug(`${label}: downloaded ${encrypted.byteLength} bytes, decrypting`);
const decrypted = decryptAesEcb(encrypted, key);
logger.debug(`${label}: decrypted ${decrypted.length} bytes`);
return decrypted;
}
/**
* Download plain (unencrypted) bytes from the CDN. Returns the raw Buffer.
*/
export async function downloadPlainCdnBuffer(
encryptedQueryParam: string,
cdnBaseUrl: string,
label: string,
): Promise<Buffer> {
const url = buildCdnDownloadUrl(encryptedQueryParam, cdnBaseUrl);
logger.debug(`${label}: fetching url=${url}`);
return fetchCdnBytes(url, label);
}