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 路由验证通过
This commit is contained in:
snowy-yang committed 2026-10-23 21:55:45 +08:00
1 parent 800871ef6e
commit dfc06bf4de
182 files changed
+26620

No files matched your search

+99
View File
@@ -0,0 +1,99 @@
"""eapi / weapi / linuxapi 加密 —— 移植自 sdk/wy/utils/crypto.ts。
算法与上游 Binaryify/NeteaseCloudMusicApi 的 util/crypto.js 逐字节一致:
* weapi 两层 AES-128-CBC(内层固定 presetKey,外层随机 secretKey),再用 RSA_NO_PADDING 包 secretKey
* linuxapi / eapi 走 AES-128-ECB
* Node 的 createCipheriv 默认 PKCS#7 自动补位,这里手工补齐;ECB 忽略 iv
改这里会让所有 eapi 请求的签名失效,必须与上游保持一致。
"""
from __future__ import annotations
import base64
import hashlib
import json
import os
from typing import Any
from cryptography.hazmat.primitives import padding as _sym_padding
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
IV = b"0102030405060708"
PRESET_KEY = b"0CoJUm6Qyw8W8jud"
LINUXAPI_KEY = b"rFgB&h#%2?^eDg:Q"
EAPI_KEY = b"e82ckenh8dichen8"
BASE62 = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
PUBLIC_KEY = (
"-----BEGIN PUBLIC KEY-----\n"
"MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDgtQn2JZ34ZC28NWYpAUd98iZ37BUrX/aKzmFbt7clFSs6sXqHauqKWqdtLkF2"
"KexO40H1YTX8z2lSgBBOAxLsvaklV8k4cBFK9snQXE9/DDaFt6Rr7iVZMldczhC0JNgTz+SHXT6CBHuX3e9SdB1Ua44oncaTWz7OBG"
"LbCiK45wIDAQAB\n"
"-----END PUBLIC KEY-----\n"
)
def _aes_encrypt(data: bytes, key: bytes, iv: bytes = b"") -> bytes:
"""AES 加密;iv 为空走 ECB,否则 CBC。补位方式与 Node 默认一致(PKCS#7)。"""
padder = _sym_padding.PKCS7(128).padder()
padded = padder.update(data) + padder.finalize()
cipher = Cipher(algorithms.AES(key), modes.CBC(iv) if iv else modes.ECB())
encryptor = cipher.encryptor()
return encryptor.update(padded) + encryptor.finalize()
def _aes_decrypt(data: bytes, key: bytes) -> bytes:
cipher = Cipher(algorithms.AES(key), modes.ECB())
decryptor = cipher.decryptor()
plain = decryptor.update(data) + decryptor.finalize()
unpadder = _sym_padding.PKCS7(128).unpadder()
return unpadder.update(plain) + unpadder.finalize()
def _rsa_encrypt(data: bytes, key: str = PUBLIC_KEY) -> bytes:
"""RSA_NO_PADDING:调用方需自行左补零到模长(Node 版同样是手工 Buffer.alloc 补齐)。
cryptography 不提供 NoPadding(其 RSA 加密强制 PKCS#1 填充),而网易 weapi 的
secretKey 必须裸加密才与上游一致,因此这里直接做 c = m^e mod n。
"""
public_key = serialization.load_pem_public_key(key.encode())
if not isinstance(public_key, rsa.RSAPublicKey):
raise RuntimeError("网易云 RSA 公钥解析失败")
numbers = public_key.public_numbers()
block_size = public_key.key_size // 8
if len(data) > block_size:
raise RuntimeError("网易云 RSA 明文超过模长")
block = b"\x00" * (block_size - len(data)) + data
return pow(int.from_bytes(block, "big"), numbers.e, numbers.n).to_bytes(block_size, "big")
def _js_json(data: Any) -> str:
"""对齐 JS 的 JSON.stringify(紧凑、保留非 ASCII)。"""
return json.dumps(data, ensure_ascii=False, separators=(",", ":"))
def weapi(data: Any) -> dict[str, str]:
text = _js_json(data)
# Node 的 randomBytes 是密码学随机,用 os.urandom 对齐;取模 62 后映射到 base62 字符的 ASCII 码
secret_key = bytes(ord(BASE62[byte % 62]) for byte in os.urandom(16))
inner = base64.b64encode(_aes_encrypt(text.encode(), PRESET_KEY, IV))
params = base64.b64encode(_aes_encrypt(inner, secret_key, IV)).decode()
return {"params": params, "encSecKey": _rsa_encrypt(secret_key[::-1]).hex()}
def linuxapi(data: Any) -> dict[str, str]:
text = _js_json(data)
return {"eparams": _aes_encrypt(text.encode(), LINUXAPI_KEY).hex().upper()}
def eapi(url: str, data: Any) -> dict[str, str]:
text = data if isinstance(data, str) else _js_json(data)
digest = hashlib.md5(f"nobody{url}use{text}md5forencrypt".encode()).hexdigest()
raw = f"{url}-36cd479b6b5-{text}-36cd479b6b5-{digest}"
return {"params": _aes_encrypt(raw.encode(), EAPI_KEY).hex().upper()}
def eapi_decrypt(cipher_buffer: bytes) -> str:
return _aes_decrypt(cipher_buffer, EAPI_KEY).decode("utf-8", errors="replace")