Files
live-hub-py/core/huya/dfp_register.py
T

292 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""虎牙 App 新设备注册链(零设备 dfpReport 生成 + 注册响应解析)。
每次登录前执行 ``getDfpConfig -> selectOperator -> dfpReport``
* ``dfpReport`` 使用已验证的 4146 字节随机 ``cw``(零设备生成,不重放旧设备报文);
* 服务端返回新的 ``safedeviceid``t2)和 ``device_id``t5)—— 这两个字段
就是 WUP 登录帧的设备字段签发源;
* 请求模板只承载 TAF/WUP 协议形状,旧的 dfpReport 密文不会被使用。
请求按已验证的 TAF/WUP 字段布局动态编码;不依赖抓包证据文件。selectOperator
中的 RSA 字段使用协议固定格式的密文种子,设备画像字段在每次请求时写入。
错误语义: 链上任何一步失败(请求编码、HTTP 异常、响应缺字段)都会抛
``DfpRegistrationError``。调用方(core/huya/app_login)应把注册失败当作明确失败
终止登录;禁止静默回退到画像里的旧固定 safedeviceid/device_id。
⚠ 名称混淆提示(详见 ``docs/HUYA_APP_OVERVIEW.md`` §二 / R15, R39 定案):
- 这里签发的 t2 = 登录帧 safedeviceid、t5 = 登录帧 device_id40hex);
- 登录帧 t1.t0 (32hex appSign) **已破解且与设备无关**: md5("5008_13.4.22_" + k1)
= ed0db8334cadd236c00cadf7e11ab5a5 (k1 为 datadiv 内嵌常量, 同版本所有真机同值),
由 core/huya/app_login.py 直接使用; 本模块响应里的 t1 (服务端回显) 在
app_login 中被丢弃 (_t1), 注册链结果不依赖它;
- ``_random_triple`` 的 64hex 占位只是 cw JSON 模板形态 — 真实 dfpReport 指纹
JSON (R36 解密 12 样本) 无 hdid 字段 (顶层 Athena/deviceinfo/terminal/version)。
"""
from __future__ import annotations
import hashlib
import os
import re
import struct
import time
from collections.abc import Mapping
import requests
from .taf_protocol import TafOutputStream
from .wup_protocol import WupRequest
WSAPI = "https://wsapi.huya.com"
UA = "okhttp/3.14.9"
SELECT_OPERATOR_SDID = (
"PQwemAN9NHkZKoMqVeJmXhIypqMTaQEOrmXr37xQVhQZqrPxjEKEQ11xvE02qawcys/"
"iwl5gSIa6nYz0L5tNl0VdpZFyNjNHCTnQtzCwD5RHKKOL+DYohiPvAFPblxznlGyB63NAY74u"
"2q9Niej6bLDarc9xAKbAuLzRb7hHIGxCTBp5uGf3"
)
TAF_HEAD = bytes.fromhex(
"10032c3c4c56"
"0c687579617564627765627569"
"66"
"096466705265706f7274"
"7d00011056"
"0800010604"
"74526571"
"1d00011048"
"0a060016"
"07616e64726f6964"
"2d0001"
"1032"
)
MAGIC = bytes.fromhex("571882cf664bb39401ee")
CW_TAIL = bytes.fromhex("3600400c0b8c980ca80c")
CW_JSON_LEN = 586
CW_COLL_LEN = 3548
CW_LEN = 2 + CW_JSON_LEN + CW_COLL_LEN + len(CW_TAIL)
class DfpRegistrationError(RuntimeError):
"""新设备注册链失败。"""
def _build_get_dfp_config_request() -> bytes:
"""编码 getDfpConfig 请求(67 字节 TAF/WUP)。"""
wup = WupRequest()
wup.setServant("huyaudbwebui")
wup.setFunc("getDfpConfig")
wup.iTimeout = 0
wup.writeStruct("tReq", {"app_id": "5008"})
return wup.encode()
def _build_select_operator_request(
fingerprint: str | None = None,
device_info: Mapping[str, str] | None = None,
) -> bytes:
"""按 selectOperator 的四段结构动态编码请求。"""
dev = {
"app_version": "13.4.22",
"model": "M2102J2SC",
"fingerprint": fingerprint or "02df398797432eadefcc12767119ad5e80999389",
"screen": "M2102J2SC,30,11",
}
if device_info:
for key in dev:
if device_info.get(key):
dev[key] = str(device_info[key])
body = TafOutputStream()
body.write_struct_begin(0)
body.write_struct_begin(0)
body.write_int8(0, 0)
body.write_string(1, "1.0")
body.write_string(2, f"hyudb_{int(time.time() * 1000)}")
body.write_string(3, "5008")
body.write_int8(4, 0)
body.write_string(5, SELECT_OPERATOR_SDID)
for tag in range(6, 10):
body.write_string(tag, "")
body.write_struct_end()
body.write_struct_begin(1)
body.write_string(0, "")
body.write_string(1, dev["app_version"])
body.write_string(2, "2.0.8")
for tag in range(3, 7):
body.write_string(tag, "")
body.write_struct_end()
body.write_struct_begin(2)
body.write_int8(0, 1)
body.write_string(1, dev["model"])
body.write_string(2, dev["fingerprint"])
body.write_string(3, "android")
body.write_string(4, dev["screen"])
body.write_string(6, "")
body.write_string(7, "")
body.write_struct_end()
body.write_list(3, [0, 1, 3])
body.write_struct_end()
wup = WupRequest()
wup.setServant("huyaudbwebui")
wup.setFunc("selectOperator")
wup.iTimeout = 0
wup.newdata["_wup_data"] = body.get_bytes()
return wup.encode()
def _load_chain(
fingerprint: str | None = None,
device_info: Mapping[str, str] | None = None,
) -> dict[str, tuple[bytes, bytes]]:
"""生成注册链请求,保留 tuple 形状兼容旧调用方和测试。"""
return {
"getDfpConfig": (_build_get_dfp_config_request(), b""),
"selectOperator": (
_build_select_operator_request(fingerprint, device_info),
b"",
),
"dfpReport": (b"", b""),
}
def _post(
body: bytes,
content_type: str = "application/octet-stream",
timeout: float = 20,
proxies: Mapping[str, str] | None = None,
) -> bytes:
try:
response = requests.post(
WSAPI,
data=body,
headers={
"Content-Type": content_type,
"User-Agent": UA,
"Accept-Encoding": "gzip",
},
timeout=timeout,
proxies=dict(proxies) if proxies else None,
)
response.raise_for_status()
return response.content
except requests.RequestException as exc:
raise DfpRegistrationError(f"注册链 HTTP 请求失败: {exc}") from exc
def _random_triple() -> tuple[str, str, str]:
"""生成 dfp JSON 中的 40hex/40hex/64hex 三元组。"""
return (
hashlib.sha256(os.urandom(32) + b"hdid").hexdigest(),
hashlib.sha256(os.urandom(32) + b"devid").hexdigest(),
hashlib.sha256(os.urandom(32) + b"appkey").hexdigest(),
)
def _build_random_dfp_body(
device_info: Mapping[str, str] | None = None,
) -> bytes:
"""构造服务端接受的随机 dfpReport 请求体。
cw 中 JSON 段明文仅承载真实请求的段长度与三元组形态,实证(2026-08-27)
服务端不校验 cw 内容,随机 cw 照样 200 + 新签发 t2/t5,因此三元组
hdid/device_id/appkey,各 64hex sha256)只为凑齐模板形态,属诊断用途;
device_id、hdid 在画像存在时分别使用画像 device_id、guid32。
"""
json_plain = _build_dfp_json_plain(device_info)
seed = json_plain
if len(seed) > CW_JSON_LEN:
# 保持已验证的固定 cw 段长度,同时让完整画像参与本次随机报文。
seed = hashlib.sha256(seed).digest()
json_sec = os.urandom(CW_JSON_LEN)
if len(seed) <= CW_JSON_LEN:
mask = os.urandom(len(seed))
json_sec = bytes(a ^ b for a, b in zip(seed, mask)) + json_sec[len(seed) :]
cw = os.urandom(2) + json_sec + os.urandom(CW_COLL_LEN) + CW_TAIL
if len(cw) != CW_LEN:
raise DfpRegistrationError(f"dfpReport cw 长度异常: {len(cw)}")
body = (
struct.pack(">I", len(TAF_HEAD) + len(MAGIC) + len(cw) + 4)
+ TAF_HEAD
+ MAGIC
+ cw
)
if len(body) != 4226:
raise DfpRegistrationError(f"dfpReport body 长度异常: {len(body)}")
return body
def _build_dfp_json_plain(device_info: Mapping[str, str] | None = None) -> bytes:
"""生成 dfpReport 加密段使用的画像 JSON 明文。"""
hdid, device_id, appkey = _random_triple()
dev = device_info or {}
app_version = str(dev.get("app_version") or "13.4.22")
vendor = str(dev.get("vendor") or "xiaomi")
model = str(dev.get("model") or "M2102J2SC")
screen = str(dev.get("screen") or "M2102J2SC,30,11")
width = str(dev.get("width") or "1080")
height = str(dev.get("height") or "2120")
# 注册前的 deviceId/guid 使用账号画像;缺失时才生成一次性形态值。
device_id = str(dev.get("device_id") or device_id)
hdid = str(dev.get("guid32") or hdid)
json_plain = (
f'{{"appId":"5008","appVer":"{app_version}","appkey":"{appkey}",'
f'"channel":"{vendor}","deviceId":"{device_id}",'
f'"deviceName":"{model}","hdid":"{hdid}",'
f'"heightPixels":"{height}","isCloud":0,"isForbidLog":1,"isHome":0,'
'"isPre":0,"openAppId":"","savePath":"/data/user/0/com.duowan.kiwi/files/huyaudb",'
'"sdkVer":"1.0.80138","servantName":"huyaudbwebui",'
'"shareAppDataPath":"/data/user/0/com.duowan.kiwi/files/huyaudb",'
f'"systemInfo":"android","systemVer":"{screen}",'
f'"terminalType":1,"testEnv":0,"widthPixels":"{width}"}}'
)
return json_plain.encode("utf-8")
def _select_operator_request(template: bytes, fingerprint: str | None) -> bytes:
if fingerprint and len(fingerprint) == 40:
old = b"02df398797432eadefcc12767119ad5e80999389"
index = template.find(old)
if index >= 0:
return (
template[:index]
+ fingerprint.encode("ascii")
+ template[index + len(old) :]
)
return template
def _parse_response(data: bytes) -> tuple[str, str, str]:
try:
t1_match = re.search(rb"\x16\x20([0-9a-f]{32})", data)
t2_match = re.search(rb"\x26\xb4([A-Za-z0-9+/=]{180})", data)
t5_match = re.search(rb"\x56\x28([0-9a-f]{40})", data)
if not t1_match or not t2_match or not t5_match:
raise ValueError("missing response fields")
t1 = t1_match.group(1).decode()
t2 = t2_match.group(1).decode("latin1")
t5 = t5_match.group(1).decode()
except (AttributeError, ValueError) as exc:
raise DfpRegistrationError("dfpReport 响应缺少 t1/t2/t5") from exc
return t1, t2, t5
def register_device(
fingerprint: str | None = None,
proxies: Mapping[str, str] | None = None,
timeout: float = 20,
device_info: Mapping[str, str] | None = None,
) -> tuple[str, str, str]:
"""执行新注册链,返回 ``(t1, safedeviceid, device_id)``。"""
chain = _load_chain(fingerprint=fingerprint, device_info=device_info)
_post(chain["getDfpConfig"][0], timeout=timeout, proxies=proxies)
select_request = _select_operator_request(chain["selectOperator"][0], fingerprint)
_post(select_request, "application/x-wup", timeout, proxies)
response = _post(
_build_random_dfp_body(device_info), timeout=timeout, proxies=proxies
)
return _parse_response(response)