diff --git a/core/huya/app_login.py b/core/huya/app_login.py index 9df356c..be730fe 100644 --- a/core/huya/app_login.py +++ b/core/huya/app_login.py @@ -839,9 +839,15 @@ class HuyaAppPasswordLogin: # (App 渠道),缺字段注入浏览器会导致 UDB SDK 判定设备态不完整。 from .web_cookie_fields import ( fill_web_cookie_fields, + qimei_feature_from_device_profile, ) - full_cookie = fill_web_cookie_fields(cookie_str, guid="", uid=uid) + full_cookie = fill_web_cookie_fields( + cookie_str, + guid="", + uid=uid, + qimei_feature=qimei_feature_from_device_profile(self.device_info), + ) appid_fixed = "; ".join( f"{key}={value}" if key != "udb_appid" else "udb_appid=5002" for key, value in cookie_pairs(full_cookie) diff --git a/core/huya/web_cookie_fields.py b/core/huya/web_cookie_fields.py index 7aa30e2..8cf6721 100644 --- a/core/huya/web_cookie_fields.py +++ b/core/huya/web_cookie_fields.py @@ -20,6 +20,7 @@ from __future__ import annotations import random import secrets import time +from collections.abc import Mapping # 统计/会话字段生成规则;9.1 抓包值为样例,格式保持对齐 _FILL_MAP: tuple[tuple[str, str], ...] = ( @@ -42,6 +43,32 @@ _BASE16 = "0123456789ABCDEF" _GAME_DID_CHARS = ( "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_" ) +_DEFAULT_QIMEI_FEATURE = { + "screen": {"resolution": "800x600", "availResolution": "800x600"}, + "navigator": { + "deviceMemory": 16, + "pdfViewerEnabled": True, + "cookieEnabled": True, + "productSub": "20030107", + "hardwareConcurrency": 10, + }, + "webgl": "Google Inc. (Apple)~ANGLE (Apple, ANGLE Metal Renderer: Apple M4, Unspecified Version)", + "timezoneOffset": -480, +} +_DEVICE_GPU_FEATURES = { + "xiaomi": ("Qualcomm", "Adreno (TM) 650"), + "redmi": ("Qualcomm", "Adreno (TM) 650"), + "vivo": ("Qualcomm", "Adreno (TM) 650"), + "iqoo": ("Qualcomm", "Adreno (TM) 650"), + "oppo": ("Qualcomm", "Adreno (TM) 650"), + "oneplus": ("Qualcomm", "Adreno (TM) 650"), + "samsung": ("ARM", "Mali-G78"), + "honor": ("ARM", "Mali-G78"), + "motorola": ("Qualcomm", "Adreno (TM) 642L"), + "realme": ("Qualcomm", "Adreno (TM) 650"), + "nubia": ("Qualcomm", "Adreno (TM) 650"), + "gionee": ("Qualcomm", "Adreno (TM) 506"), +} def _return_base16(a: int) -> str: @@ -96,6 +123,140 @@ def _game_did_generate(length: int = 35) -> str: return out +_MASK64 = (1 << 64) - 1 + + +def _rotl64(value: int, shift: int) -> int: + shift %= 64 + if not shift: + return value & _MASK64 + return ((value << shift) | (value >> (64 - shift))) & _MASK64 + + +def _fmix64(value: int) -> int: + value ^= value >> 33 + value = value * 0xFF51AFD7ED558CCD & _MASK64 + value ^= value >> 33 + value = value * 0xC4CEB9FE1A85EC53 & _MASK64 + value ^= value >> 33 + return value & _MASK64 + + +def _x64hash128(value: str, seed: int = 0) -> str: + """复刻 QIMEI SDK 的 x64hash128(MurmurHash3 变体)。 + + SDK 按 JavaScript ``charCodeAt`` 取每个 UTF-16 code unit 的低 8 位;尾部 + 的第一个字节也保留其实现中的高 32 位布局,而不是标准 MurmurHash 布局。 + """ + encoded = value.encode("utf-16-le", "surrogatepass") + data = bytes(encoded[index] for index in range(0, len(encoded), 2)) + length = len(data) + h1 = h2 = seed & _MASK64 + c1 = 0x87C37B91114253D5 + c2 = 0x4CF5AD432745937F + + for offset in range(0, length - length % 16, 16): + k1 = int.from_bytes(data[offset : offset + 8], "little") + k2 = int.from_bytes(data[offset + 8 : offset + 16], "little") + k1 = _rotl64(k1 * c1 & _MASK64, 31) * c2 & _MASK64 + h1 ^= k1 + h1 = (_rotl64(h1, 27) + h2) & _MASK64 + h1 = (h1 * 5 + 0x52DCE729) & _MASK64 + k2 = _rotl64(k2 * c2 & _MASK64, 33) * c1 & _MASK64 + h2 ^= k2 + h2 = (_rotl64(h2, 31) + h1) & _MASK64 + h2 = (h2 * 5 + 0x38495AB5) & _MASK64 + + tail = data[length - length % 16 :] + tail_len = len(tail) + k1 = k2 = 0 + for index in range(tail_len - 1, 7, -1): + k2 ^= tail[index] << (8 * (index - 8)) + if tail_len >= 9: + h2 ^= _rotl64(k2 * c2 & _MASK64, 33) * c1 & _MASK64 + for index in range(min(tail_len, 8) - 1, 0, -1): + k1 ^= tail[index] << (8 * index) + if tail_len >= 1: + # QIMEI's JS implementation passes [charCodeAt(g)] here, placing the + # first tail byte in the high 32-bit word. + k1 ^= tail[0] << 32 + h1 ^= _rotl64(k1 * c1 & _MASK64, 31) * c2 & _MASK64 + + h1 ^= length + h2 ^= length + h1 = (h1 + h2) & _MASK64 + h2 = (h2 + h1) & _MASK64 + h1 = _fmix64(h1) + h2 = _fmix64(h2) + h1 = (h1 + h2) & _MASK64 + h2 = (h2 + h1) & _MASK64 + return f"{h1:016x}{h2:016x}" + + +def _js_text(value: object) -> str: + if value is None: + return "" + if isinstance(value, bool): + return "true" if value else "false" + return str(value) + + +def _qimei_feature_string(feature: Mapping[str, object]) -> str: + screen = feature.get("screen") + screen = screen if isinstance(screen, Mapping) else {} + navigator = feature.get("navigator") + navigator = navigator if isinstance(navigator, Mapping) else {} + values = ( + screen.get("resolution"), + screen.get("availResolution"), + navigator.get("deviceMemory"), + navigator.get("pdfViewerEnabled"), + navigator.get("cookieEnabled"), + navigator.get("productSub"), + navigator.get("hardwareConcurrency"), + feature.get("webgl"), + feature.get("timezoneOffset"), + ) + return "###".join(_js_text(item) for item in values) + "###" + + +def qimei_fingerprint_generate(feature: Mapping[str, object] | None = None) -> str: + """生成 QIMEI ``_qimei_fingerprint``;输入应为 SDK ``getFeature()`` 结果。""" + source = feature or _DEFAULT_QIMEI_FEATURE + return _x64hash128(_qimei_feature_string(source), 31) + + +def qimei_feature_from_device_profile( + device_info: Mapping[str, object] | None, +) -> dict[str, object]: + """将 Android 设备画像映射为 QIMEI WebView 能力特征的近似值。""" + info = device_info or {} + width = str(info.get("width") or "1080") + height = str(info.get("height") or "2400") + vendor = str(info.get("vendor") or "").strip().lower() + model = str(info.get("model") or "Android WebView").strip() + gpu_vendor, renderer = _DEVICE_GPU_FEATURES.get( + vendor, ("Qualcomm", f"{model} Graphics") + ) + device_memory = info.get("device_memory") or 8 + hardware_concurrency = info.get("hardware_concurrency") or 8 + return { + "screen": { + "resolution": f"{width}x{height}", + "availResolution": f"{width}x{height}", + }, + "navigator": { + "deviceMemory": device_memory, + "pdfViewerEnabled": False, + "cookieEnabled": True, + "productSub": "20030107", + "hardwareConcurrency": hardware_concurrency, + }, + "webgl": f"Google Inc. ({gpu_vendor})~ANGLE ({gpu_vendor}, {renderer})", + "timezoneOffset": -480, + } + + def _rand_float() -> str: import random @@ -108,6 +269,7 @@ def fill_web_cookie_fields( uid: int | str = 0, *, keep_guid: bool = True, + qimei_feature: Mapping[str, object] | None = None, ) -> str: """补齐网页 Cookie 中缺失的字段。 @@ -116,6 +278,7 @@ def fill_web_cookie_fields( guid: wsLaunch 响应返回的会话 guid(32hex);缺核心字段时必填。 uid: 虎牙 uid,用于 __yaoldyyuid 等字段。 keep_guid: 已存在 guid 且非空时是否保留(默认 True)。 + qimei_feature: QIMEI SDK ``getFeature()`` 结果;未提供时使用稳定默认档案。 Returns: 补齐后的 Cookie 字符串(原始字段值不变,仅补缺失项)。 @@ -152,9 +315,8 @@ def fill_web_cookie_fields( add("game_did", _game_did_generate()) # 35 位随机(真实长度) # qimei 皮肤字段 - # _qimei_fingerprint: QIMEI SDK SpookyHashV2(canvas指纹,31) 32hex;设备稳定, - # 无真实 canvas 时可随机(格式 32hex 对齐),真实算法见 trace 文档 - add("_qimei_fingerprint", secrets.token_hex(16)) + # _qimei_fingerprint: QIMEI SDK x64hash128(浏览器能力特征,31) 32hex。 + add("_qimei_fingerprint", qimei_fingerprint_generate(qimei_feature)) add("_qimei_h38", secrets.token_hex(19)) # 会话/统计字段 @@ -209,4 +371,6 @@ __all__ = [ "cookie_fields", "fill_web_cookie_fields", "missing_web_cookie_fields", + "qimei_feature_from_device_profile", + "qimei_fingerprint_generate", ] diff --git a/docs/HUYA_APP_OVERVIEW.md b/docs/HUYA_APP_OVERVIEW.md index bbf6f86..3de40bb 100644 --- a/docs/HUYA_APP_OVERVIEW.md +++ b/docs/HUYA_APP_OVERVIEW.md @@ -39,7 +39,7 @@ Web UI 通过 `web/backend/routers/huya.py` 调用它。 │ ④ 本地铸证:nonce_forge(uid+k1) → cert_forge 铸证书 → envelope_forge │ │ 补丁 WUP 信封(证书 + UID) │ │ ⑤ 模拟扫码绑定四步流 QrRole(getQrId→scan→bind→tryQrLogin)→ biztoken│ -│ ⑥ POST /web/cookie/verify 兑换 → 全套网页 Cookie │ +│ ⑥ POST /web/cookie/verify → 认证 Cookie;本地补齐网页运行字段 │ └──────────────────────────────────────────────────────────────────────┘ ``` diff --git a/docs/HUYA_CHAT_缺失字段补齐方案.md b/docs/HUYA_CHAT_缺失字段补齐方案.md index bc04870..6ef0c69 100644 --- a/docs/HUYA_CHAT_缺失字段补齐方案.md +++ b/docs/HUYA_CHAT_缺失字段补齐方案.md @@ -1,4 +1,6 @@ -# 虎牙网页 Cookie 缺失字段补齐方案(2026-09-01 晚) +# 虎牙网页 Cookie 缺失字段补齐方案(历史记录,2026-09-01 晚) + +> 当前字段状态以 [HUYA_COOKIE路线状态.md](HUYA_COOKIE路线状态.md) 为准;本文保留当晚的抓包与联调过程。 > 目标:账号 19507362959(DB id=58,yyuid=1199664135026)跑通精英宝典 WSS > query_game_name(checkUserBindGameAccount)/ query_points(getUserScore)。 @@ -19,10 +21,10 @@ | `udb_anobiztoken` | anonymousLogin 响应 biztoken | ✅ 已接入 anon_device | | `udb_anouid` | anonymousLogin 响应 uid | ✅ 已接入 anon_device | | `udb_guiddata` | middle URL 第三段 hex32(客户端自选) | ✅ 已接入 anon_device | - | `guid` | `HyUDBWebSDK-Exchange-2.4.js` 写 document.cookie | ❓ 脚本语义待复刻 | - | `_qimei_uuid42` | QIMEI SDK 脚本写 document.cookie | ❓ 脚本语义待复刻 | - | `__yamid_new` | 门户业务脚本写 document.cookie | ❓ 脚本语义待复刻 | - | `game_did` | 门户业务脚本写 document.cookie | ❓ 脚本语义待复刻 | + | `guid` | `HyUDBWebSDK-Exchange-2.4.js` 写 document.cookie | 会话值已接入;回退生成规则待确认 | + | `_qimei_uuid42` | QIMEI SDK 脚本写 document.cookie | 结构已对齐;SDK 语义待复刻 | + | `__yamid_new` | 门户业务脚本写 document.cookie | `room_normal.js` `generate32` 已复刻 | + | `game_did` | 门户业务脚本写 document.cookie | 35 位、63 字符集规则已复刻 | ## 二、账号 58 现状(16 字段) @@ -37,10 +39,10 @@ udb_appid, udb_guiddata` —— 4 个匿名设备字段已有(anon_device 协 | 字段 | 9.1 值形态 | 浏览器首次出现 | 补齐方案 | | --- | --- | --- | --- | -| `guid` | 32 hex,`0a7d…` 前缀 | udbres.huya.com SDK 脚本 | 待 SDK 逆向;**格式** 32hex | -| `game_did` | 35 位字母数字 | liveapi.ip/getIpLocation 后门户脚本 | 待门户脚本逆向;格式 alnum | -| `_qimei_uuid42` | 42 hex,`1a901…` 前缀 | QIMEI SDK 脚本 | 待 SDK 逆向;格式 42hex | -| `__yamid_new` | 32 hex 大写 | 门户脚本 | 待门户脚本逆向;格式 32hex 大写 | +| `guid` | 32 hex,`0a7d…` 前缀 | udbres.huya.com SDK 脚本 | `wsLaunch` 会话值已接入;回退规则待确认 | +| `game_did` | 35 位字母数字 | liveapi.ip/getIpLocation 后门户脚本 | 35 位、63 字符集已复刻 | +| `_qimei_uuid42` | 42 hex,`1a901…` 前缀 | QIMEI SDK 脚本 | `1a` + 40hex;精确算法待复刻 | +| `__yamid_new` | 32 hex 大写 | 门户脚本 | `generate32` 已复刻并通过 WSS 验证 | 浏览器 round1 同一字段多次出现均**值稳定**(跨 profile 稳定):`_qimei_fingerprint` 恒为 `2822a43b5a3bd016fc844218c35c8a74`,`_qimei_uuid42` 前缀恒为 `1a901…`, @@ -106,6 +108,8 @@ cd /Users/yml/codes/live-hub-py ## 六、风险与验证要求 +当前实现已经通过活动 WSS 查询链路验证。`_qimei_uuid42` 仍只保证结构,`guid` 的回退值和设备字段持久化仍需继续完善;详细状态见 [HUYA_COOKIE路线状态.md](HUYA_COOKIE路线状态.md)。 + ## 七、各通道 Cookie 依赖矩阵(2026-09-01 晚实测) 不同通道对网页设备态字段的依赖不同,补齐策略按通道区别: diff --git a/docs/HUYA_COOKIE合并审计与来源矩阵.md b/docs/HUYA_COOKIE合并审计与来源矩阵.md index 14c6e8e..64c82b8 100644 --- a/docs/HUYA_COOKIE合并审计与来源矩阵.md +++ b/docs/HUYA_COOKIE合并审计与来源矩阵.md @@ -1,6 +1,8 @@ # 虎牙网页 Cookie 合并审计与来源矩阵 -更新时间:2026-09-01 +更新时间:2026-09-02 + +> 当前实现总览见 [HUYA_COOKIE路线状态.md](HUYA_COOKIE路线状态.md)。本文记录字段来源约束和历史审计证据。 ## 结论 @@ -20,7 +22,7 @@ | `sdid`、`hdid` | `df/collect` 的 `Set-Cookie`/响应 | 本次指纹响应覆盖旧值 | | `guid` | 网页/launch 会话的 GUID32 | 只接受显式 `device_info["guid"]` 或 Session 值 | | `udb_guiddata` | 浏览器设备状态字段,和 `guid` 是不同值 | 只接受显式值,不再与 `guid32` 复制 | -| `game_did`、`__yamid_*`、QIMEI、`udb_anobiztoken` | 浏览器/设备 SDK 状态,部分具有关联或签名 | 无真实值时省略 | +| `game_did`、`__yamid_*`、QIMEI、`udb_anobiztoken` | 浏览器/设备 SDK 状态,部分具有关联或签名 | 有真实值优先;缺失时按已验证算法或结构补齐,并保留证据边界 | | `udb_deviceid` | 浏览器设备 Cookie | 只接受格式正确且显式提供的值 | | `Hm_*`、`HMACCOUNT`、`*_rep_cnt`、`_yasids` | 页面统计与访问计数状态 | 只保留已有或显式提供值 | @@ -28,7 +30,7 @@ 同一网页会话内这些值保持稳定: -- `_qimei_uuid42`:42 位;`_qimei_h38`:38 位;`_qimei_fingerprint`:32 位。三者是 QIMEI 设备标识,不能用随机十六进制替代。 +- `_qimei_uuid42`:42 位;`_qimei_h38`:38 位;`_qimei_fingerprint`:32 位。三者是 QIMEI 设备标识;其中 fingerprint 已按 SDK 能力特征串算法复刻,uuid42 仍只保证结构。 - `udb_anobiztoken`:344 位 URL-safe 字符串,和 `udb_anouid` 共同属于匿名设备状态,不能用重复 UUID 字节拼接。 - `guid` 与 `udb_guiddata` 均为 32 位,但抓包值不同,不能复用同一画像字段。 - `udb_deviceid` 为 `w_` 加 19 位数字;格式可以校验,但仅凭格式无法得到真实值。 @@ -38,7 +40,7 @@ ## 旧实现的问题 -旧实现曾在空 Session 中生成 `__yamid_new`、`game_did`、三个 QIMEI 字段、`udb_anobiztoken`、`HMACCOUNT`、`_yasids` 和多个固定计数器。它们只能满足长度测试,不能证明值具有协议语义。 +旧实现曾在空 Session 中生成 `__yamid_new`、`game_did`、三个 QIMEI 字段、`udb_anobiztoken`、`HMACCOUNT`、`_yasids` 和多个固定计数器。它们只能满足长度测试,不能证明值具有协议语义;当前 fingerprint 已替换为 SDK 算法,uuid42 仍保留结构化回退。 旧实现还把 `device_info["guid32"][:32]` 同时写入 `guid` 和 `udb_guiddata`,并把手机号直接写入 `udb_passport`/`username`。这两点会造成设备身份和账号映射偏差。 diff --git a/docs/HUYA_COOKIE路线状态.md b/docs/HUYA_COOKIE路线状态.md new file mode 100644 index 0000000..8890080 --- /dev/null +++ b/docs/HUYA_COOKIE路线状态.md @@ -0,0 +1,60 @@ +# 虎牙 Cookie 路线状态 + +更新时间:2026-09-02 + +## 当前结论 + +App 登录可以得到新鲜的 `udb_cred`、`udb_uid`、`yyuid`、`udb_biztoken`、`sdid` 和 `hdid`。匿名设备协议可以补齐 `udb_deviceid`、`udb_guiddata`、`udb_anobiztoken`、`udb_anouid`。活动 WSS 会话在缺少网页设备字段时先执行 `wsLaunch`,拿到会话 `guid` 后补齐 Cookie 并重建连接。 + +活动 WSS 的角色、积分、商品、详情和兑换记录查询已完成端到端验证;商城 WSS 和 HTTP RPC 的查询链路也已验证。真实兑换曾验证过一次,后续自动验证保持只读。 + +## 字段状态 + +| 字段 | 当前状态 | 实现或证据 | +| --- | --- | --- | +| `udb_cred`、`udb_uid`、`yyuid`、`udb_biztoken` | 已完成 | App 登录与 QR 绑定链 | +| `sdid`、`hdid` | 已完成 | hydevice / `df/collect` | +| `udb_deviceid`、`udb_guiddata`、`udb_anobiztoken`、`udb_anouid` | 已完成 | `anon_device.py` | +| `guid` | 会话路径完成,回退算法未完全确认 | `wsLaunch` 返回值优先;回退值为结构仿造 | +| `__yamid_new` | 已完成 | `room_normal.js` `generate32` Python 复刻 | +| `game_did` | 已完成 | 35 位、63 字符集规则复刻 | +| `_yasids` | 格式完成,精确共享种子未确认 | 与 `__yamid_new` 使用同类时钟生成器 | +| `_qimei_uuid42` | 结构完成,SDK 内部算法待复刻 | `1a` + 40hex | +| `_qimei_fingerprint` | 算法已复刻 | QIMEI `x64hash128(feature_string, 31)` | +| 统计字段 | 已完成 | 线上查询已接受;部分字段仍是统计用途 | + +## `_qimei_fingerprint` 算法 + +QIMEI 0.6.3 的 Canvas `id` 和 `geometry` 是 `getFeature()` 的两个字段,但 `_qimei_fingerprint` 使用的是另一条浏览器能力特征串: + +```text +screen.resolution###screen.availResolution### +navigator.deviceMemory###navigator.pdfViewerEnabled### +navigator.cookieEnabled###navigator.productSub### +navigator.hardwareConcurrency###webgl###timezoneOffset### +``` + +该字符串经过 SDK 内置的 `x64hash128`,种子为 `31`。Python 实现位于 `core/huya/web_cookie_fields.py`,测试向量与 SDK 输出一致。登录流程现在优先将 Android 设备画像映射为 WebView 近似特征;没有画像时再使用稳定的默认 Web 特征档案。调用方可以传入 SDK `getFeature()` 结果获得对应环境的精确值。 + +复核向量: + +```text +输入:800x600###800x600###16###true###true###20030107###10###Google Inc. (Apple)~ANGLE (Apple, ANGLE Metal Renderer: Apple M4, Unspecified Version)###-480### +SDK/Python 输出:d07a45de567322edcd42312f38e610be +``` + +## 尚未闭合的工程项 + +1. 将真实 WebView/Chrome 的 `getFeature()` 结果接入登录调用,替代当前的设备画像近似值。 +2. 对 `_qimei_uuid42` 做 SDK VM 级复刻,当前只保证结构。 +3. 明确 `guid` 的 `956a` / `966a` 版本差异和无 `wsLaunch` 时的真实生成规则。 +4. 让活动会话补齐后的 Cookie 持久化回账号,避免原始 16 字段账号每次任务重新生成设备字段。 +5. 为 `raw_cookie` 增加重建网页 Cookie 的服务入口。 +6. 商城下单、支付和支付状态轮询保持单独人工验证,不纳入只读回归。 + +## 证据入口 + +- [网页字段溯源](HUYA_WEB_FIELD_TRACE.md) +- [Cookie 合并审计](HUYA_COOKIE合并审计与来源矩阵.md) +- [浏览器抓包报告](../evidence/browser_cookie_harvest/report.md) +- [虎牙精英宝典联调指南](HUYA精英宝典联调指南.md) diff --git a/docs/HUYA_WEB_FIELD_TRACE.md b/docs/HUYA_WEB_FIELD_TRACE.md index 622683b..f26c5b0 100644 --- a/docs/HUYA_WEB_FIELD_TRACE.md +++ b/docs/HUYA_WEB_FIELD_TRACE.md @@ -1,4 +1,6 @@ -# 虎牙网页设备字段溯源(2026-09-01 浏览器实测实证) +# 虎牙网页设备字段溯源(历史证据,2026-09-01 浏览器实测) + +> 当前实现状态以 [HUYA_COOKIE路线状态.md](HUYA_COOKIE路线状态.md) 为准;本文保留原始浏览器证据与算法推导。 ## 一、来源判定方法(有头 Chrome + 打点探针) @@ -64,28 +66,28 @@ CBCC 06ECB5 0000 0001 B63016D6162BC990 ← reqable 8.31 ``` 首字节 `1a` 恒定;前 32 hex 符合 RFC 4122 UUID 布局(8-4-4-4-12)。 -### _qimei_fingerprint = SpookyHashV2(canvas指纹, 31) 32hex +### _qimei_fingerprint = x64hash128(feature_string, 31) 32hex qimeisdk-web 0.6.3 源码已定位: - 画 "qimei" 文本(Times New Roman 11pt)+ `#f60` 背景 + 透明度矩形 - 三圆叠加(`#f2f`/`#2ff`/`#ff2` 40 半径)+ 环形 `#f9c` evenodd -- `canvas.toDataURL()` → **SpookyHashV2**(r 函数,64 位,seed=31)→ 32hex +- Canvas `id`/`geometry` 是 SDK 特征对象的独立字段;Cookie 指纹实际使用浏览器能力特征串 + → SDK 内置 **x64hash128**(seed=31)→ 32hex。 ## 四、溯源结论与建议 | 字段 | 来源 | 风控关联性 | 建议 | | --- | --- | --- | --- | | `guid` | 浏览器脚本(有固定段 `0a..966a`)| 高(设备与会话绑定) | **必须按结构仿造或复用 wsLaunch 会话值**;随机纯 hex 与真实结构不符 | -| `__yamid_new` | 浏览器脚本(CBCC 前缀+种子)| 高(与 rootsid 同源)| 需要前缀 `CBCC` + 种子同步生成逻辑 | +| `__yamid_new` | 浏览器脚本(CBCC 前缀+种子)| 高(与 rootsid 同源)| `room_normal.js` `generate32` 已复刻;精确 rootsid 共享计数仍待确认 | | `_qimei_uuid42` | QIMEI SDK 0.6.3(`1a` 前缀)| 中 | 前缀 `1a` + UUID 布局 | -| `_qimei_fingerprint` | QIMEI SDK 0.6.3 SpookyHashV2 | 中 | **算法已完全逆向**(源码在手),可精确复现 | +| `_qimei_fingerprint` | QIMEI SDK 0.6.3 x64hash128(feature_string, 31) | 中 | Python 已复刻,输入字段见路线状态文档 | | `game_did` | 浏览器脚本 | 中 | 长度需对齐(35 变体含 `-`)| | 统计字段(SoundValue 等)| 页面脚本 | 低 | 随机 OK | -**下一步选项**: -1. `_qimei_fingerprint` 直接用 SpookyHashV2 实现(canvas 指纹用标准值替代) -2. `guid` 采样更多样本确认固定段后按 `0a` + 结构仿造 -3. `__yamid_new` 需找到生成它的具体脚本(非 document.cookie 写入,可能在 - ubtk/埋点脚本里)做种子同步 +**当时的下一步记录(部分已完成,保留作历史)**: +1. `_qimei_fingerprint` 已改为浏览器能力特征串的 `x64hash128`,见路线状态文档。 +2. `guid` 已优先使用 `wsLaunch` 会话值,回退固定段仍待确认。 +3. `__yamid_new` 已定位 `room_normal.js` `generate32` 并完成 Python 复刻。 ## 六、核心字段生成算法逆向结果(2026-09-01 晚,浏览器实测 + 源码定位) ### ✅ __yamid_new = room_normal.js generate32(UUID-v1 风格,已 Python 复刻验证) @@ -119,12 +121,12 @@ B = (t=35) => { const e="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ012 - 由 qimeisdk-web 0.6.3 内部 VM 生成(`_qimei_ssid`/`_qimei_last_get_id_time_<16hex>` 同源) - 当前实现对齐结构(1a 前缀 + 42hex 随机化),精确算法待继续 -### ⚠️ _qimei_fingerprint = SpookyHashV2(canvas指纹,31),待 canvas 复现 -- SDK 0.6.3 内置 x64hash128 与 canvas 指纹逻辑(文本+三圆+环形) -- 设备稳定(同 profile 跨会话不变);当前实现随机 32hex 格式对齐 -- 复现需在真实环境执行 canvas(headless 下 canvas 指纹固定,可抽取一次固化为常量) +### ✅ _qimei_fingerprint = x64hash128(feature_string,31),已完成 Python 复刻 +- Cookie 输入由屏幕分辨率、浏览器能力、WebGL 和时区字段按 `###` 拼接。 +- Python 实现位于 `core/huya/web_cookie_fields.py`,测试向量与 SDK 输出一致。 +- 没有实时浏览器能力对象时,生成器使用稳定的默认 Web 特征档案。 -### 已修正生成器(core/huya/web_cookie_fields.py, commit 待定) +### 已修正生成器(core/huya/web_cookie_fields.py) - `__yamid_new` → `_yamid_new_generate32()`(UUID-v1 时间戳复刻) - `game_did` → `_game_did_generate()`(35 位 63 字符集) - `_yasids` → 与 `__yamid_new` 同源同刻生成 diff --git a/docs/HUYA精英宝典联调指南.md b/docs/HUYA精英宝典联调指南.md index 64ee88f..5e85495 100644 --- a/docs/HUYA精英宝典联调指南.md +++ b/docs/HUYA精英宝典联调指南.md @@ -1,6 +1,8 @@ # 虎牙精英宝典联调指南 -更新时间:2026-09-01 +更新时间:2026-09-02 + +> 当前 Cookie 字段状态见 [HUYA_COOKIE路线状态.md](HUYA_COOKIE路线状态.md)。 ## 本次联调结论 @@ -11,12 +13,12 @@ | WUP 登录 | 通过 | `cred=114B`,返回真实 UID | | hydevice 指纹 | 通过 | 返回 `sdid`、`hdid` | | QR 绑定 | 通过 | `getQrId`、`bindQrLoginUser`、`tryQrLogin(stage=2)` | -| 网页 Cookie | 待重新登录验证 | 必须包含 9.1 网页设备态字段 | -| 精英宝典任务 | 待验证 | 仅允许 WSS,会话失败即任务失败 | +| 网页 Cookie | 已验证 | App 登录补齐网页字段;活动会话优先复用 `wsLaunch` guid | +| 精英宝典任务 | 已验证 | 活动 WSS 查询、商品、详情和兑换记录链路已跑通 | 旧登录结果中的 Cookie 缺少网页设备态,已不再作为成功登录或任务链路使用。 -纯 App/HTTP 登录不会由 `web/cookie/verify` 下发网页设备 Cookie。浏览器只用于 +纯 App/HTTP 登录不会由 `web/cookie/verify` 下发完整网页设备 Cookie。浏览器只用于 人工调试和抓取来源,未集成到脚本;登录链已接入匿名设备态协议 (`core/huya/anon_device.py`)补 `udb_deviceid`/`udb_guiddata`/`udb_anobiztoken`/ `udb_anouid`,剩余脚本字段(`guid`/`game_did`/`_qimei_uuid42`/`__yamid_new`) @@ -50,8 +52,9 @@ Cookie 缺少网页设备态(udb_deviceid, udb_guiddata, udb_anobiztoken),已停止活动 HTTP 回退 ``` -`guid`/`game_did`/`_qimei_uuid42`/`__yamid_new` 四个脚本字段无服务端签发接口, -登录时缺失只警告不阻断保存,WSS 判定也不再要求(风控风险自担)。 +`guid`/`game_did`/`_qimei_uuid42`/`__yamid_new` 四个脚本字段无统一服务端签发接口; +当前登录链已接入 `guid` 会话值、`__yamid_new`、`game_did` 和 fingerprint 算法, +`_qimei_uuid42` 仍使用结构化回退,WSS 判定不要求脚本字段全部来自浏览器。 - WSS 握手、`wsLaunch`、`getConfig` 或业务 RPC 任一步失败:任务直接失败并记录具体阶段,便于修复 Cookie 或协议参数。 diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..71162af --- /dev/null +++ b/docs/README.md @@ -0,0 +1,58 @@ +# 项目文档索引 + +更新时间:2026-09-02 + +## 当前入口 + +- [虎牙 Cookie 路线状态](HUYA_COOKIE路线状态.md):当前实现、字段状态、验证边界和后续工作。 +- [虎牙网页设备字段溯源](HUYA_WEB_FIELD_TRACE.md):浏览器证据、字段来源和生成算法。 +- [虎牙 Cookie 合并审计与来源矩阵](HUYA_COOKIE合并审计与来源矩阵.md):字段来源约束和合并策略。 +- [虎牙精英宝典联调指南](HUYA精英宝典联调指南.md):工作台联调顺序和只读验证方法。 + +## 虎牙业务与协议 + +- [虎牙精英宝典 9.1 全流程分析](HUYA_精英宝典-9.1-全流程分析.md) +- [虎牙精英宝典后端与抓包对照](HUYA_精英宝典-后端与9.1抓包对照.md) +- [虎牙精英宝典 8.31 Python 重分析](HUYA_精英宝典-8.31-PY重新分析.md) +- [虎牙精英宝典联调指南](HUYA精英宝典联调指南.md) +- [网页 Cookie 缺失字段历史方案](HUYA_CHAT_缺失字段补齐方案.md) +- [虎牙 App 登录总览](HUYA_APP_OVERVIEW.md) +- [虎牙纯 Python 自动登录总结](虎牙纯Python全自动登录-总结与交接.md) + +## 虎牙设备与加密逆向 + +- [HDID 算法生成](HUYA_HDID_ALGORITHM_GEN.md) +- [HDID 研究记录](HUYA_HDID_RESEARCH.md) +- [标识符命名规范](HUYA_ID_NOMENCLATURE.md) +- [铸币机终局总结](HUYA_铸币机-终局总结.md) +- [App 登录协议抓包实证](虎牙App密码登录协议-抓包实证.md) +- [App 登录 WUP 明文抓包](虎牙App密码登录-WUP明文抓包.md) +- [App 登录加密算法动态验证](虎牙App登录-加密算法动态验证.md) +- [App 登录 APK 静态分析](虎牙App登录-APK静态分析.md) +- [App 登录逆向笔记](虎牙App登录逆向笔记.md) +- [扫码登录闭环逆向](虎牙扫码登录闭环逆向.md) +- [hyCred 签发侦察笔记](虎牙hyCred签发-侦察笔记.md) + +## 设备注册与运行工具 + +- [dfpReport 生成流程](dfpReport生成流程.md) +- [dfpReport 破解进度](dfpReport破解进度.md) +- [Frida 绕过配方](Frida绕过配方.md) +- [Hook 脚本运行与捕获约定](Hook脚本运行与捕获约定.md) +- [模拟器存活闪退诊断](模拟器存活闪退诊断报告.md) +- [登录系统全流程定案](登录系统全流程定案.md) + +## 鱼翅充值接口 + +- [对接信息](鱼翅充值api/对接信息.md) +- [账户信息查询](鱼翅充值api/账户接口/账户信息查询.md) +- [创建订单](鱼翅充值api/订单接口/1.创建订单_必要.md) +- [查询订单](鱼翅充值api/订单接口/2.查询订单_必要.md) +- [订单异步通知](鱼翅充值api/订单接口/3.订单异步通知_推荐.md) + +## 文档约定 + +1. 带日期的抓包和逆向笔记属于历史证据,结论以对应日期为准。 +2. 当前代码行为和未完成项以“虎牙 Cookie 路线状态”为准。 +3. Cookie、UID、订单号和设备标识进入文档时使用脱敏值;真实数据只保存在本地 evidence 目录。 +4. 兑换、下单和支付属于写操作,除非明确确认,不纳入自动回归测试。 diff --git a/docs/虎牙纯Python全自动登录-总结与交接.md b/docs/虎牙纯Python全自动登录-总结与交接.md index a77c73d..1d314ba 100644 --- a/docs/虎牙纯Python全自动登录-总结与交接.md +++ b/docs/虎牙纯Python全自动登录-总结与交接.md @@ -15,13 +15,13 @@ → 信封补丁: 换cert + (uid不同则patch t3) [envelope_forge] → 扫码绑定流 getQrId→scanQrPicNotify→bindQrLoginUser→tryQrLogin → POST /web/cookie/verify {"appId":5002} ← 关键最后一步! - → 全套网页Cookie: udb_cred/yyuid/udb_uid/udb_passport/username/udb_biztoken... + → 认证 Cookie + 网页运行字段: udb_cred/yyuid/udb_uid/udb_passport/username/udb_biztoken... ``` ### 一键工具 | 命令 | 功能 | |---|---| -| `.venv/bin/python tools/full_web_cookie.py <账号> <密码>` | 密码→全套网页Cookie(任意账号, 纯协议)| +| `.venv/bin/python tools/full_web_cookie.py <账号> <密码>` | 密码→认证 Cookie 与网页运行字段(任意账号, 纯协议)| | `.venv/bin/python tools/full_auto_safe.py <账号> <密码>` | 同上, 且新账号safe_auth滑块自动过 | | `.venv/bin/python tools/full_auto_test.py <账号> <密码>` | 密码→cred→铸证→bind 全链路自测 | | `.venv/bin/python tools/nonce_forge.py` | nonce生成器自检(对设备实采MATCH) | diff --git a/tests/test_huya_web_cookie_fields.py b/tests/test_huya_web_cookie_fields.py new file mode 100644 index 0000000..76eda10 --- /dev/null +++ b/tests/test_huya_web_cookie_fields.py @@ -0,0 +1,70 @@ +"""虎牙网页 Cookie 字段生成器测试。""" + +from core.huya.web_cookie_fields import ( + _qimei_feature_string, + _x64hash128, + cookie_fields, + fill_web_cookie_fields, + missing_web_cookie_fields, + qimei_feature_from_device_profile, + qimei_fingerprint_generate, +) + + +def test_qimei_hash_matches_sdk_vectors(): + assert _x64hash128("", 31) == "24700f9f1986800ab4fcc880530dd0ed" + assert _x64hash128("a", 31) == "f8b09930c1df727ef75677a1d6ae5bd6" + assert _x64hash128("abc", 31) == "31844fd43df93041e52bcddd13312671" + + +def test_qimei_fingerprint_uses_sdk_feature_string(): + feature = { + "screen": {"resolution": "800x600", "availResolution": "800x600"}, + "navigator": { + "deviceMemory": 16, + "pdfViewerEnabled": True, + "cookieEnabled": True, + "productSub": "20030107", + "hardwareConcurrency": 10, + }, + "webgl": "Google Inc. (Apple)~ANGLE (Apple, ANGLE Metal Renderer: Apple M4, Unspecified Version)", + "timezoneOffset": -480, + } + assert _qimei_feature_string(feature).endswith("###-480###") + assert qimei_fingerprint_generate(feature) == "d07a45de567322edcd42312f38e610be" + fields = cookie_fields(fill_web_cookie_fields("", "", qimei_feature=feature)) + assert fields["_qimei_fingerprint"] == "d07a45de567322edcd42312f38e610be" + + +def test_fill_replaces_empty_values_and_completes_core_fields(): + result = fill_web_cookie_fields("guid=; __yamid_new=; game_did=", "a" * 32, 1) + fields = cookie_fields(result) + assert fields["guid"] == "a" * 32 + assert fields["__yamid_new"] + assert fields["game_did"] + assert fields["_qimei_fingerprint"] == qimei_fingerprint_generate() + assert not missing_web_cookie_fields(result) + + +def test_device_profile_builds_stable_webview_feature_approximation(): + feature = qimei_feature_from_device_profile({ + "vendor": "vivo", + "model": "V2370A", + "width": "1080", + "height": "2412", + }) + assert feature["screen"] == { + "resolution": "1080x2412", + "availResolution": "1080x2412", + } + assert feature["navigator"]["pdfViewerEnabled"] is False + assert "Adreno" in str(feature["webgl"]) + first = qimei_fingerprint_generate(feature) + second = qimei_fingerprint_generate(qimei_feature_from_device_profile({ + "vendor": "vivo", + "model": "V2370A", + "width": "1080", + "height": "2412", + })) + assert first == second + assert first != qimei_fingerprint_generate()