diff --git a/core/huya/web_cookie_fields.py b/core/huya/web_cookie_fields.py index 1de18c7..6c375ca 100644 --- a/core/huya/web_cookie_fields.py +++ b/core/huya/web_cookie_fields.py @@ -357,7 +357,9 @@ def fill_web_cookie_fields( # qimei 皮肤字段 # _qimei_fingerprint: QIMEI SDK x64hash128(浏览器能力特征,31) 32hex。 add("_qimei_fingerprint", qimei_fingerprint_generate(qimei_feature)) - add("_qimei_h38", secrets.token_hex(19)) + # SDK 内部 h38 就是 42 位 UUID42,并不存在独立的 38 位生成器。当前活动 + # getUserScore 只检查同名 Cookie 键是否存在,不检查值;复用 SDK h38 的真实值。 + add("_qimei_h38", pairs["_qimei_uuid42"]) # 会话/统计字段 yasmid = _rand_float() diff --git a/docs/HUYA_COOKIE合并审计与来源矩阵.md b/docs/HUYA_COOKIE合并审计与来源矩阵.md index ed060bf..b2aab19 100644 --- a/docs/HUYA_COOKIE合并审计与来源矩阵.md +++ b/docs/HUYA_COOKIE合并审计与来源矩阵.md @@ -30,7 +30,7 @@ 同一网页会话内这些值保持稳定: -- `_qimei_uuid42`:42 位;`_qimei_h38`:38 位;`_qimei_fingerprint`:32 位。三者是 QIMEI 设备标识;其中 fingerprint 与 uuid42 均已按 SDK 0.6.3 算法复刻。 +- `_qimei_uuid42`:42 位;`_qimei_fingerprint`:32 位,两者已按 SDK 0.6.3 算法复刻。早期文档曾把 `_qimei_h38` 记为独立 38 位设备标识,后续 VM 追踪确认 SDK 内部 `h38` 就是 42 位 UUID42;当前活动积分链路仅要求 Cookie 中存在 `_qimei_h38` 键。 - `udb_anobiztoken`:344 位 URL-safe 字符串,和 `udb_anouid` 共同属于匿名设备状态,不能用重复 UUID 字节拼接。 - `guid` 与 `udb_guiddata` 均为 32 位,但抓包值不同,不能复用同一画像字段。 - `udb_deviceid` 为 `w_` 加 19 位数字;格式可以校验,但仅凭格式无法得到真实值。 @@ -40,7 +40,7 @@ ## 旧实现的问题 -旧实现曾在空 Session 中生成 `__yamid_new`、`game_did`、三个 QIMEI 字段、`udb_anobiztoken`、`HMACCOUNT`、`_yasids` 和多个固定计数器。它们只能满足长度测试,不能证明值具有协议语义;当前 fingerprint 与 uuid42 均已替换为 SDK 0.6.3 算法,仍需以同一会话的线上结果确认风控关联性。 +旧实现曾在空 Session 中生成 `__yamid_new`、`game_did`、两个 QIMEI 值、`_qimei_h38` 兼容键、`udb_anobiztoken`、`HMACCOUNT`、`_yasids` 和多个固定计数器。它们只能满足长度测试,不能证明值具有协议语义;当前 fingerprint 与 uuid42 均已替换为 SDK 0.6.3 算法,`_qimei_h38` 也已改为复用 SDK 内部 `h38` 的 UUID42 值。 旧实现还把 `device_info["guid32"][:32]` 同时写入 `guid` 和 `udb_guiddata`,并把手机号直接写入 `udb_passport`/`username`。这两点会造成设备身份和账号映射偏差。 @@ -52,7 +52,7 @@ ### 9.1 抓包新增证据 `705-meta.json` 的 `GET www.huya.com/30596253` 在请求发出时已经携带 43 个网页 Cookie, -包括 `guid`、`game_did`、三个 QIMEI、`udb_guiddata`、`udb_deviceid`、`udb_anobiztoken` +包括 `guid`、`game_did`、QIMEI 相关字段、`udb_guiddata`、`udb_deviceid`、`udb_anobiztoken` 和统计字段;该响应没有 `Set-Cookie`。随后 `714-meta.json` 的 `POST df.huya.com/web/df/collect` 只返回 `sdid`、`hdid` 两个 `Set-Cookie`。 diff --git a/docs/HUYA_COOKIE路线状态.md b/docs/HUYA_COOKIE路线状态.md index aad46aa..69e5423 100644 --- a/docs/HUYA_COOKIE路线状态.md +++ b/docs/HUYA_COOKIE路线状态.md @@ -21,6 +21,7 @@ App 登录可以得到新鲜的 `udb_cred`、`udb_uid`、`yyuid`、`udb_biztoken | `_yasids` | 格式完成,精确共享种子未确认 | 与 `__yamid_new` 使用同类时钟生成器 | | `_qimei_uuid42` | 已完成 | QIMEI SDK 0.6.3 的 14 位时间前缀 + CRC32 替换段 + 20 位随机半字节段 | | `_qimei_fingerprint` | 算法已复刻 | QIMEI `x64hash128(feature_string, 31)` | +| `_qimei_h38` | 兼容门槛已闭合 | SDK 内部 `h38` 就是 42 位 UUID42;活动积分链路只校验 Cookie 键存在,缺失时复用 `_qimei_uuid42` | | 统计字段 | 已完成 | 线上查询已接受;部分字段仍是统计用途 | ## `_qimei_fingerprint` 算法 @@ -72,3 +73,9 @@ sequence=0..f,0..3 17b0f060d14100b2230123456789abcdef012396a9 ``` 每个值都可以拆成 `14 + 4 + 20 + 4 = 42` 位。时间前缀由本地时间组成:年份末两位转十六进制并补两位、月份转十六进制、日/时/分/秒转十六进制并各补两位,最后追加 `cookieEnabled ? 100 : 000`。中间 20 位由 20 次独立的 `floor(Math.random() * 16)` 依次拼接;上面的单值向量因为探针固定了每次随机返回值,所以表现为相同半字节重复。两个 `----` 占位组成模板后,SDK 对模板 ASCII 字节计算标准 CRC32(多项式 `0xEDB88320`),将 8 位小写结果的前后四位分别替换占位,最后调用 `toLocaleLowerCase()`。Python 实现位于 `core/huya/web_cookie_fields.py`,并用上述固定与递增序列向量回归;随机上界按 15 处理,避免理论输入 `1.0` 产生两位半字节。VM 追踪在 `/tmp/qvmtrace17.out`,生成器入口为 `e=6064`,模板拼接和替换发生在 `m≈5015..5137`。该实现只用于查询链路的设备态生成,不触发兑换、下单或支付。 + +## `_qimei_h38` 兼容键(2026-09-02) + +QIMEI SDK 0.6.3 的 `getQimei36(false)` 结果中,内部字段 `h38` 实际为上述 42 位 UUID42。当前无头 Chrome 在活动页不会生成名为 `_qimei_h38` 的 Cookie,现有浏览器采集证据中也没有该键。 + +账号 id=60 的活动 WSS 只读 A/B 表明:完全删除键时,角色查询返回 `200`,积分查询连续三轮均在 10 秒后无响应;保留空值、1 位或 8/37/38/39/42 位十六进制、38 位非十六进制值时,积分均在约 0.3 秒返回 `200`。因此它是当前积分链路的键存在性兼容门槛,不是独立的 38 位协议值;项目在缺失时写入 SDK `h38` 的真实值,即 `_qimei_uuid42`,已有值继续保留。 diff --git a/docs/HUYA_WEB_FIELD_TRACE.md b/docs/HUYA_WEB_FIELD_TRACE.md index c621fdd..8a7f9fe 100644 --- a/docs/HUYA_WEB_FIELD_TRACE.md +++ b/docs/HUYA_WEB_FIELD_TRACE.md @@ -81,6 +81,7 @@ qimeisdk-web 0.6.3 源码已定位: | `__yamid_new` | 浏览器脚本(CBCC 前缀+种子)| 高(与 rootsid 同源)| `room_normal.js` `generate32` 已复刻;精确 rootsid 共享计数仍待确认 | | `_qimei_uuid42` | QIMEI SDK 0.6.3 VM 生成器 `e=6064` | 中 | Python 已按时间编码、CRC32 和随机半字节逐项复刻 | | `_qimei_fingerprint` | QIMEI SDK 0.6.3 x64hash128(feature_string, 31) | 中 | Python 已复刻,输入字段见路线状态文档 | +| `_qimei_h38` | 当前页面不写入同名 Cookie;SDK 内部 `h38` = UUID42 | 低 | 积分链路仅检查键存在,缺失时复用 `_qimei_uuid42` | | `game_did` | 浏览器脚本 | 中 | 长度需对齐(35 变体含 `-`)| | 统计字段(SoundValue 等)| 页面脚本 | 低 | 随机 OK | @@ -127,6 +128,7 @@ B = (t=35) => { const e="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ012 随后计算模板 ASCII 字节的标准 CRC32,将 8 位小写结果的前 4 位和后 4 位分别填入占位,最后调用 `toLocaleLowerCase()`。例如固定每次 `random=0.25` 时,模板是 `17b0f060d14100----44444444444444444444----`,替换值是 `037f37df`,结果为 `17b0f060d14100037f4444444444444444444437df`;递增序列探针则得到随机段 `0123456789abcdef0123`,证明 SDK 会独立抽样 20 次。`navigator.cookieEnabled` 只影响时间前缀末三位(`100`/`000`)。 - 由 qimeisdk-web 0.6.3 内部 VM 生成(`_qimei_ssid`/`_qimei_last_get_id_time_<16hex>` 同源);字段本身为 42 位小写十六进制。 - Python 实现按 SDK 时间、CRC32 和随机上界复刻,固定向量覆盖 `0`、`.25`、`.5`、`.75`、`.999`。 +- SDK 对该内部值使用 `h38` 属性名,但它的长度是 42;`_qimei_h38` 只是活动积分链路的 Cookie 兼容键,不存在另一套 38 位 SDK 生成算法。 ### ✅ _qimei_fingerprint = x64hash128(feature_string,31),已完成 Python 复刻 - Cookie 输入由屏幕分辨率、浏览器能力、WebGL 和时区字段按 `###` 拼接。 diff --git a/tests/test_huya_web_cookie_fields.py b/tests/test_huya_web_cookie_fields.py index c952adb..807a911 100644 --- a/tests/test_huya_web_cookie_fields.py +++ b/tests/test_huya_web_cookie_fields.py @@ -44,9 +44,25 @@ def test_fill_replaces_empty_values_and_completes_core_fields(): assert fields["__yamid_new"] assert fields["game_did"] assert fields["_qimei_fingerprint"] == qimei_fingerprint_generate() + assert fields["_qimei_h38"] == fields["_qimei_uuid42"] assert not missing_web_cookie_fields(result) +def test_qimei_h38_aliases_uuid42_without_overwriting_existing_value(): + uuid42 = "1" * 42 + fields = cookie_fields( + fill_web_cookie_fields(f"_qimei_uuid42={uuid42}; _qimei_h38=", "") + ) + assert fields["_qimei_h38"] == uuid42 + + fields = cookie_fields( + fill_web_cookie_fields( + f"_qimei_uuid42={uuid42}; _qimei_h38=historical", "" + ) + ) + assert fields["_qimei_h38"] == "historical" + + def test_qimei_uuid42_matches_sdk_vectors_and_random_upper_bound(): base = 1700000000000000000 assert _qimei_uuid42_generate(base, random_value=0.25) == (