feat(huya): 整合 App 协议登录获取 Cookie 全链路并与 Web 登录解耦
- core/huya: 新增 app_login, wup_encoder, nonce_forge, cert_forge, envelope_forge, device_profile, udb_aes - core/huya/__init__.py: 导出 login_huya_app_password 与 HuyaAppPasswordLogin - web/backend: 新增 /accounts/app-password-login 与 /accounts/app-password-login/selected 路由及 Schema,与原 Web 密码登录独立分开 - web/frontend: 增加 App 登录 API 与前端界面“App 登录选中”操作,弹窗结果明确区分 - tests: 新增 test_huya_app_login.py 单元测试覆盖全链路
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
"""虎牙 udb 登录证书 (biz_token) 铸造与解析工具。
|
||||
|
||||
证书格式:base64( [0x0c][key_idx][AES-128-ECB(key16, zeropad(P1))] )
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import os
|
||||
import struct
|
||||
|
||||
from .udb_aes import udb_decrypt, udb_encrypt
|
||||
|
||||
KEY_TABLE_RAW = [
|
||||
"4VYcPdvKKqjBHZtCmbroRXHk",
|
||||
"xXEDWqiKLGwEZ6HubEiswCqK",
|
||||
"3FMHubdKosFrhmXNLHTNHZwe",
|
||||
]
|
||||
DEFAULT_KEY16 = KEY_TABLE_RAW[0][:16].encode()
|
||||
|
||||
|
||||
def build_p1(
|
||||
app_id: bytes,
|
||||
fingerprint: bytes,
|
||||
cred: bytes,
|
||||
rnd: bytes | None = None,
|
||||
) -> bytes:
|
||||
"""按样本结构组装 P1 明文。
|
||||
|
||||
- app_id: b'5008' 等 4 字节
|
||||
- fingerprint: 40 字节 ASCII hex
|
||||
- cred: 114 字节 hyCred
|
||||
- rnd: 20 字节 nonce,缺省用 os.urandom(20)
|
||||
"""
|
||||
assert len(fingerprint) == 40, f"指纹应为40B, 实际为 {len(fingerprint)}"
|
||||
assert len(cred) == 114, f"cred应为114B, 实际为 {len(cred)}"
|
||||
r = os.urandom(20) if rnd is None else rnd
|
||||
p1 = b"\x01\x04\x00" + app_id
|
||||
p1 += struct.pack("<H", len(r)) + r
|
||||
p1 += struct.pack("<H", len(fingerprint)) + fingerprint
|
||||
p1 += struct.pack("<H", len(cred)) + cred
|
||||
assert len(p1) == 187, f"P1长度应为187,实际为 {len(p1)}"
|
||||
return p1
|
||||
|
||||
|
||||
def parse_p1(data: bytes) -> dict:
|
||||
"""解析 P1 结构。"""
|
||||
o = 0
|
||||
|
||||
def tk(n: int) -> bytes:
|
||||
nonlocal o
|
||||
b = data[o:o + n]
|
||||
o += n
|
||||
return b
|
||||
|
||||
out = {
|
||||
"head": tk(3).hex(),
|
||||
"app_id": tk(4),
|
||||
}
|
||||
n = struct.unpack("<H", tk(2))[0]
|
||||
out["rnd"] = tk(n)
|
||||
n = struct.unpack("<H", tk(2))[0]
|
||||
out["fingerprint"] = tk(n)
|
||||
n = struct.unpack("<H", tk(2))[0]
|
||||
out["cred"] = tk(n)
|
||||
out["trailing"] = data[o:]
|
||||
return out
|
||||
|
||||
|
||||
def forge_cert(
|
||||
p1: bytes,
|
||||
key16: bytes = DEFAULT_KEY16,
|
||||
key_idx: int = 0x20,
|
||||
type_byte: int = 0x0C,
|
||||
) -> bytes:
|
||||
"""P1 -> 二进制证书 [type][key_idx][192B AES 密文]。"""
|
||||
ct = udb_encrypt(key16, p1)
|
||||
assert len(ct) == 192, len(ct)
|
||||
return bytes([type_byte, key_idx]) + ct
|
||||
|
||||
|
||||
def parse_cert(blob: bytes) -> tuple[int, int, bytes]:
|
||||
"""[0x0c][idx][192B] -> (type, idx, ct)。"""
|
||||
assert blob[0] == 0x0C and len(blob) >= 194, "非证书结构"
|
||||
return blob[0], blob[1], blob[2:194]
|
||||
|
||||
|
||||
def decrypt_cert(blob: bytes, key16: bytes = DEFAULT_KEY16) -> bytes:
|
||||
"""解密二进制证书为 P1 明文。"""
|
||||
_, _, ct = parse_cert(blob)
|
||||
return udb_decrypt(key16, ct)
|
||||
Reference in New Issue
Block a user