No description
  • JavaScript 100%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
congshuhao bebd4d7a75 糖心VLOG 解锁:从 YQC 私密专区模块提取的独立实现
- 拦截 tx*.com /h5/{user/info,system/info,movie/block,movie/search},AES-128-ECB/PKCS7 解包后改写会员态与广告位
- /h5/movie/detail 转发上游桥接服务解锁(含签名/加码/保护包协议实现)
- 支持 crypt_key 本地模式,完全脱离上游服务
- dump_crypt / dump_detail 只读诊断开关
- 与原脚本逐字段等价对拍通过
2026-09-28 13:33:02 +08:00
tools 糖心VLOG 解锁:从 YQC 私密专区模块提取的独立实现 2026-09-28 13:33:02 +08:00
.gitignore 糖心VLOG 解锁:从 YQC 私密专区模块提取的独立实现 2026-09-28 13:33:02 +08:00
README.md 糖心VLOG 解锁:从 YQC 私密专区模块提取的独立实现 2026-09-28 13:33:02 +08:00
tx-vip.js 糖心VLOG 解锁:从 YQC 私密专区模块提取的独立实现 2026-09-28 13:33:02 +08:00
tx-vip.sgmodule 糖心VLOG 解锁:从 YQC 私密专区模块提取的独立实现 2026-09-28 13:33:02 +08:00

糖心VLOG 解锁方案(提取自 YQC「私密专区」模块)

来源:https://yqc-premium.pages.dev/Surge/nsfw.sgmodule(需带 User-Agent: Surge/... 请求头,否则站点返回 403)。 模块本身只是壳,真正的逻辑在 https://yqc-premium.pages.dev/Scripts/NSFW/tx.vip.js(240 KB,string-array + RC4/Base64 混淆,另含 vue/crypto-js/URL polyfill 打包)。 本目录是该脚本中糖心VLOG部分的方案提取与可读重写。

tx-vip.sgmodule   仅含糖心VLOG的 Surge 模块(3 条脚本规则 + MITM + 桥接直连规则)
tx-vip.js         提取后的可读实现(等价于原脚本的糖心VLOG分支)
tools/dump-crypt.js       离线解开 crypt 保护包,取出 tx 接口 AES 密钥

安装

Surge → 模块 → 从 URL 安装:

https://git.szzn.dev/congshuhao/tx-vip/raw/branch/main/tx-vip.sgmodule

装完直接打开模块的参数界面填写,不用改模块正文、改完立即生效:

参数 默认 说明
crypt_key 空 tx 接口 AES 密钥(16 位)→ 本地模式,不访问上游服务
vip_key 空 上游桥接服务会员密钥 → 桥接模式(上游仅允许中国大陆 IP)
policy_group DIRECT 桥接请求使用的策略组
dump_crypt 空 填 1:把 crypt_key 连同来源推送出来(优先读原脚本缓存,可离线取密钥)
dump_detail 空 填 1:把 /h5/movie/detail 明文推送出来(只读,不改写)

crypt_key 与 vip_key 填一个即可;两个都填时 crypt_key 优先。模块里 script-path 写的是绝对 URL(script-update-interval=3600,最多 1 小时拉一次更新),因此脚本文件必须与模块一起放在仓库里。

1. 拦截面

接口 类型 作用
/h5/user/info http-response 伪造尤物卡(会员态)
/h5/system/info http-response 去除全局广告配置
/h5/movie/block http-response 过滤信息流广告条目
/h5/movie/search http-response 过滤搜索结果中的推广条目
/h5/movie/detail http-request 详情页解锁(转发到上游服务端)
  • MITM:tx*.com、*.tx*.com
  • 规则:^https?://(\w+\.)?tx[a-z]{1}\d+\.com/h5/(user/info|movie/(block|search)|system/info)、^https?://(\w+\.)?tx[a-z]{1}\d+\.com/h5/movie/detail
  • 四个响应接口全部 requires-body=true,响应体是密文。

2. 接口体编解码

ciphertext = Base64( AES-128-ECB / PKCS7 ( UTF-8(JSON) ) )
key        = crypt.key(16 位 ASCII,由服务端按 app 版本下发)
  • 解密:CryptoJS.AES.decrypt(body, Utf8.parse(crypt.key), {mode: ECB, padding: Pkcs7})
  • 回写:同一密钥加密后 $done({body})
  • 解密失败 → 强制刷新一次 crypt 配置再试,仍失败则 $done({})(原样放行)

/h5/movie/detail 是请求脚本(echo response):把原始请求原样转给上游,上游返回的内容直接作为响应体回写(已由上游加密/改写好),脚本本身不碰详情体。

3. crypt 配置获取协议(私有桥接服务)

糖心的 AES 密钥不在客户端,由作者的服务端下发,请求经 vipKey 签名。

POST https://api.yqc-premium.com/nsfw
headers:
  x-ver: 3
  content-type: application/json; charset=utf-8
  x-auth-uid: shuffle(vipKey)                       # 确定性洗牌,LCG(seed=42, 9301, 49297, 233280)
  x-tools: Base64(UTF-8(encodeURIComponent(环境名)))  # Surge/Loon/Stash/Quantumult X/Egern/Shadowrocket
  x-timestamp: UTC+8 毫秒时间戳
  x-id: txvlog                                      # app 代号
  x-sign: HmacMD5(message, vipKey)                  # message = "environment:<env>\ntimestamp:<ts>\ncode:<code>\nv9w$=gxaqa"
  x-policy: encodeURIComponent(策略组)
body: {"y": <逐字符加码(JSON.stringify(payload), code)>}   # charCodeAt 相加,code=tvxlog 循环作为密钥
payload(action=crypt): {"action":"crypt"}

响应若带 x-enc: 1,体格式为 $<key>$<Base64(AES-CBC/PKCS7)>,其中 key 同时充当 IV。

保护包解包(crypt 下发体):

{"t.me/yqc_000": {"v":"p1", "s":<seed>, "d":<密文>, "h":<校验>}}

derived   = MD5(vipKey + ":" + appId + ":" + s + ":yqc-crypt")        # 32 位十六进制
key       = derived[8:24]        signSeed = derived[0:16]
maskShift = parseInt(derived[24:26],16) % 65
chunkSize = 4 + parseInt(derived[26:28],16) % 3
校验      : h == MD5(v + ":" + s + ":" + d + ":" + signSeed)
解 d      : ① 按 chunkSize 逐块反转;② 用错位 65 字符表映射回标准 Base64;③ AES-CBC(key=IV=key) 解密

解出的 JSON 使用单字符短字段名,需还原:e→expiresAt r→refreshIntervalMs t→fetchedAt g→fetchedDate k→key v→iv c→crypt d→data f→profile。 结果缓存在 $persistentStore,默认 24h(expiresAt/refreshIntervalMs/跨天任一不满足即重取)。

3.1 取出 crypt.key

密钥只在桥接服务的响应里,且桥接服务只接受中国大陆出口 IP(实测:HK/JP/US/NL/搬瓦工/直连一律 403 {"code":403,"message":"仅允许中国大陆 IP 访问"})。三条路径:

  1. 手机上有缓存(最省事):原模块跑成功过一次,密文包就在 Surge 持久化里。 给本模块参数「导出密钥」填 1,跑一次糖心,通知/日志即给出 crypt_key(优先读原脚本缓存 YQC_PREMIUM.txvlog,无需任何桥接请求):

    通知: crypt_key = xxxxxxxxxxxxxxxx   来源: 原脚本缓存
    

    若把持久化存储导出成 JSON,也可离线解:

    node tools/dump-crypt.js <会员密钥> <存储 JSON 或桥接响应>
    
  2. 从桥接服务现取:必须在中国大陆网络下(手机蜂窝/国内宽带)发请求;本机所有可用出口都被拒。

  3. 从 App 里取:密钥物理上必须在客户端(App 要能解密自己 /h5/* 的响应体)。模块里的 tx*a?.com 域名已全部轮换/被抢注(实测 260 个候选今天无一是原 App),txapp.tv 已死,发布地址页已被 ParkLogic 停放,需要重新拿到当前 APK 或 H5 包再逆向(搜 AES/ECB/Pkcs7/16 位常量;若运行时下发则 hook App 的解密函数)。

crypt_key 是按 App 版本轮换的快照,本地模式不会自动刷新:密钥失效后本脚本会逐项失败回退为原样放行(这也是原方案要挂桥接服务的原因)。

4. 本地改写语义

/h5/system/info  → data 合并:
  { ad_auto_jump:'n', ad_show_time:0, ad:null, ads:[], bottom_ad:null, bottom_ads:[],
    layer_ad:null, layer_ads:[], layer_app:[], post_banner:[], dating_frame:[], dark_tips:'' }

/h5/movie/block  → data = data.filter(item => !item.ad)            // 丢弃带 ad 字段的条目
/h5/movie/search → data = data.filter(item => item.ad_link === '') // ad_link 非空即推广位
/h5/user/info    → data = {...原 data, nickname, headico, sign, ad:[],
                     is_vip:'y', is_dark_vip:'y', is_up:'y', is_mer:'y',
                     phone:'18888888888', exp_level:'6',
                     group_name:'糖心至尊卡', group_end_time:'2099-01-01', fans_club_link}

movie/search 是保留 ad_link === '' 的条目(ad_link 非空代表推广结果),原脚本即如此,不要写反。

5. 详情解锁

/h5/movie/detail 走 script-analyze-echo-response:把被拦截的原始请求

{ "url": "...", "method": "...", "headers": {...}, "body": "..." }

按第 3 节的签名协议 POST 到 https://api.yqc-premium.com/nsfw(x-id: txvlog),服务端用同样的 tx 密钥/算法取号并回写已解锁的详情体,脚本以

$done({ response: { status, headers, body } })

短路上游。失败时 $done({}),请求按原样继续。解锁逻辑在服务端,脚本侧没有本地实现。

注意作者的实现选择:他们自己就持有 tx 密钥(四个净化接口的 key 由他们下发),却没有本地改写详情,说明播放权限(能否拿到可播地址)是在服务端判定的,客户端改包改不动。这是推断,证据是他们的实现方式。

5.1 详情探针:判断本地能不能补

本地模式(crypt_key)放行详情请求后,响应体会被下面的规则解密出来看一眼,不改写:

详情探针=1   →  通知/日志给出 /h5/movie/detail 的明文 JSON

用法:找一部看不了的视频与一部能看的视频,分别打开一次,对比两份明文。差异字段就是权限开关:

  • 若差异出现在 is_buy/is_vip/play_auth 这类可改字段 → 本地解锁有戏,可以照净化接口的方式补;
  • 若 m3u8/dash 是空串、播放地址由服务端另行签发 → 本地无论怎么改都拿不到地址,只能走桥接服务。

6. 与 tx-vip.js 的差异

  • 保留:全部接口、编解码算法、桥接协议、签名/加码/保护包算法、缓存与强制刷新策略、$done 回写形态(与原脚本逐字节比对通过)。
  • 去除:作者通知中的 Telegram/图片推广链接、拉黑/授权逻辑、其它 app 分支、打包的 vue/crypto-js/URL polyfill(改用宿主 require('crypto-js'))。
  • user/info 的 nickname/headico/sign/fans_club_link 改为中性值(原为作者推广),功能字段一致。
  • 新增 crypt_key 参数:直接给出 tx 密钥即可完全脱离桥接服务(vip_key 与 crypt_key 二选一)。此模式下详情接口无法解锁,会原样放行。
  • 新增 dump_crypt / dump_detail 诊断开关:只读导出,不改写任何响应。

7. 部署

仓库(Forgejo):git@git.szzn.dev:congshuhao/tx-vip.git。 模块用绝对 URL 拉脚本,改脚本后无需重装模块(script-update-interval=3600)。

nginx/Gitea raw 目录结构:
tx-vip.sgmodule
tx-vip.js
tools/dump-crypt.js

模块里的 [Rule] DOMAIN-SUFFIX,yqc-premium.com,DIRECT 保证桥接请求走直连(上游只要大陆 IP);只用本地模式可以删掉这条。

8. 验证

原脚本与本实现在同一套 Surge mock($request/$response/$done/$httpClient/$persistentStore/$notification,x-enc 信封、保护包、签名全部按上文生成)下逐项比对:

  • /h5/user/info、/h5/system/info、/h5/movie/block、/h5/movie/search:解密结果逐字段一致,会员字段与广告清理一致;
  • /h5/movie/detail:转发载荷与最终 $done({response}) 完全一致;桥接请求经签名/加码校验(x-sign、x-tools、x-auth-uid、x-id、x-policy、体加码全部匹配);
  • 缓存命中不重复请求、密钥失配后强制重取、crypt_key 本地模式零桥接请求、未配置密钥时 $done({}) 原样放行、模块三条规则匹配/拒绝用例;
  • dump_crypt:用真实原脚本跑出的持久化缓存离线解出 key,桌面工具与脚本内两条路径都能还原;
  • dump_detail:只读导出详情明文,响应体逐字节原样放行,且不影响其它接口。