Skip to content

Commit 8900a7c

Browse files
committed
fix: 修复大文件解密崩溃并将 XOR 解密移至服务端
问题修复: - 修复解密超过 80MB 视频时页面崩溃的问题(issue #4 #6 #8) - 根因:旧实现将整段视频经 CDP 管道传入浏览器做 XOR,base64 膨胀后超过 Chromium DevTools 协议 100MB 单条消息上限,导致页面被关闭 - 将 XOR 解密从浏览器移至 Node.js 端,浏览器仅负责生成 128KB 密钥流, 视频数据不再进入浏览器,文件大小不再受此限制 新功能: - 新增 lib/decrypt.js 解密核心模块(decryptBuffer / assertMp4) - 新增 decode_key 密钥流 LRU 缓存,命中后跳过浏览器 WASM 调用, 可通过 KEYSTREAM_CACHE_MAX 环境变量配置上限(默认100) - 新增 test/decrypt.test.js 单元测试,覆盖 120MB 大文件边界场景 其他: - Dockerfile 补充拷贝 lib 目录,修复容器内模块缺失 - worker.html 的 decryptVideo 标注为已弃用(保留向后兼容) - 更新 README 架构说明,补充大文件故障排除与「如何获取 decode_key」说明 - 版本号升级至 2.1.0
1 parent 9f0c8a5 commit 8900a7c

8 files changed

Lines changed: 371 additions & 107 deletions

File tree

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,3 +205,8 @@ cython_debug/
205205
marimo/_static/
206206
marimo/_lsp/
207207
__marimo__/
208+
/todo/
209+
210+
# api-service 为 Node.js 子项目,其源码目录不应被 Python 模板的 lib/ 规则忽略
211+
!api-service/lib/
212+
!api-service/lib/**

api-service/Dockerfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ COPY --from=builder /root/.cache/ms-playwright /ms-playwright
6363
COPY server.js .
6464
COPY worker.html .
6565
COPY docs.html .
66+
COPY lib ./lib
6667
COPY wechat_files ./wechat_files
6768

6869
# Create non-root user for security

api-service/README.md

Lines changed: 98 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,37 @@
1515
- **健康检查**: 内置服务健康监控
1616
- **大文件支持**: 最大支持 500MB 视频文件
1717

18+
## 如何获取 decode_key?(常见问题)
19+
20+
> 对应 issue #1 / #9**本仓库只负责解密,不负责抓取微信接口。**
21+
22+
`decode_key` 和加密视频 URL 都来自**微信原始接口的响应 JSON**,结构如下:
23+
24+
```json
25+
{
26+
"data": {
27+
"object_desc": {
28+
"media": [{
29+
"decode_key": "2136343393", // 解密种子(即本服务的 decode_key)
30+
"url": "https://...", // 加密视频下载链接
31+
"file_size": 14088528
32+
}]
33+
}
34+
}
35+
}
36+
```
37+
38+
获取途径(任选其一):
39+
40+
1. **第三方接口**:例如 `api.tikhub.io` 的视频号详情接口,响应直接包含
41+
`decode_key` 与加密视频 URL。
42+
2. **自行抓包**:在 PC 端微信安装 SSL 证书抓包,打开视频号并搜索关键字,
43+
即可在接口响应 JSON 中找到 `decode_key` 和对应加密视频 URL。
44+
> 注意:微信部分流量走私有协议 mmtls,普通 HTTPS 抓包不一定能拿到,需视
45+
> 客户端版本与抓包方案而定。
46+
47+
拿到 `decode_key` 和加密视频文件后,再调用本服务 `POST /api/decrypt` 解密。
48+
1849
## 架构说明
1950

2051
```
@@ -27,7 +58,8 @@
2758
│ Express.js API Server │
2859
│ (Node.js + Multer + CORS) │
2960
└───────────────────┬─────────────────────────────────┘
30-
│ RPC Call via page.evaluate()
61+
│ ① RPC:仅生成 128KB 密钥流
62+
│ page.evaluate(generateKeystream)
3163
3264
┌─────────────────────────────────────────────────────┐
3365
│ Playwright Chromium Browser │
@@ -39,13 +71,24 @@
3971
│ │ └─────────────────────────────────┘ │ │
4072
│ │ │ │
4173
│ │ RPC Functions: │ │
42-
│ │ - generateKeystream(decodeKey) │ │
43-
│ │ - decryptVideo(encrypted, keystream) │ │
74+
│ │ - generateKeystream(decodeKey) ← 唯一职责 │ │
4475
│ │ - checkWasmStatus() │ │
4576
│ └───────────────────────────────────────────┘ │
77+
└───────────────────┬─────────────────────────────────┘
78+
│ ② 返回 128KB 密钥流
79+
80+
┌─────────────────────────────────────────────────────┐
81+
│ ③ Node.js 端 XOR 解密(lib/decrypt.js) │
82+
│ 视频整段 Buffer 不进浏览器 → 规避 CDP 100MB 上限 │
4683
└─────────────────────────────────────────────────────┘
4784
```
4885

86+
> **v2.1 架构变更(修复大文件解密崩溃)**:旧版把整段视频经 CDP 管道
87+
> 传入浏览器做 XOR,base64 膨胀后超过 Chromium DevTools 协议 100MB 单条
88+
> 消息上限,导致 80MB+ 文件解密时页面崩溃(issue #4 / #6 / #8)。现在
89+
> 浏览器只负责生成 128KB 密钥流,XOR 解密改在 Node.js 端用 Buffer 完成,
90+
> 文件大小不再受此限制。
91+
4992
### 为什么选择 Playwright?
5093

5194
WeChat 的 WASM 模块依赖浏览器特定的 API (`fetch`, `self`, `window` 等),无法直接在 Node.js 环境中运行。Playwright 方案的优势:
@@ -129,7 +172,7 @@ GET /
129172
```json
130173
{
131174
"service": "WeChat Channels Video Decryption API",
132-
"version": "2.0.0",
175+
"version": "2.1.0",
133176
"engine": "Playwright + Chromium",
134177
"author": "Evil0ctal",
135178
"endpoints": {
@@ -153,7 +196,7 @@ GET /health
153196
{
154197
"status": "ok",
155198
"service": "wechat-decrypt-api",
156-
"version": "2.0.0",
199+
"version": "2.1.0",
157200
"engine": "playwright",
158201
"wasm": {
159202
"loaded": true,
@@ -428,19 +471,22 @@ const keystreamBase64 = await page.evaluate(async (key) => {
428471

429472
**关键**: 使用 `http://` 而非 `file://` 协议,避免浏览器 CORS 限制,使本地 WASM 文件加载成功。
430473

431-
### 3. 数据传输
474+
### 3. 数据传输(v2.1:视频不进浏览器)
432475

433-
使用 Base64 编码在 Node.js 和浏览器之间传输二进制数据:
476+
浏览器与 Node.js 之间**只传输 128KB 密钥流**,加密视频本身始终留在 Node.js 端:
434477

435478
```javascript
436-
// Node.js → Browser
437-
const encryptedBase64 = videoFile.buffer.toString('base64');
438-
439-
// Browser → Node.js
440-
const decryptedBase64 = await page.evaluate(...);
441-
const decrypted = Buffer.from(decryptedBase64, 'base64');
479+
// Browser → Node.js:仅密钥流(base64,约 175KB,远小于 CDP 100MB 上限)
480+
const keystreamBase64 = await page.evaluate(
481+
async (key) => await window.generateKeystream(key),
482+
decode_key
483+
);
484+
const keystream = Buffer.from(keystreamBase64, 'base64');
442485
```
443486

487+
> 旧版会把整段视频 `toString('base64')` 传入浏览器,80MB 文件膨胀到约
488+
> 107MB,超过 CDP 管道 100MB 上限而导致页面崩溃。v2.1 不再传输视频数据。
489+
444490
### 4. Isaac64 密钥流生成
445491

446492
WASM 模块实现了微信魔改的 Isaac64 算法:
@@ -455,16 +501,25 @@ window.wasm_isaac_generate = function(ptr, size) {
455501

456502
**重要**: 密钥流必须反转 (`reverse()`) 才能正确解密,这是微信特有的实现细节。
457503

458-
### 5. XOR 解密
504+
### 5. XOR 解密(v2.1:Node.js 端执行)
459505

460-
前 128KB 数据通过 XOR 操作解密:
506+
微信仅加密文件**前 128KB**,其余为明文。XOR 解密在 Node.js 端用 Buffer 完成
507+
`lib/decrypt.js``decryptBuffer`),无需把视频送进浏览器:
461508

462509
```javascript
463-
for (let i = 0; i < 131072 && i < encrypted.length; i++) {
464-
decrypted[i] = encrypted[i] ^ keystream[i];
510+
function decryptBuffer(encrypted, keystream) {
511+
const decrypted = Buffer.from(encrypted); // 明文部分原样保留
512+
const decryptLen = Math.min(KEYSTREAM_SIZE, encrypted.length, keystream.length);
513+
for (let i = 0; i < decryptLen; i++) {
514+
decrypted[i] = encrypted[i] ^ keystream[i];
515+
}
516+
return decrypted;
465517
}
466518
```
467519

520+
该逻辑与 Python CLI(`decrypt_wechat_video_cli.py`)字节级一致,并有单元测试
521+
`npm test`)覆盖含 120MB 大文件在内的边界场景。
522+
468523
## 故障排除
469524

470525
### 问题: WASM 模块加载超时
@@ -506,6 +561,20 @@ docker-compose build --no-cache
506561
2. 检查 API 响应中的错误信息
507562
3. 验证解密文件的前 12 字节应为: `00 00 00 XX 66 74 79 70` (MP4 签名)
508563

564+
### 问题: 大文件(80MB+)解密报错 "Target page... has been closed"
565+
566+
**症状**(issue #4 / #6 / #8):
567+
```
568+
page.evaluate: Target page, context or browser has been closed
569+
Too large read data is pending: capacity=104857600 ...
570+
```
571+
572+
**原因**: 旧版(≤ v2.0)把整段视频经 CDP 管道传入浏览器做 XOR,base64 膨胀后
573+
超过 Chromium DevTools 协议 100MB 单条消息上限,导致页面崩溃。
574+
575+
**解决方案**: 升级到 **v2.1+**。XOR 解密已移至 Node.js 端,浏览器只生成 128KB
576+
密钥流,文件大小不再受此限制(仅受 multer 500MB 与服务器内存约束)。
577+
509578
### 问题: 文件上传失败 (413 错误)
510579

511580
**症状**:
@@ -537,9 +606,11 @@ deploy:
537606
memory: 4G # 增加内存限制
538607
```
539608
540-
### 3. 启用请求缓存
609+
### 3. 密钥流缓存(v2.1 已内置)
541610
542-
对于相同的 decode_key,可以缓存生成的密钥流以提高性能。
611+
相同 `decode_key` 的密钥流恒定,服务端已内置 LRU 缓存:命中后跳过浏览器 WASM
612+
调用(并发接口连页面池都不占用)。缓存上限可通过环境变量 `KEYSTREAM_CACHE_MAX`
613+
配置(默认 100)。
543614

544615
### 4. 负载均衡
545616

@@ -569,6 +640,14 @@ Evil0ctal - evil0ctal1985@gmail.com
569640

570641
## 更新日志
571642

643+
### v2.1.0 (2026-06-09)
644+
- **修复大文件解密崩溃**(issue #4 / #6 / #8):XOR 解密从浏览器移至 Node.js 端,
645+
视频数据不再经 CDP 管道传输,规避 DevTools 协议 100MB 单条消息上限。80MB+ /
646+
100MB+ / 300MB+ 文件均可正常解密。
647+
- **新增密钥流 LRU 缓存**:相同 `decode_key` 跳过浏览器 WASM 调用,可经
648+
`KEYSTREAM_CACHE_MAX` 配置上限。
649+
- **解密核心抽离为 `lib/decrypt.js`** 并新增单元测试(`npm test`,含 120MB 边界场景)。
650+
572651
### v2.0.0 (2025-10-17)
573652
- 采用 Playwright + RPC 架构
574653
- 100% 兼容微信官方 WASM v1.2.46

api-service/lib/decrypt.js

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/**
2+
* 视频解密核心(纯函数,Node.js 端执行)
3+
*
4+
* 微信视频号仅加密文件前 KEYSTREAM_SIZE(128KB) 字节,其余为明文。
5+
* 解密无需浏览器:浏览器只负责用 WASM 生成 128KB 密钥流,XOR 在此完成。
6+
* 旧实现把整段视频经 CDP 管道传入浏览器,会触发 DevTools 协议 100MB 单条
7+
* 消息上限,导致大文件解密时页面崩溃(见 issue #4 / #6 / #8)。
8+
*
9+
* @module lib/decrypt
10+
*/
11+
12+
// 微信加密作用的字节数(密钥流长度)
13+
const KEYSTREAM_SIZE = 131072;
14+
15+
/**
16+
* 对加密视频执行 XOR 解密
17+
* @param {Buffer} encrypted 原始加密视频 Buffer
18+
* @param {Buffer} keystream 128KB 密钥流 Buffer
19+
* @returns {Buffer} 解密后的视频 Buffer(明文部分原样保留)
20+
*/
21+
function decryptBuffer(encrypted, keystream) {
22+
const decrypted = Buffer.from(encrypted); // 拷贝一份,明文部分原样保留
23+
const decryptLen = Math.min(KEYSTREAM_SIZE, encrypted.length, keystream.length);
24+
for (let i = 0; i < decryptLen; i++) {
25+
decrypted[i] = encrypted[i] ^ keystream[i];
26+
}
27+
return decrypted;
28+
}
29+
30+
/**
31+
* 校验解密结果是否为合法 MP4(偏移 4 处应为 ftyp 签名)
32+
* @param {Buffer} buffer 解密后的视频 Buffer
33+
* @throws {Error} 当签名缺失时(通常是 decode_key 不匹配)
34+
*/
35+
function assertMp4(buffer) {
36+
const ftyp = buffer.toString('utf8', 4, 8);
37+
if (ftyp !== 'ftyp') {
38+
throw new Error('解密失败:未找到 MP4 ftyp 签名,请检查 decode_key');
39+
}
40+
}
41+
42+
module.exports = { KEYSTREAM_SIZE, decryptBuffer, assertMp4 };

api-service/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
{
22
"name": "wechat-decrypt-api-playwright",
3-
"version": "2.0.0",
3+
"version": "2.1.0",
44
"description": "WeChat Channels Video Decryption API Service (Playwright Edition) - 100% compatible with WeChat official WASM module",
55
"main": "server.js",
66
"scripts": {
77
"start": "node server.js",
88
"dev": "nodemon server.js",
9-
"test": "node test.js",
9+
"test": "node test/decrypt.test.js",
1010
"install-browsers": "npx playwright install chromium --with-deps"
1111
},
1212
"keywords": [

0 commit comments

Comments
 (0)