Files

170 lines
7.2 KiB
Python
Raw Permalink 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.
"""账号 ↔ 设备环境 一对一绑定与登录(生产入口)。
目标: **一个账号密码 ↔ 一套设备环境**, 首次绑定后环境终身复用。
整体链路(每步"是什么/从哪来"见各函数与行内注释):
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 .app_login import HuyaAppPasswordLogin
from .device_profile import (
_load_db,
_save_db,
get_profile, # 幂等画像: 同账号永远复用同一套 (data/huya_device_profiles.json)
)
# ---------------------------------------------------------------------------
# 环境生成: 一号一套的"环境"定义
# ---------------------------------------------------------------------------
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:]))