统一虎牙 App 登录设备画像

This commit is contained in:
yml2213
2026-08-30 23:04:28 +08:00
parent bc02018719
commit f4ed518b50
6 changed files with 281 additions and 119 deletions
+135 -42
View File
@@ -7,10 +7,10 @@
就是 WUP 登录帧的设备字段签发源;
* 请求模板只承载 TAF/WUP 协议形状,旧的 dfpReport 密文不会被使用。
请求模板来自 ``evidence/dfp_chain_golden.json``,与现有项目的证书/信封样本
一样作为协议模板使用。模板中的设备字段会在 selectOperator 步骤按当前画像更新
请求按已验证的 TAF/WUP 字段布局动态编码;不依赖抓包证据文件。selectOperator
中的 RSA 字段使用协议固定格式的密文种子,设备画像字段在每次请求时写入
错误语义: 链上任何一步失败(模板缺失/损坏、HTTP 异常、响应缺字段)都会抛
错误语义: 链上任何一步失败(请求编码、HTTP 异常、响应缺字段)都会抛
``DfpRegistrationError``。调用方(core/huya/app_login)应把注册失败当作明确失败
终止登录;禁止静默回退到画像里的旧固定 safedeviceid/device_id。
@@ -26,23 +26,25 @@
from __future__ import annotations
import base64
import hashlib
import json
import os
import re
import socket
import ssl
import struct
from pathlib import Path
from typing import Mapping
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"
ROOT = Path(__file__).resolve().parents[2]
CHAIN_FILE = ROOT / "evidence" / "dfp_chain_golden.json"
SELECT_OPERATOR_SDID = (
"PQwemAN9NHkZKoMqVeJmXhIypqMTaQEOrmXr37xQVhQZqrPxjEKEQ11xvE02qawcys/"
"iwl5gSIa6nYz0L5tNl0VdpZFyNjNHCTnQtzCwD5RHKKOL+DYohiPvAFPblxznlGyB63NAY74u"
"2q9Niej6bLDarc9xAKbAuLzRb7hHIGxCTBp5uGf3"
)
TAF_HEAD = bytes.fromhex(
"10032c3c4c56"
@@ -69,20 +71,86 @@ class DfpRegistrationError(RuntimeError):
"""新设备注册链失败。"""
def _load_chain() -> dict[str, tuple[bytes, bytes]]:
if not CHAIN_FILE.exists():
raise DfpRegistrationError(f"注册链模板不存在: {CHAIN_FILE}")
try:
data = json.loads(CHAIN_FILE.read_text(encoding="utf-8"))
return {
name: (
base64.b64decode(item["req_b64"]),
base64.b64decode(item["resp_b64"]),
)
for name, item in data.items()
}
except (OSError, ValueError, KeyError) as exc:
raise DfpRegistrationError(f"注册链模板读取失败: {exc}") from exc
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(
@@ -118,26 +186,21 @@ def _random_triple() -> tuple[str, str, str]:
)
def _build_random_dfp_body() -> bytes:
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)只为凑齐模板形态,属诊断用途
hdid/device_id/appkey,各 64hex sha256)只为凑齐模板形态,属诊断用途
device_id、hdid 在画像存在时分别使用画像 device_id、guid32。
"""
hdid, device_id, appkey = _random_triple()
json_plain = (
'{"appId":"5008","appVer":"13.4.22","appkey":"%s",'
'"channel":"xiaomi","deviceId":"%s",'
'"deviceName":"M2102J2SC","hdid":"%s",'
'"heightPixels":"2120","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",'
'"systemInfo":"android","systemVer":"M2102J2SC,30,11",'
'"terminalType":1,"testEnv":0,"widthPixels":"1080"}'
) % (appkey, device_id, hdid)
seed = json_plain.encode("utf-8")
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))
@@ -156,6 +219,33 @@ def _build_random_dfp_body() -> bytes:
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"
@@ -188,11 +278,14 @@ 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()
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(), timeout=timeout, proxies=proxies)
response = _post(
_build_random_dfp_body(device_info), timeout=timeout, proxies=proxies
)
return _parse_response(response)