Files
MusicServer/app/sdk/auth/kg.py
T
snowy-yang dfc06bf4de feat(python): 完成 Python 全量迁移——剩余平台、登录通道与公共层修复
- 新增 5sing/apple/xm/me/qt/lr 平台包与取流实现(含 xm-sign、MissEvan 签名、蜻蜓 HMAC、Apple developer token)
- 新增 wy/kg/tx/lr 登录:扫码、手机号、账密(kg 20028 风控指纹)、cookie 绑定、kg/tx 续期
- request 层:保留多值 Set-Cookie;JSON body 紧凑化(修复 kg 网关验签);直连不经系统代理(对齐 Node)
- 登录路由统一 Body(embed=True),修复扫码轮询 422;lifespan 接入登录续期调度
- ruff/format/pyright 全绿;14 平台真实联网与 HTTP 路由验证通过
2026-10-23 21:55:45 +08:00

687 lines
26 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.
"""酷狗登录(对应 sdk/auth/kg.ts)—— token 绑定 / 扫码 / 短信验证码 / 账号密码。
与 MakcRe/KuGouMusicApi 协议对齐:
* 扫码走 login-user.kugou.com 的 web 签名通道;
* 短信走 android 签名 + AES/RSA;
* 账密走 /v9/login_by_pwd,触发 20028 风控时返回验证码挑战(ssa-code 事件 +
sid/edt 行为指纹,指纹由 kg_simulate.py 服务端生成,无需浏览器 WASM);
* token 也可从酷狗 App 抓包获取(login.token)后经 /login/kg 绑定。
设备指纹(guid/mid/dfid)与取流共用 platforms/kg/util.py 的实现(落盘 _kg_device)。
"""
from __future__ import annotations
import base64
import json
import time
from typing import Any
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from app.http.api_error import ApiError
from app.sdk.auth.kg_simulate import generate_simulate
from app.sdk.auth_store import get_credentials, set_credentials
from app.sdk.common.request import http_fetch
from app.sdk.common.utils import as_dict, to_md5
from app.sdk.platforms.kg.util import (
ANDROID_SALT,
WEB_SALT,
android_sign,
encode_uri_component,
get_device,
pkcs7_pad,
pkcs7_unpad,
random_string,
)
APPID = 1005
CLIENTVER = 11430
#: 登录专用域按参考实现固定 clientver 20489(qr/短信接口实测通过的版本)
LOGIN_CLIENTVER = 20489
SRC_APPID = 2919
QR_URL_PREFIX = "https://h5.kugou.com/apps/loginQRCode/html/index.html"
PUBLIC_KEY = """-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDIAG7QOELSYoIJvTFJhMpe1s/gbjDJX51HBNnEl5HXqTW6lQ7LC8jr9fWZTwusknp+sVGzwd40MwP6U5yDE27M/X1+UR4tvOGOqp94TJtQ1EPnWGWXngpeIW5GxoQGao1rmYWAu6oi1z9XkChrsUdC6DJE5E221wf/4WLFxwAtRQIDAQAB
-----END PUBLIC KEY-----"""
#: 固定串派生 token 续期密钥(AES-256-CBC key / iv)
AES_KEY_SEED = "90b8382a1bb4ccdcf063102053fd75b8"
AES_IV = "f063102053fd75b8"
# login_by_pwd 的设备校验常量(与参考实现一致,疑似风控指纹固定值)
PWD_T1 = (
"562a6f12a6e803453647d16a08f5f0c2ff7eee692cba2ab74cc4c8ab47fc467561a7c6b586ce7dc46a63613b246737c0"
"3a1dc8f8d162d8ce1d2c71893d19f1d4b797685a4c6d3d81341cbde65e488c4829a9b4d42ef2df470eb102979fa5ad"
"cdd9b4eecfea8b909ff7599abeb49867640f10c3c70fc444effca9d15db44a9a6c907731e2bb0f22cd9b3536380169"
"995693e5f0e2424e3378097d3813186e3fe96bbe7023808a0981b4e2b6135a76faac"
)
PWD_T2 = (
"31c4daf4cf480169ccea1cb7d4a209295865a9d2b788510301694db229b87807469ea0d41b4d4b9173c2151da7294a"
"eebfc9738df154bbdf11a4e117bb5dff6a3af8ce5ce333e681c1f29a44038f27567d58992eb81283e080778ac77db14"
"00fdf49b7cf7e26be2e5af4da7830cc3be4"
)
PWD_T3 = "MCwwLDAsMCwwLDAsMCwwLDA="
PWD_CLIENTVER = 11440
VERIFY_VTYPES = {
16: "QQ 绑定验证",
18: "微信绑定验证",
22: "活体认证",
23: "滑块/图形验证码",
32: "手机短信验证码",
34: "人脸核身",
}
VERIFY_URL_TYPES = {
"KGCodeGT": "极验",
"KGCodeTX": "腾讯",
"KGCodeKG": "酷狗旋转",
"KGCodeKG2": "酷狗滑块",
"KGCodeSM": "数美",
"KGCodeYD": "易盾",
}
_ANDROID_UA = "Android15-1070-11083-46-0-DiscoveryDRADProtocol-wifi"
_public_key = None
def _load_public_key() -> Any:
global _public_key
if _public_key is None:
_public_key = serialization.load_pem_public_key(PUBLIC_KEY.encode())
return _public_key
def _js_json(data: Any) -> str:
return json.dumps(data, ensure_ascii=False, separators=(",", ":"))
def _rsa_pkcs1_hex(data: dict[str, Any]) -> str:
"""RSA PKCS#1 v1.5 加密(register_dev 的 p / 登录的 pk 之外的默认形态)。"""
return _load_public_key().encrypt(_js_json(data).encode(), padding.PKCS1v15()).hex()
def _rsa_no_padding_hex(data: dict[str, Any], *, pad_left: bool) -> str:
"""RSA 裸填充加密(128 字节;verify 左补零,登录/续期右补零,与参考实现一致)。"""
payload = _js_json(data).encode()
if len(payload) > 128:
raise RuntimeError("RSA 负载超长")
filler = b"\x00" * (128 - len(payload))
padded = filler + payload if pad_left else payload + filler
numbers = _load_public_key().public_numbers()
cipher = pow(int.from_bytes(padded, "big"), numbers.e, numbers.n).to_bytes(128, "big")
return cipher.hex()
def _aes_cbc_encrypt_hex(data: str, key: str, iv: str) -> str:
cipher = Cipher(algorithms.AES(key.encode()), modes.CBC(iv.encode()))
encryptor = cipher.encryptor()
return (encryptor.update(pkcs7_pad(data.encode())) + encryptor.finalize()).hex()
def _aes_encrypt_random(data: str | dict[str, Any]) -> dict[str, str]:
"""随机密钥 AES-256-CBC:key=md5(rand16),iv=key[16:32]。"""
temp_key = random_string(16)
key_hex = to_md5(temp_key)
text = data if isinstance(data, str) else _js_json(data)
return {"str": _aes_cbc_encrypt_hex(text, key_hex, key_hex[16:32]), "key": temp_key}
def _aes_decrypt_hex(hex_text: str, temp_key: str) -> Any:
key_hex = to_md5(temp_key)
decryptor = Cipher(algorithms.AES(key_hex.encode()), modes.CBC(key_hex[16:32].encode())).decryptor()
out = decryptor.update(bytes.fromhex(hex_text)) + decryptor.finalize()
text = pkcs7_unpad(out).decode("utf-8", errors="replace")
try:
return json.loads(text)
except ValueError:
return text
def _playlist_aes_encrypt(data: dict[str, Any]) -> dict[str, str]:
key = random_string(6)
key_hex = to_md5(key)
cipher = Cipher(algorithms.AES(key_hex[:16].encode()), modes.CBC(key_hex[16:32].encode()))
encryptor = cipher.encryptor()
payload = encryptor.update(pkcs7_pad(_js_json(data).encode())) + encryptor.finalize()
return {"key": key, "str": base64.b64encode(payload).decode()}
def _playlist_aes_decrypt(raw_base64: str, key: str) -> Any:
key_hex = to_md5(key)
decryptor = Cipher(algorithms.AES(key_hex[:16].encode()), modes.CBC(key_hex[16:32].encode())).decryptor()
out = decryptor.update(base64.b64decode(raw_base64)) + decryptor.finalize()
text = pkcs7_unpad(out).decode("utf-8", errors="replace")
try:
return json.loads(text)
except ValueError:
return text
def _int_or_zero(value: Any) -> int:
try:
return int(value)
except (TypeError, ValueError):
return 0
def _gateway_headers(params: dict[str, Any], router: str | None, extra: dict[str, str] | None) -> dict[str, str]:
headers = {
"dfid": str(params["dfid"]),
"mid": str(params["mid"]),
"clienttime": str(params["clienttime"]),
"kg-rc": "1",
"kg-thash": "5d816a0",
"kg-rec": "1",
"kg-rf": "B9EDA08A64250DEFFBCADDEE00F8F25F",
"User-Agent": _ANDROID_UA,
}
if router:
headers["x-router"] = router
if extra:
headers.update(extra)
return headers
async def gateway_request(
url: str,
*,
params: dict[str, Any] | None = None,
data: Any = None,
router: str | None = None,
host: str = "https://gateway.kugou.com",
headers: dict[str, str] | None = None,
with_headers: bool = False,
) -> Any:
"""网关移动端签名请求(默认 gateway.kugou.com + x-router,可 host 直连登录专用域)。"""
dev = get_device()
all_params: dict[str, Any] = {
"dfid": dev["dfid"],
"mid": dev["mid"],
"uuid": "-",
"appid": APPID,
"clientver": CLIENTVER,
"clienttime": int(time.time()),
}
if params:
all_params.update(params)
signature = android_sign(all_params, _js_json(data) if data is not None else "")
query = "&".join(f"{key}={encode_uri_component(value)}" for key, value in all_params.items())
kwargs: dict[str, Any] = {
"method": "POST",
"headers": _gateway_headers(all_params, router, headers),
"timeout": 15_000,
}
if data is not None:
kwargs["body"] = data
resp = await http_fetch(f"{host}{url}?{query}&signature={signature}", **kwargs)
body = as_dict(resp.body)
if with_headers:
return body, resp.headers
return body
def _web_sign(params: dict[str, Any]) -> str:
items = sorted(f"{key}={value}" for key, value in params.items())
return to_md5(f"{WEB_SALT}{''.join(items)}{WEB_SALT}")
async def _login_user_get(url: str, extra_params: dict[str, Any] | None = None) -> dict[str, Any]:
"""login-user.kugou.com GET 请求(web 签名,与取流共用同一设备指纹)。"""
dev = get_device()
params: dict[str, Any] = {
"dfid": dev["dfid"],
"mid": dev["mid"],
"uuid": "-",
"appid": APPID,
"clientver": LOGIN_CLIENTVER,
"clienttime": int(time.time()),
}
if extra_params:
params.update(extra_params)
query = "&".join(f"{key}={encode_uri_component(value)}" for key, value in params.items())
resp = await http_fetch(
f"https://login-user.kugou.com{url}?{query}&signature={_web_sign(params)}",
headers=_gateway_headers(params, None, None),
timeout=15_000,
)
body = as_dict(resp.body)
if body.get("status") != 1:
raise ApiError(502, str(body.get("error") or "酷狗登录服务请求失败"))
return body
async def verify_token(token: str, userid: str | int) -> dict[str, Any]:
"""验证 token 并返回账号资料(usercenter /v3/get_my_info,p = RSA 裸填充左补零)。"""
unix_sec = int(time.time())
p = _rsa_no_padding_hex({"token": token, "clienttime": unix_sec}, pad_left=True).upper()
body = await gateway_request(
"/v3/get_my_info",
router="usercenter.kugou.com",
params={"plat": 1},
data={"visit_time": unix_sec, "usertype": 1, "p": p, "userid": _int_or_zero(userid)},
)
data = as_dict(body.get("data"))
if body.get("status") != 1 or not data:
raise ApiError(401, str(body.get("error") or "token 无效或已过期"))
return {
"userid": data.get("userid") or _int_or_zero(userid),
"nickname": data.get("nickname") or data.get("username") or None,
"vip": bool(data["vip"]) if data.get("vip") else None,
}
async def login_by_token(token: str, userid: str | int) -> dict[str, Any]:
"""绑定并验证 kg token。"""
profile = await verify_token(token, userid)
set_credentials("kg", {"token": token, "userid": profile["userid"], "nickname": profile["nickname"]})
return profile
async def _apply_kg_login(token: str, userid: Any) -> dict[str, Any]:
"""保存登录凭据并拉取账号资料(扫码/短信登录共用;资料校验失败不阻断登录)。"""
try:
profile = await verify_token(token, userid)
set_credentials("kg", {"token": token, "userid": profile["userid"], "nickname": profile["nickname"]})
return profile
except Exception: # noqa: BLE001 - 资料校验失败不影响登录本身
uid = _int_or_zero(userid)
set_credentials("kg", {"token": token, "userid": uid})
return {"userid": uid, "nickname": None, "vip": None}
# ---- 扫码登录 ----
async def qr_key() -> dict[str, Any]:
"""创建扫码登录 key 与二维码(qrimg 为可直接展示的 data URL)。"""
body = await _login_user_get(
"/v2/qrcode",
{
"appid": 1001,
"type": 1,
"plat": 4,
"qrcode_txt": f"{QR_URL_PREFIX}?appid={APPID}&",
"srcappid": SRC_APPID,
},
)
data = as_dict(body.get("data"))
key = data.get("qrcode")
if not key:
raise RuntimeError("获取二维码 key 失败")
qrurl = f"{QR_URL_PREFIX}?qrcode={key}"
qrimg = str(data.get("qrcode_img") or "")
if not qrimg:
from app.sdk.common.qrcode import to_data_url
try:
qrimg = to_data_url(qrurl)
except Exception: # noqa: BLE001 - 二维码渲染失败不影响 key/url 返回
qrimg = ""
return {"key": key, "qrurl": qrurl, "qrimg": qrimg}
async def qr_check(key: str) -> dict[str, Any]:
"""轮询扫码状态(酷狗原生 0/1/2/4 → 800/801/802/803)。"""
body = await _login_user_get(
"/v2/get_userinfo_qrcode",
{"plat": 4, "appid": APPID, "srcappid": SRC_APPID, "qrcode": key},
)
data = as_dict(body.get("data"))
qr_status = data.get("status")
if qr_status == 4:
token = data.get("token")
if not token:
raise RuntimeError("登录响应中缺少 token")
return {"code": 803, "message": "登录成功", "profile": await _apply_kg_login(token, data.get("userid"))}
mapping: dict[Any, tuple[int, str]] = {0: (800, "二维码已过期"), 1: (801, "等待扫码"), 2: (802, "已扫码,待确认")}
code, message = mapping.get(qr_status, (qr_status, f"未知状态 {qr_status}"))
return {"code": code, "message": message}
# ---- 手机验证码登录 ----
def _sign_login_key(clienttime_ms: int) -> str:
return to_md5(f"{APPID}{ANDROID_SALT}{LOGIN_CLIENTVER}{clienttime_ms}")
async def send_captcha(mobile: str) -> dict[str, Any]:
"""发送短信验证码(businessid 5 = 登录场景)。"""
body = await gateway_request(
"/v7/send_mobile_code",
router="login.user.kugou.com",
params={"clientver": LOGIN_CLIENTVER},
data={"businessid": 5, "mobile": str(mobile), "plat": 3},
)
if body.get("status") != 1:
raise ApiError(502, str(body.get("error") or body.get("msg") or "验证码发送失败"))
return {"sent": True}
async def cellphone_login(mobile: str, code: str) -> dict[str, Any]:
"""手机验证码登录(mobile + code,成功后自动保存凭据并返回账号资料)。"""
clienttime_ms = int(time.time() * 1000)
enc = _aes_encrypt_random({"mobile": str(mobile), "code": str(code)})
body = await gateway_request(
"/v7/login_by_verifycode",
host="https://loginserviceretry.kugou.com",
params={"clientver": LOGIN_CLIENTVER},
data={
"plat": 1,
"support_multi": 1,
"t1": 0,
"t2": 0,
"clienttime_ms": clienttime_ms,
"mobile": f"{str(mobile)[:2]}*****{str(mobile)[10:11]}",
"key": _sign_login_key(clienttime_ms),
"pk": _rsa_no_padding_hex({"clienttime_ms": clienttime_ms, "key": enc["key"]}, pad_left=False).upper(),
"params": enc["str"],
"t3": "MCwwLDAsMCwwLDAsMCwwLDA=",
},
headers={"User-Agent": "Android16-1070-11440-130-0-LOGIN-wifi", "support-calm": "1"},
)
if body.get("status") != 1:
raise ApiError(401, str(body.get("error") or f"登录失败(code {body.get('error_code', '未知')})"))
data = as_dict(body.get("data"))
decrypted = _aes_decrypt_hex(str(data["secu_params"]), enc["key"]) if data.get("secu_params") else None
token = decrypted.get("token") if isinstance(decrypted, dict) else decrypted
if not token:
raise RuntimeError("登录响应解密失败")
userid = decrypted.get("userid") if isinstance(decrypted, dict) else None
return await _apply_kg_login(str(token), userid or data.get("userid") or 0)
# ---- token 自动续期 ----
async def refresh_login() -> dict[str, Any]:
"""刷新 kg token(login_by_token),成功后写回凭据存储。"""
cred = get_credentials("kg") or {}
if not cred.get("token") or str(cred.get("userid") or "") in ("", "0"):
return {"skipped": True}
clienttime = int(time.time())
p3 = _aes_cbc_encrypt_hex(
_js_json({"clienttime": clienttime, "token": cred["token"]}), to_md5(AES_KEY_SEED), AES_IV
)
enc = _aes_encrypt_random({})
pk = _rsa_no_padding_hex({"clienttime_ms": clienttime * 1000, "key": enc["key"]}, pad_left=False)
body = await gateway_request(
"/v5/login_by_token",
router="login.user.kugou.com",
params={"clientver": 20349},
data={
"p3": p3,
"params": enc["str"],
"userid": _int_or_zero(cred.get("userid")),
"need_toneinfo": 1,
"clienttime_ms": clienttime * 1000,
"dfid": "-",
"dev": "SDY-AN00",
"plat": 1,
"pk": pk,
"t1": "0",
"gitversion": "a23c277",
"t2": "0",
"t3": "MCwwLDEsMSwwLDYsMSw2LDA=",
},
headers={
"kg-thash": "6a6a1ba",
"User-Agent": "Android12-AndroidPhone-20349-201-0-ting#958959317/661004247-LOGIN-wifi",
"support-calm": "1",
},
)
data = as_dict(body.get("data"))
if body.get("error_code") != 0 or not data.get("secu_params"):
error = body.get("error")
raise RuntimeError(f"刷新失败(code {body.get('error_code')}{f': {error}' if error else ''})")
decrypted = _aes_decrypt_hex(str(data["secu_params"]), enc["key"])
if not isinstance(decrypted, dict) or not decrypted.get("token"):
raise RuntimeError("刷新响应解密失败")
set_credentials("kg", {"token": decrypted["token"]})
return {"refreshed": True}
# ---- 设备 dfid 正式注册 ----
async def register_dfid() -> dict[str, Any]:
"""注册正式 dfid(幂等:已注册直接返回;失败抛错由调用方决定是否容忍)。
随机 dfid 匿名取流易撞 20028 验证;注册后的 dfid 是官方客户端形态,可显著降低风控。
协议:设备画像 → AES-128-CBC(随机 6 位 key) → base64 作 body;
p = RSA-PKCS1({aes:key, uid, token});响应为二进制密文,按 base64 解码后同密钥解密。
"""
dev = get_device()
if dev.get("dfidRegistered"):
return dev
cred = get_credentials("kg") or {}
userid = _int_or_zero(cred.get("userid"))
device_map = {
"availableRamSize": 4983533568,
"availableRomSize": 48114719,
"availableSDSize": 48114717,
"basebandVer": "",
"batteryLevel": 100,
"batteryStatus": 3,
"brand": "Redmi",
"buildSerial": "unknown",
"device": "marble",
"imei": dev["guid"],
"imsi": "",
"manufacturer": "Xiaomi",
"uuid": dev["guid"],
"accelerometer": False,
"accelerometerValue": "",
"gravity": False,
"gravityValue": "",
"gyroscope": False,
"gyroscopeValue": "",
"light": False,
"lightValue": "",
"magnetic": False,
"magneticValue": "",
"orientation": False,
"orientationValue": "",
"pressure": False,
"pressureValue": "",
"step_counter": False,
"step_counterValue": "",
"temperature": False,
"temperatureValue": "",
}
enc = _playlist_aes_encrypt(device_map)
p = _rsa_pkcs1_hex({"aes": enc["key"], "uid": userid, "token": cred.get("token") or ""})
params: dict[str, Any] = {
"dfid": dev["dfid"],
"mid": dev["mid"],
"uuid": "-",
"appid": APPID,
"clientver": CLIENTVER,
"clienttime": int(time.time()),
"part": 1,
"platid": 1,
"p": p,
}
params["signature"] = android_sign(params, enc["str"])
query = "&".join(f"{key}={encode_uri_component(value)}" for key, value in params.items())
resp = await http_fetch(
f"https://userservice.kugou.com/risk/v2/r_register_dev?{query}",
method="POST",
headers={
"dfid": str(dev["dfid"]),
"mid": str(dev["mid"]),
"clienttime": str(params["clienttime"]),
"User-Agent": _ANDROID_UA,
},
body=enc["str"],
timeout=15_000,
)
raw_b64 = (
base64.b64encode(resp.raw).decode()
if resp.raw
else base64.b64encode(_js_json(resp.body or "").encode()).decode()
)
body = _playlist_aes_decrypt(raw_b64, enc["key"])
body = as_dict(body)
data = as_dict(body.get("data"))
if body.get("status") != 1 or not data.get("dfid"):
raise RuntimeError(f"dfid 注册失败(status {body.get('status', '未知')} {body.get('error', '')})".strip())
updated = {**dev, "dfid": str(data["dfid"]), "dfidRegistered": True}
set_credentials("_kg_device", updated)
return updated
# ---- 账号密码登录 + 20028 验证码挑战 ----
async def get_verify_info(eventid: str) -> dict[str, Any]:
"""查询验证事件详情(/verifyservice/v3/get_verify_info)。"""
cred = get_credentials("kg") or {}
body = await gateway_request(
"/verifyservice/v3/get_verify_info",
data={
"eventid": eventid,
"userid": _int_or_zero(cred.get("userid")),
"platid": 2,
"rtype": 1,
"wasm": 1,
"i": "",
"sid": "",
"edt": "",
},
)
if body.get("status") != 1 and body.get("error_code") != 0:
raise RuntimeError(str(body.get("error") or f"查询验证信息失败(code {body.get('error_code', '未知')})"))
data = body.get("data")
return data if isinstance(data, dict) else {}
async def verify_user_info(
eventid: str,
v_type: int,
verifycode: str,
sid: str | None = None,
edt: str | None = None,
) -> dict[str, Any]:
"""提交验证结果(/v4/verify_user_info,verifycode 为 KGCodeXX|{json} 或短信码)。"""
dev = get_device()
cred = get_credentials("kg") or {}
userid = _int_or_zero(cred.get("userid"))
fingerprint = {"sid": sid, "edt": edt} if sid and edt else generate_simulate(dev["mid"], userid, dev["dfid"])
data_map: dict[str, Any] = {
"eventid": eventid,
"userid": userid,
"platid": 2,
"v_type": _int_or_zero(v_type) or 23,
"wasm": 1,
"i": "",
"sid": fingerprint["sid"],
"edt": fingerprint["edt"],
}
enc = _aes_encrypt_random({"code": verifycode} if data_map["v_type"] == 32 else {})
if data_map["v_type"] == 32:
# 手机短信验证码:明文 code + 密文 params 均携带
data_map.update({"code": verifycode, "verifycode": verifycode})
data_map["pk"] = _rsa_no_padding_hex({"key": enc["key"]}, pad_left=False)
else:
# 滑块等图形验证码:KGCodeXX|{ticket,...} 明文上送
data_map.update({"verifycode": verifycode})
data_map["pk"] = _rsa_no_padding_hex({"key": enc["key"]}, pad_left=False)
data_map["params"] = enc["str"]
body = await gateway_request(
"/v4/verify_user_info",
host="https://verifyservice.kugou.com",
params={"clientver": 11510},
data=data_map,
)
if body.get("status") != 1:
raise ApiError(401, str(body.get("error") or f"验证提交失败(code {body.get('error_code', '未知')})"))
return {"verified": True}
async def password_login(username: str, password: str, code: str | None = None) -> dict[str, Any]:
"""账号密码登录(/v9/login_by_pwd);命中 20028 风控返回验证码挑战。"""
# 账密登录风控最敏感,先确保 dfid 已正式注册(失败容忍,随机 dfid 继续尝试)
try:
await register_dfid()
except Exception: # noqa: BLE001 - 注册失败不阻断登录
pass
clienttime_ms = int(time.time() * 1000)
enc = _aes_encrypt_random({"pwd": str(password), "code": str(code or ""), "clienttime_ms": clienttime_ms})
body, headers = await gateway_request(
"/v9/login_by_pwd",
router="login.user.kugou.com",
params={"clientver": PWD_CLIENTVER},
data={
"plat": 1,
"support_multi": 1,
"clienttime_ms": clienttime_ms,
"t1": PWD_T1,
"t2": PWD_T2,
"t3": PWD_T3,
"dev": "SDY-AN00",
"username": str(username),
"params": enc["str"],
"pk": _rsa_no_padding_hex({"clienttime_ms": clienttime_ms, "key": enc["key"]}, pad_left=False).upper(),
},
headers={"User-Agent": f"Android16-1070-{PWD_CLIENTVER}-130-0-LOGIN-wifi", "support-calm": "1"},
with_headers=True,
)
data = as_dict(body.get("data"))
if body.get("status") == 1 and data.get("secu_params"):
decrypted = _aes_decrypt_hex(str(data["secu_params"]), enc["key"])
token = decrypted.get("token") if isinstance(decrypted, dict) else decrypted
if not token:
raise RuntimeError("登录响应解密失败")
userid = decrypted.get("userid") if isinstance(decrypted, dict) else None
return await _apply_kg_login(str(token), userid or data.get("userid") or 0)
if body.get("error_code") == 20028:
eventid = str(headers.get("ssa-code") or body.get("ssaCode") or "")
if not eventid:
raise RuntimeError("触发风控验证但未返回事件 id(ssa-code),请改用扫码或短信登录")
dev = get_device()
cred = get_credentials("kg") or {}
fingerprint = generate_simulate(dev["mid"], _int_or_zero(cred.get("userid")), dev["dfid"])
try:
info = await get_verify_info(eventid)
except Exception: # noqa: BLE001 - 挑战详情失败时按默认类型返回
info = {}
v_type = _int_or_zero(info.get("v_type")) or 23
url = str(info.get("url") or "")
provider = next((key for key in VERIFY_URL_TYPES if key in url), "")
challenge: dict[str, Any] = {
"eventid": eventid,
"v_type": v_type,
"vTypeLabel": VERIFY_VTYPES.get(v_type, f"未知类型 {v_type}"),
"url": url or None,
"provider": VERIFY_URL_TYPES[provider] if provider else None,
"txappid": info.get("txappid"),
"sessionid": info.get("sessionid"),
"sid": fingerprint["sid"],
"edt": fingerprint["edt"],
}
return challenge
raise ApiError(401, str(body.get("error") or f"登录失败(code {body.get('error_code', '未知')})"))
async def status() -> dict[str, Any] | None:
"""当前 kg 登录资料(未登录返回 None)。"""
cred = get_credentials("kg") or {}
if not cred.get("token"):
return None
return {"userid": cred.get("userid"), "nickname": cred.get("nickname")}