"""虎牙多账号设备画像生成与管理。 为每个账号生成并持久化独立的设备身份画像(机型、屏幕、指纹、设备ID等)。 注意:画像只承载 *soft* 设备字段(机型/屏幕/随机指纹/随机 device_id)。 ``safedeviceid``(RSA action 令牌)与登录帧 ``device_id`` 由 :mod:`core.huya.dfp_register` 在每次 WUP 登录前经新设备注册链实时签发, 不再写入画像,也不再允许任何固定金样本令牌被多账号复用。 ⚠ 字段状态速览(详见 ``docs/HUYA_APP_OVERVIEW.md`` §四): - 随机: fingerprint / 机型 / 屏幕 / 宽高 / device_id(画像侧); - 固定: hdid(32hex 硬锚,全账号同一) / app_version / sdk_version; - 动态签发: safedeviceid、登录帧 device_id。 """ from __future__ import annotations import hashlib import json import os import random from collections.abc import Mapping from pathlib import Path ROOT = Path(__file__).resolve().parent.parent.parent DATA_DIR = ROOT / "data" PRIMARY_PROFILE_DB = DATA_DIR / "huya_device_profiles.json" FALLBACK_PROFILE_DB = ROOT / "evidence" / "device_profiles.json" REAL_MODELS = [ ("xiaomi", "M2102J2SC", "M2102J2SC,30,11", (1080, 2120)), ("vivo", "V2370A", "V2370A,34,13", (1080, 2412)), ("oppo", "PFFM20", "PFFM20,34,13", (1080, 2412)), ("honor", "SDY-AN00", "SDY-AN00,31,12", (1080, 2400)), ("samsung", "SM-G9910", "SM-G9910,31,12", (1440, 3200)), ("oneplus", "PGZ110", "PGZ110,34,13", (1080, 2412)), ("redmi", "23049PCD8G", "23049PCD8G,34,13", (1080, 2400)), ("realme", "RMX3366", "RMX3366,34,13", (1080, 2412)), ("iqoo", "V2183A", "V2183A,34,13", (1080, 2400)), ("nubia", "NX729J", "NX729J,33,13", (1080, 2400)), ("motorola", "XT2301-5", "XT2301-5,33,13", (1080, 2400)), ("gionee", "GN9013", "GN9013,29,10", (720, 1560)), ] # 登录帧 t1.t0 32hex 设备证书 (R15: md5("5008_13.4.22_"+k1); k1 见 R39 datadiv 常量; # 换 app 版本时需按新 app_version 重算 — 与 GUID32/doLaunch 体系无关) HDID32 = "ed0db8334cadd236c00cadf7e11ab5a5" APP_VERSION = "13.4.22" SDK_VERSION = "1.0.80138" def mobile_user_agent(device_info: Mapping[str, object]) -> str: """根据统一设备画像生成 App WebView UA。""" screen = str(device_info.get("screen") or "") parts = screen.split(",") android_ver = parts[2] if len(parts) >= 3 and parts[2] else "11" api_level = parts[1] if len(parts) >= 2 and parts[1] else "30" model = str(device_info.get("model") or "M2102J2SC") vendor = str(device_info.get("vendor") or "android") app_version = str(device_info.get("app_version") or APP_VERSION) return ( f"Mozilla/5.0 (Linux; Android {android_ver}; {model} " "Build/RKQ1.200826.002; wv) AppleWebKit/537.36 (KHTML, like Gecko) " f"Version/4.0 Chrome/149.0.7827.159 Mobile Safari/537.36 huya adr/" f"{app_version}/{vendor}/{api_level}" ) def _rand_sha1_hex() -> str: return hashlib.sha1(os.urandom(20)).hexdigest() def generate_profile(model_pick=None) -> dict: """生成一套随机设备画像。""" vendor, model, screen, (w, h) = ( random.choice(REAL_MODELS) if model_pick is None else model_pick ) return { "app_version": APP_VERSION, "sdk_version": SDK_VERSION, "vendor": vendor, "model": model, "os": "android", "ip": "127.0.0.1", "fingerprint": _rand_sha1_hex(), "screen": screen, "width": str(w), "height": str(h), "device_id": _rand_sha1_hex(), # hdid = 登录帧 t1.t0 的 32hex appSign (R15 定案): # md5("5008_" + app_version + "_" + k1) = ed0db833...ab5a5 # k1 = 865a4924a40897ac1fcfe6b4c2cbb0e3 (libhydeviceid.so datadiv 内嵌常量, R39) # → 是 app 版本级常量 (同版本所有真机同值), 非设备级, 离线可算 "hdid": HDID32, # 注: 不含 safedeviceid —— 每次登录前由 dfp_register 注册链签发, # 画像不再持久化固定令牌 (见模块注释)。 } def record_login(account: str, ok: bool, message: str = "") -> None: """记录该账号环境的一次登录结果 (最后登录时间/成败)。 幂等元数据: bound_at 只在首次出现时写入; 失败也记录 (设备绑定页要展示)。 供 app_login 登录流程调用, GUI 设备绑定页读取展示。 """ import time as _time db = _load_db() rec = db.get(account) if rec is None: return # 尚无环境的账号 (纯 Cookie 导入) 不生成记录 now = int(_time.time()) rec.setdefault("bound_at", now) rec["last_login"] = {"ok": bool(ok), "msg": str(message or "")[:120], "at": now} _save_db(db) def _load_db() -> dict: if PRIMARY_PROFILE_DB.exists(): try: return json.loads(PRIMARY_PROFILE_DB.read_text("utf-8")) except Exception: pass if FALLBACK_PROFILE_DB.exists(): try: return json.loads(FALLBACK_PROFILE_DB.read_text("utf-8")) except Exception: pass return {} def _save_db(db: dict) -> None: try: PRIMARY_PROFILE_DB.parent.mkdir(parents=True, exist_ok=True) PRIMARY_PROFILE_DB.write_text( json.dumps(db, indent=2, ensure_ascii=False), encoding="utf-8" ) except Exception: pass def _enrich_profile(profile: dict) -> tuple[dict, bool]: """补齐 dfp 一致性字段 (幂等): guid32 / Hebe_D1-D5, 返回 (新画像, 是否有变更)。 来源: R36 解密的真实 dfp 指纹 JSON 结构 (deviceinfo.guid + deviceinfo.Hebe_D1-D5)。 一号一设备: 每账号独立生成后终身不变, 供后续真实载荷构建与 GUI 设备绑定页展示。 注意: 返回新字典 (不原地改), 调用方据此判断是否需要回写。 """ out = dict(profile) changed = False if not out.get("guid32"): out["guid32"] = hashlib.sha256(os.urandom(16) + b"guid").hexdigest() changed = True if len(out.get("hebe") or {}) < 5: out["hebe"] = { f"Hebe_D{i}": hashlib.sha256( os.urandom(16) + f"hebe{i}".encode() ).hexdigest() for i in range(1, 6) } changed = True return out, changed def get_profile(account: str, force_new: bool = False) -> dict: """按账号获取或创建画像(幂等:同账号复用同一套, 自动补齐缺失一致性字段)。""" db = _load_db() if not force_new and account in db: enriched, changed = _enrich_profile(db[account]) if changed: db[account] = enriched _save_db(db) return enriched p, _ = _enrich_profile(generate_profile()) db[account] = p _save_db(db) return p