Files
live-hub-py/core/huya/device_profile.py
T
yml2213 4f099cc196 fix(huya): 一号一设备补完 — hydevice 设备参数跟随账号画像 + 画像一致性字段下沉
用户实弹测试暴露两个缺口:
1. Hebe/GUID32 全空: 登录路径走 get_profile, 未经过 account_env 补字段
   → _enrich_profile 下沉到 get_profile (幂等, 存量账号自动补齐)
2. sdid 前缀跨账号同源 (0UnHUgv0_qmfD4KAKlwzh...): env.js 是静态设备仿真
   (写死 M2102J2SC), 状态目录隔离只改了'存储', 没改'采集输入'
   → env.js makeEnv(overrides) 支持按画像覆盖 UA/屏幕/机型/Android版本;
     runner 接收 device.json; get_huya_sdid(device_hint=画像);
     下次登录每账号 hydevice 采集输入=各自绑定机型 → sdid 彻底分家
2026-08-29 16:51:41 +08:00

134 lines
5.0 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.
"""虎牙多账号设备画像生成与管理。
为每个账号生成并持久化独立的设备身份画像(机型、屏幕、指纹、设备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 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 _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 _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) -> dict:
"""补齐 dfp 一致性字段 (幂等): guid32 / Hebe_D1-D5。
来源: R36 解密的真实 dfp 指纹 JSON 结构 (deviceinfo.guid + deviceinfo.Hebe_D1-D5)。
一号一设备: 每账号独立生成后终身不变, 供后续真实载荷构建与 GUI 设备绑定页展示。
"""
changed = False
if not profile.get("guid32"):
profile["guid32"] = hashlib.sha256(os.urandom(16) + b"guid").hexdigest()
changed = True
hebe = profile.get("hebe") or {}
if len(hebe) < 5:
profile["hebe"] = {f"Hebe_D{i}": hashlib.sha256(os.urandom(16) + f"hebe{i}".encode()).hexdigest()
for i in range(1, 6)}
changed = True
return profile if changed else profile
def get_profile(account: str, force_new: bool = False) -> dict:
"""按账号获取或创建画像(幂等:同账号复用同一套, 自动补齐缺失一致性字段)。"""
db = _load_db()
if not force_new and account in db:
enriched = _enrich_profile(db[account])
if enriched is not db[account]:
db[account] = enriched
_save_db(db)
return enriched
p = _enrich_profile(generate_profile())
db[account] = p
_save_db(db)
return p