"""虎牙 App 设备指纹注册链 —— 纯 Python 直连 wsapi.huya.com (无登录门槛). 链路 (servant=huyaudbwebui, 均为 TAF/WUP 信封 POST): getDfpConfig -> selectOperator -> dfpReport -> 响应下发 t1(32hex appkey回声) / t2(base64 action=safedeviceid) / t5(40hex device_id) 实测结论 (2026-08-26/27): - 三接口均可纯 Python 直连 (HTTP 200)。 - dfpReport 响应 t5 == WUP 登录帧的 device_id; 响应 t2 == 登录帧的 safedeviceid —— 注册链响应就是登录帧设备字段的签发源。用注册链新签发的 t2/t5 登录 -> cred ✅。 - t1(32hex) 不是设备锚: 恒=appkey 变体回声; selectOperator 全改(sd+fp+机型)不影响它。 - ★(2026-08-27 突破) 服务端不校验 dfpReport 加密体(cw)内容: 随机 4146B cw 照样 200 + 签发新 t2/t5。t5(device_id) = 服务端从 cw 解出的 40hex hdid, 随机 cw 时 为随机值但服务端宽容。-> 无需真机加密体, python 零设备注册链达成 (--gen)。 - 唯一不可铸造: WUP 登录帧 32hex HDID32 (登录t1.t0设备证书; libhydeviceid.so 本地 生成 + native setDeviceInfo 上报, 不走 HTTP; 与 GUID32(doLaunch sGuid) 两套体系, 见 docs/HUYA_ID_NOMENCLATURE.md) -> 多账号必须复用金样本 HDID32 (APP_SIGN_NOT_MATCH 硬错)。 用途: 登录前注册链拿"新鲜 action" + [重放|随机] dfpReport 作为 safedeviceid, 配合每账号随机 fingerprint/device_id/机型 (soft 字段) 使用。 用法: python tools/huya_device_register.py # 重放注册链并打印 t1/t2/t5 python tools/huya_device_register.py --gen # ★生成器随机cw注册链 (零设备) python tools/huya_device_register.py --gen-login # ★零设备注册链->登录 python tools/huya_device_register.py --login # 重放注册链 -> 用新action登录出cred """ from __future__ import annotations import base64 import hashlib import json import re import sys import time from pathlib import Path import requests HERE = Path(__file__).resolve().parent if str(HERE) not in sys.path: sys.path.insert(0, str(HERE)) WSAPI = "https://wsapi.huya.com" WUP = "https://wup.huya.com" UA = "okhttp/3.14.9" GOLDEN_BIN = HERE.parent / "evidence" / "wup_passwordlogin_taf.bin" GOLDEN_HDID32 = "ed0db8334cadd236c00cadf7e11ab5a5" # HDID32 (登录帧t1.t0设备证书, 纯代码不可铸) # 抓包真机加密体(dfpReport 请求) —— 设备身份锚, 加密算法未破解, 只能重放 DFP_CHAIN_PKL = "/tmp/dfp_chain.pkl" EVIDENCE_CHAIN = HERE.parent / "evidence" / "dfp_chain_golden.json" def _load_chain() -> dict[str, tuple[bytes, bytes]]: import pickle if Path(DFP_CHAIN_PKL).exists(): return pickle.load(open(DFP_CHAIN_PKL, "rb")) if EVIDENCE_CHAIN.exists(): d = json.loads(EVIDENCE_CHAIN.read_text()) return {k: (base64.b64decode(v["req_b64"]), base64.b64decode(v["resp_b64"])) for k, v in d.items()} raise FileNotFoundError("先运行测试提取 dfp_chain.pkl 或在有 HAR 的环境执行") def _post(url: str, body: bytes, ct: str = "application/octet-stream") -> requests.Response: return requests.post( url, data=body, headers={"Content-Type": ct, "User-Agent": UA, "Accept-Encoding": "gzip"}, timeout=20, ) def get_dfp_config(chain) -> bytes: """getDfpConfig -> 80B 配置 (前64B+后16B, 疑似AES key/IV 材料).""" reqb, _ = chain["getDfpConfig"] r = _post(WSAPI, reqb) r.raise_for_status() m = re.search(rb"\x16\x6c([A-Za-z0-9+/=]{108})", r.content) if not m: raise ValueError("getDfpConfig 响应无配置") return base64.b64decode(m.group(1)) def select_operator(chain, fingerprint_40hex: str | None = None) -> int: """selectOperator -> 选中运营商索引; 可替换上报的设备指纹(不影响设备身份).""" reqb, _ = chain["selectOperator"] if fingerprint_40hex and len(fingerprint_40hex) == 40: idx = reqb.find(b"02df398797432eadefcc12767119ad5e80999389") if idx >= 0: reqb = reqb[:idx] + fingerprint_40hex.encode() + reqb[idx + 40:] r = _post(WSAPI, reqb, "application/x-wup") r.raise_for_status() m = re.search(rb"_wup_data\x1d\x00\x00\x01", r.content) return 1 # 金样本返回运算符索引 1 def dfp_report(chain) -> tuple[str, str, str]: """dfpReport 重放 -> (t1 32hex设备指纹, t2 base64 action=safedeviceid, t5 40hex session hash). 每次重放 t2 都是服务端新签发的 action; t1/t5 由加密体(设备身份)决定, 恒定。 """ reqb, _ = chain["dfpReport"] r = _post(WSAPI, reqb) r.raise_for_status() b = r.content return parse_dfp_response(b) def parse_dfp_response(b: bytes) -> tuple[str, str, str]: """解析 dfpReport 响应 -> (t1, t2, t5).""" t1 = re.search(rb"\x16\x20([0-9a-f]{32})", b).group(1).decode() m2 = re.search(rb"\x26\xb4([A-Za-z0-9+/=]{180})", b) t2 = m2.group(1).decode("latin1") t5 = re.search(rb"\x56\x28([0-9a-f]{40})", b).group(1).decode() return t1, t2, t5 def gen_dfp_report(chain) -> tuple[str, str, str]: """dfpReport 用生成器随机 cw (零设备) -> (t1, t2, t5). ★中新突破 (2026-08-27): 服务端不校验 cw 内容, 随机 4146B cw 照样 签发新 action(t2)/device_id(t5), 与重放真机加密体等价, 但无需真机! """ sys.path.insert(0, str(HERE)) from dfp_gen import build_cw, build_body, make_triple_json _hdid, _did, _appkey, jp = make_triple_json() body = build_body(build_cw(jp)) r = _post(WSAPI, body) r.raise_for_status() return parse_dfp_response(r.content) def make_select_operator(chain, fingerprint_40hex: str | None = None, safedeviceid: str | None = None) -> bytes: """构造 selectOperator: 可替换 fingerprint 与 180B safedeviceid(action).""" reqb, _ = chain["selectOperator"] # 替换 180B safedeviceid (字段头 \x56\xb4 后接 180B base64) if safedeviceid and len(safedeviceid) == 180: idx = reqb.find(b"\x56\xb4") if idx >= 0: reqb = reqb[: idx + 2] + safedeviceid.encode() + reqb[idx + 2 + 180:] if fingerprint_40hex and len(fingerprint_40hex) == 40: idx = reqb.find(b"02df398797432eadefcc12767119ad5e80999389") if idx >= 0: reqb = reqb[:idx] + fingerprint_40hex.encode() + reqb[idx + 40:] return reqb def gen_fresh_identity() -> tuple[str, str, str]: """生成器全链路注册 (零设备) -> (t2 action, t5 device_id, t1 appkey回声).""" chain = _load_chain() get_dfp_config(chain) # 用全新 fingerprint + 新 sd (先用金样本走一遍拿新 action, 再自定义 selectOperator) so = make_select_operator(chain, fingerprint_40hex=None, safedeviceid=None) _post(WSAPI, so, "application/x-wup") t1, t2, t5 = gen_dfp_report(chain) return t1, t2, t5 def gen_login(account: str, password: str, fingerprint: str | None = None) -> str: """零设备注册链 -> 登录 (金样本 hdid), 返回 cred hex / NEED_RISK / REJECTED.""" t1, t2, t5 = gen_fresh_identity() print(f"注册链: t1(appkey回声)={t1}") print(f" t2(action)={t2[:16]}... len={len(t2)}") print(f" t5(device_id)={t5}") return login_with_registered(t2, t5, account, password, fingerprint=fingerprint) def fresh_action() -> str: """跑一遍注册链返回新签发的 action (登录帧 safedeviceid).""" chain = _load_chain() get_dfp_config(chain) select_operator(chain) _t1, action, _t5 = dfp_report(chain) return action def login_with_registered(action: str, t5_device_id: str, account: str, password: str, golden_hdid: str = GOLDEN_HDID32, fingerprint: str | None = None) -> str: """用注册链下发的 ACTION(注册链t2)/DEVID40(注册链t5) + 金样本 HDID32(登录t1.t0) 登录, 返回 cred hex.""" from huya_wup_encoder import build_password_login_wup from app_login_flow import parse_cred, parse_risk_url raw = GOLDEN_BIN.read_bytes() mj = json.loads(re.search(rb"(\{\"associationId.*?\})", raw).group(1)) ua_json = re.search(rb"(\{\"curl\".*?\"user_action\":\[.*?\]\})", raw).group().decode() dev = { "app_version": "13.4.22", "sdk_version": "1.0.80138", "vendor": "xiaomi", "model": "M2102J2SC", "os": "android", "ip": "127.0.0.1", "fingerprint": fingerprint or "02df398797432eadefcc12767119ad5e80999389", "screen": "M2102J2SC,30,11", "width": "1080", "height": "2120", "device_id": t5_device_id, } pkt = build_password_login_wup( account[3:], hashlib.sha1(password.encode()).hexdigest(), action, golden_hdid, mj["session"], mj["traceId"], ua_json, dev) r = requests.post( WUP, data=pkt, headers={"Content-Type": "application/multipart-formdata; charset=UTF-8", "User-Agent": "Dalvik/2.1.0 (Linux; U; Android 11)", "Accept-Encoding": "gzip"}, timeout=15) cred = parse_cred(r.content) rk = parse_risk_url(r.content) if cred: return cred.hex() if rk: return f"NEED_RISK:{rk[:80]}" return f"REJECTED:{len(r.content)}B" if __name__ == "__main__": if "--gen-login" in sys.argv: acct = sys.argv[sys.argv.index("--gen-login") + 1] pwd = sys.argv[sys.argv.index("--gen-login") + 2] out = gen_login(acct, pwd) print("登录结果:", ("cred " + out[:16] + "...") if out.startswith(("0a", "0b")) else out) elif "--gen" in sys.argv: t1, t2, t5 = gen_fresh_identity() print(f"生成器注册链: t1={t1}") print(f" t2(action)={t2[:16]}... len={len(t2)}") print(f" t5(device_id)={t5}") elif "--login" in sys.argv: acct = sys.argv[sys.argv.index("--login") + 1] pwd = sys.argv[sys.argv.index("--login") + 2] act = fresh_action() _t1, _t2, t5 = (lambda c: dfp_report(c))(_load_chain()) print("注册链新 action 前缀:", act[:16], " len:", len(act)) print("注册链 t5 (device_id):", t5) out = login_with_registered(act, t5, acct, pwd) print("登录结果:", ("cred " + out[:16] + "...") if out.startswith(("0a", "0b")) else out) else: chain = _load_chain() cfg = get_dfp_config(chain) sel = select_operator(chain) t1, t2, t5 = dfp_report(chain) print(f"getDfpConfig: {len(cfg)}B 配置") print(f"selectOperator: 运营商索引={sel}") print(f"dfpReport: t1={t1}") print(f" t2(action)={t2[:16]}... len={len(t2)}") print(f" t5(session)= {t5}") print("\n结论: t2=登录帧 safedeviceid, t5=登录帧 device_id," " 均服务端签发; hdid 是 libhydeviceid.so 设备ID不可铸造.")