diff --git a/core/huya/web_cookie_fields.py b/core/huya/web_cookie_fields.py index 379448d..1de18c7 100644 --- a/core/huya/web_cookie_fields.py +++ b/core/huya/web_cookie_fields.py @@ -276,8 +276,9 @@ def _qimei_uuid42_generate( ``now_ns`` 保留旧回退函数的纳秒参数约定;SDK 实际只读取本地时间的 年/月/日/时/分/秒,因此毫秒和纳秒不会进入结果。时间前缀的年份和月份 使用 SDK 的十六进制编码,最后三位来自 ``navigator.cookieEnabled``。 - ``random_value`` 仅用于固定测试;生产调用沿用 ``Math.random()`` 等价的 - Python ``random.random()``,并将理论上的 1.0 夹到合法半字节上界 15。 + ``random_value`` 仅用于固定测试;生产调用会像 SDK 一样独立调用 20 次 + ``Math.random()`` 等价的 Python ``random.random()``,并将理论上的 1.0 + 夹到合法半字节上界 15。 """ now_ms = time.time_ns() // 1_000_000 if now_ns is None else now_ns // 1_000_000 current = datetime.fromtimestamp(now_ms / 1000, tz=UTC).astimezone() @@ -292,12 +293,14 @@ def _qimei_uuid42_generate( ) prefix = year_hex + month_hex + date_parts + ("100" if cookie_enabled else "000") - value = random.random() if random_value is None else float(random_value) - nibble = min(15, max(0, int(value * 16))) - repeated = f"{nibble:x}" * 20 - template = prefix + "----" + repeated + "----" + random_source = random.random if random_value is None else lambda: random_value + random_segment = "".join( + f"{min(15, max(0, int(float(random_source()) * 16))):x}" + for _ in range(20) + ) + template = prefix + "----" + random_segment + "----" replacement = f"{zlib.crc32(template.encode('ascii')) & 0xFFFFFFFF:08x}" - return prefix + replacement[:4] + repeated + replacement[4:] + return prefix + replacement[:4] + random_segment + replacement[4:] def fill_web_cookie_fields( diff --git a/docs/HUYA_COOKIE路线状态.md b/docs/HUYA_COOKIE路线状态.md index c8a6a3f..aad46aa 100644 --- a/docs/HUYA_COOKIE路线状态.md +++ b/docs/HUYA_COOKIE路线状态.md @@ -68,6 +68,7 @@ random=0.25 17b0f060d14100037f4444444444444444444437df random=0.5 17b0f060d1410053db888888888888888888883ff6 random=0.75 17b0f060d14100966bcccccccccccccccccccc1cf3 random=.999 17b0f060d141003174ffffffffffffffffffffd85d +sequence=0..f,0..3 17b0f060d14100b2230123456789abcdef012396a9 ``` -每个值都可以拆成 `14 + 4 + 20 + 4 = 42` 位。时间前缀由本地时间组成:年份末两位转十六进制并补两位、月份转十六进制、日/时/分/秒转十六进制并各补两位,最后追加 `cookieEnabled ? 100 : 000`。中间 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`。该实现只用于查询链路的设备态生成,不触发兑换、下单或支付。 +每个值都可以拆成 `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`。该实现只用于查询链路的设备态生成,不触发兑换、下单或支付。 diff --git a/docs/HUYA_WEB_FIELD_TRACE.md b/docs/HUYA_WEB_FIELD_TRACE.md index ef9ad4c..c621fdd 100644 --- a/docs/HUYA_WEB_FIELD_TRACE.md +++ b/docs/HUYA_WEB_FIELD_TRACE.md @@ -64,7 +64,7 @@ CBCC 06ECB5 0000 0001 B63016D6162BC990 ← reqable 8.31 17b0f060d1410053db888888888888888888883ff6 ← 固定时间/随机 .5 17b0f060d141003174ffffffffffffffffffffd85d ← 固定时间/随机 .999 ``` -时间前缀的编码为:年份末两位十六进制(补两位)+ 月份十六进制 + 日/时/分/秒各两位十六进制 + `cookieEnabled` 标志(`100`/`000`)。模板 `prefix + "----" + nibble * 20 + "----"` 的标准 CRC32 小写结果会拆成前后四位填回两个占位;`nibble = floor(Math.random() * 16)`,因此合法范围为 `0` 到 `f`。 +时间前缀的编码为:年份末两位十六进制(补两位)+ 月份十六进制 + 日/时/分/秒各两位十六进制 + `cookieEnabled` 标志(`100`/`000`)。模板 `prefix + "----" + random_nibbles + "----"` 的标准 CRC32 小写结果会拆成前后四位填回两个占位;`random_nibbles` 由 20 次独立的 `floor(Math.random() * 16)` 依次拼接,因此每位合法范围为 `0` 到 `f`。 ### _qimei_fingerprint = x64hash128(feature_string, 31) 32hex qimeisdk-web 0.6.3 源码已定位: @@ -121,10 +121,10 @@ B = (t=35) => { const e="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ012 清理 localStorage/cookie 后对 QIMEI SDK 0.6.3 做固定时间、固定随机值追踪,定位到 VM 生成器 `e=6064`。它先拼出: ```text -14 位时间前缀 + "----" + 20 位随机半字节重复串 + "----" +14 位时间前缀 + "----" + 20 次独立抽样组成的随机半字节串 + "----" ``` -随后计算模板 ASCII 字节的标准 CRC32,将 8 位小写结果的前 4 位和后 4 位分别填入占位,最后调用 `toLocaleLowerCase()`。例如 `random=0.25` 的模板是 `17b0f060d14100----44444444444444444444----`,替换值是 `037f37df`,结果为 `17b0f060d14100037f4444444444444444444437df`。`navigator.cookieEnabled` 只影响时间前缀末三位(`100`/`000`)。 +随后计算模板 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`。 diff --git a/tests/test_huya_web_cookie_fields.py b/tests/test_huya_web_cookie_fields.py index 5b07621..c952adb 100644 --- a/tests/test_huya_web_cookie_fields.py +++ b/tests/test_huya_web_cookie_fields.py @@ -61,6 +61,22 @@ def test_qimei_uuid42_matches_sdk_vectors_and_random_upper_bound(): assert all(ch in "0123456789abcdef" for ch in upper) +def test_qimei_uuid42_uses_twenty_independent_random_draws(monkeypatch): + values = iter(value / 16 for value in (*range(16), *range(4))) + calls = 0 + + def next_random(): + nonlocal calls + calls += 1 + return next(values) + + monkeypatch.setattr("core.huya.web_cookie_fields.random.random", next_random) + assert _qimei_uuid42_generate(1700000000000000000) == ( + "17b0f060d14100b2230123456789abcdef012396a9" + ) + assert calls == 20 + + def test_qimei_uuid42_encodes_local_date_and_cookie_flag(): base = 1700000000000000000 assert _qimei_uuid42_generate(base, random_value=0, cookie_enabled=False) == ( @@ -75,7 +91,7 @@ def test_qimei_uuid42_handles_year_zero_padding(): def test_sdk_uuid42_trace_vectors_have_confirmed_outer_layout(): - """锁定 SDK 追踪确认的 14+4+20+4 外层布局,不约束未闭合的替换算法。""" + """锁定 SDK 追踪确认的 14+4+20+4 外层布局和固定随机向量。""" vectors = { 0.0: "17b0f060d1410085cc000000000000000000003207", 0.25: "17b0f060d14100037f4444444444444444444437df",