- core/huya/account_env.py: 账号↔设备环境一对一绑定 (幂等画像 + guid32/Hebe_D1-D5
每账号终身一致字段, 来源 R36 解密真实指纹 JSON), 主流程 bind_and_login +
CLI (python -m core.huya.account_env 账号 密码)
- device_profile.py: 修复 generate_profile 引用未定义 HDID 的 NameError; HDID32
注释补 R15/R39 来源 (md5('5008_ver_'+k1), k1=datadiv 常量)
- 实弹验证: 环境指纹走注册链, 服务端签发 t2(180B)/t5(40hex) ✅
155 lines
7.2 KiB
Python
155 lines
7.2 KiB
Python
"""账号 ↔ 设备环境 一对一绑定与登录(生产入口)。
|
||
|
||
目标: **一个账号密码 ↔ 一套设备环境**, 首次绑定后环境终身复用。
|
||
|
||
整体链路(每步"是什么/从哪来"见各函数与行内注释):
|
||
|
||
1. get_or_create_env(account) 生成/复用该账号专属设备环境 (幂等, 落盘)
|
||
2. HuyaAppPasswordLogin(...) 内部链: dfp 注册链(用本环境指纹) → 服务端签发
|
||
safedeviceid(t2) + device_id(t5) → WUP 密码登录
|
||
→ cred(114B) → 全套 Cookie (风控滑块自动过验)
|
||
3. 绑定元数据回写 data/huya_device_profiles.json
|
||
|
||
为什么环境可以"一号一套"(算法依据, 均已破解定案):
|
||
- 登录帧 t1.t0 (hdid32) 是 **app 版本级常量**, 与设备无关 (R15):
|
||
md5("5008_13.4.22_" + k1) = ed0db8334cadd236c00cadf7e11ab5a5
|
||
k1 = 865a4924a40897ac1fcfe6b4c2cbb0e3 是 libhydeviceid.so datadiv 内嵌常量 (R39),
|
||
同 app 版本所有真机算出同一值 → 服务端无法用 t1.t0 区分设备, 离线永远算得对。
|
||
- 服务端真正区分设备靠注册链实时签发的 safedeviceid(t2)/device_id(t5)
|
||
(2026-08-27 差分实验); 每账号环境固定 → 每次注册都等同"同一台设备换发令牌"。
|
||
- dfp 上报报文本体 = gzip + RC4(865a...abc798) (R36/R37), RC4 key 同为 APK 内嵌
|
||
常量 → 伪造报文与真机密码学不可区分; 当前注册链实测服务端不校验 cw 内容,
|
||
环境字段用于画像一致性, 不作为注册成败条件。
|
||
|
||
用法:
|
||
python -m core.huya.account_env <账号> <密码> # 绑定(如未有)+登录
|
||
python -m core.huya.account_env <账号> <密码> --new-device # 抛弃旧环境, 换新设备
|
||
python -m core.huya.account_env <账号> --show # 只查看该账号绑定
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import hashlib
|
||
import json
|
||
import os
|
||
import sys
|
||
import time
|
||
|
||
from .device_profile import (
|
||
HDID32, # 登录帧 t1.t0 32hex 常量 (R15 公式结果, k1 来源见 R39; device_profile.py)
|
||
_load_db,
|
||
_save_db,
|
||
get_profile, # 幂等画像: 同账号永远复用同一套 (data/huya_device_profiles.json)
|
||
)
|
||
from .app_login import HuyaAppPasswordLogin
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 环境生成: 一号一套的"环境"定义
|
||
# ---------------------------------------------------------------------------
|
||
def _rand_hex(n_bytes: int, salt: bytes) -> str:
|
||
"""随机 n_bytes 的 hex 串 (salt 仅用于区分字段用途, 服务端不校验)。"""
|
||
return hashlib.sha256(os.urandom(n_bytes) + salt).hexdigest()[: n_bytes * 2]
|
||
|
||
|
||
def get_or_create_env(account: str, force_new: bool = False) -> dict:
|
||
"""取回/首次生成该账号绑定的设备环境 (幂等)。
|
||
|
||
字段来源:
|
||
- vendor/model/screen/width/height : 真机参数池 (device_profile.REAL_MODELS,
|
||
与金样本捕获真机 M2102J2SC 同构)
|
||
- fingerprint (40hex) : CDID 形态设备指纹 (sha1 随机); 金样本
|
||
02df398797432eadefcc12767119ad5e80999389 即此形态; 注册链会把它替换进
|
||
selectOperator 请求 (dfp_register._select_operator_request)
|
||
- device_id (40hex) : 画像侧占位; 登录实际用注册链签发的 t5
|
||
- hdid (32hex) : 登录帧 t1.t0 = R15 常量 (HDID32)
|
||
- guid32 (32hex) : GUID 形态标识 (金样本 0a7dfaa8...; 出现于
|
||
dfp 指纹 JSON deviceinfo.guid, R36 解密样本), 每账号独立
|
||
- hebe (D1-D5, 32hex ×5) : dfp 指纹 JSON deviceinfo.Hebe_D1-D5
|
||
(R36 解密真实结构), 每账号独立生成终身复用 → 一号一设备的一致性来源;
|
||
当前注册链服务端不校验 cw 内容, 此字段为后续真实载荷构建预留
|
||
"""
|
||
env = get_profile(account, force_new=force_new)
|
||
db = _load_db()
|
||
record = dict(db.get(account) or env)
|
||
changed = False
|
||
# guid32 / hebe: 一号一致字段, 首次生成后终身不变
|
||
if "guid32" not in record:
|
||
record["guid32"] = _rand_hex(16, b"guid")
|
||
changed = True
|
||
if "hebe" not in record:
|
||
record["hebe"] = {f"Hebe_D{i}": _rand_hex(16, f"hebe{i}".encode()) for i in range(1, 6)}
|
||
changed = True
|
||
if changed:
|
||
db[account] = record
|
||
_save_db(db)
|
||
return record
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 绑定 + 登录: 主流程
|
||
# ---------------------------------------------------------------------------
|
||
def bind_and_login(account: str, password: str,
|
||
force_new_device: bool = False, proxies: dict | None = None) -> dict:
|
||
"""账号 ↔ 环境绑定并登录。
|
||
|
||
注册链在 login_cred_with_flow 内部执行一次 (register_device 用的 fingerprint
|
||
就是本环境的 40hex → 签发的 t2/t5 与环境绑定); safe_auth 滑块过验后的重发
|
||
沿用同一组设备字段 (app_login.login_cred_with_flow)。
|
||
"""
|
||
env = get_or_create_env(account, force_new=force_new_device)
|
||
print(f"[env] {account} ↔ {env.get('vendor')}/{env.get('model')} "
|
||
f"fp40={env.get('fingerprint', '')[:12]}... guid32={env.get('guid32', '')[:12]}... "
|
||
f"t1.t0={env.get('hdid', '')[:12]}...")
|
||
|
||
# device_info 显式传入本环境 (否则 HuyaAppPasswordLogin 内部会再走一次
|
||
# get_profile —— 结果相同, 但显式传入让"环境→注册→登录"的数据流向可读);
|
||
# 登录帧 device_id 用注册链本次签发的 t5 (login_cred_with_flow 内部完成替换)
|
||
login_env = dict(env)
|
||
login_env.setdefault("device_id", env.get("device_id"))
|
||
|
||
result = HuyaAppPasswordLogin(
|
||
account, password, proxies=proxies, device_info=login_env,
|
||
).login()
|
||
|
||
# ---- 绑定元数据回写 (令牌不入库: t2/t5 每次登录实时签发, 环境才是长期身份) ----
|
||
db = _load_db()
|
||
record = dict(db.get(account) or env)
|
||
record.setdefault("bound_at", int(time.time())) # 首次绑定时间
|
||
record["last_login"] = {
|
||
"ok": result.success,
|
||
"msg": result.message[:120],
|
||
"at": int(time.time()),
|
||
}
|
||
db[account] = record
|
||
_save_db(db)
|
||
|
||
return {
|
||
"account": account,
|
||
"env": {"model": env.get("model"), "vendor": env.get("vendor"),
|
||
"fingerprint": env.get("fingerprint"), "guid32": env.get("guid32"),
|
||
"hdid_t1t0": env.get("hdid")},
|
||
"login_success": result.success,
|
||
"login_message": result.message,
|
||
"code": getattr(result, "code", None),
|
||
}
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# CLI
|
||
# ---------------------------------------------------------------------------
|
||
def main(argv: list[str]) -> int:
|
||
args = [a for a in argv if not a.startswith("--")]
|
||
if "--show" in argv and args:
|
||
print(json.dumps(get_or_create_env(args[0]), indent=2, ensure_ascii=False))
|
||
return 0
|
||
if len(args) < 2:
|
||
print(__doc__)
|
||
return 2
|
||
out = bind_and_login(args[0], args[1], force_new_device="--new-device" in argv)
|
||
print(json.dumps(out, indent=2, ensure_ascii=False))
|
||
return 0 if out["login_success"] else 1
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main(sys.argv[1:]))
|