Archived
为什么:单文件 1100~1900 行的巨石模块让「改一处要通读全文」;同时注释里残留 大量已删台账的编号,以及「Flask/FastAPI 双栈迁移期」这类已失实的架构描述, 会误导后来者按不复存在的架构去改代码。 拆分(对外导入路径不变,行为零变更): - music_downloader.py -> music_downloader/(models/downloader/cover/_http/_log/_concurrency) - auth_system.py -> auth_system/(db/security/users/credits/ledger/mail/cards/backup...) - music_api.py -> music_api/(api/client/crypto/qr/cookies/compat/constants/errors) - services/runtime.py -> services/(api_service/_shared/_taskcore/downloads/batch/podcast/ streaming/tasks);services/runtime.py 保留为兼容门面 兼容性做法:各包用 __init__.py 以 __all__ 原样再导出全部符号,既有调用点 (auth_system.xxx / services.xxx / from music_api import Y)零改动;路径常量改为经包属性 动态读取(auth_system.DB_PATH),否则测试的 monkeypatch 会静默失效并写到生产库。 修复 9 处代码损伤(上一轮注释清理把 `()` 当「空括号残留」删掉所致;用「与拆分前逐字 等价」校验逐一定位,静态检查只会报出其中 4 处): - core/config.py ).strip() or '*' - core/state.py threading.Lock() x2 - qr_login.py datetime.now() / .strip() - music_downloader/downloader.py .lower() - services/podcast.py set() / time.time() x2 清理与删除: - 删除 部署文档.md / 功能介绍.md / README_私有包警告.txt - 清理指向已删台账与文档的过时编号注释;把「迁移期 / 两栈 / 双框架 / 已迁移」等 失实描述改写为现状描述(保留 `<-> Flask /path` 映射表等仍有价值的契约来源说明) - _mkpkg.py:公开包剔除清单去掉已不存在的文档名;私有包警告改为自包含 - pyproject.toml:补 pythonpath = ["."],使裸跑 pytest 不再 import core 失败 验证:ruff 全通过;pyright 0 errors / 0 warnings(102 文件);pytest 144 passed; 真实 uvicorn 冒烟 6 条路由符合预期且生产 data/ 指纹未变;忽略注释的逐字等价性校验通过。
106 lines
4.4 KiB
Python
106 lines
4.4 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""weapi 加解密(AES + RSA)。
|
|
|
|
(自 music_api.py 拆出,行为与原实现一致。)
|
|
"""
|
|
|
|
import json
|
|
import urllib.parse
|
|
from hashlib import md5
|
|
from typing import Any
|
|
|
|
from cryptography.hazmat.primitives import padding
|
|
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
|
|
|
from .constants import (
|
|
APIConstants,
|
|
)
|
|
|
|
|
|
class CryptoUtils:
|
|
"""加密工具类"""
|
|
@staticmethod
|
|
def hex_digest(data: bytes) -> str:
|
|
"""将字节数据转换为十六进制字符串"""
|
|
return "".join([hex(d)[2:].zfill(2) for d in data])
|
|
|
|
@staticmethod
|
|
def hash_digest(text: str) -> bytes:
|
|
"""计算MD5哈希值"""
|
|
return md5(text.encode("utf-8")).digest()
|
|
|
|
@staticmethod
|
|
def hash_hex_digest(text: str) -> str:
|
|
"""计算MD5哈希值并转换为十六进制字符串"""
|
|
return CryptoUtils.hex_digest(CryptoUtils.hash_digest(text))
|
|
|
|
@staticmethod
|
|
def encrypt_params(url: str, payload: dict[str, Any]) -> str:
|
|
"""加密请求参数"""
|
|
url_path = urllib.parse.urlparse(url).path.replace("/eapi/", "/api/")
|
|
digest = CryptoUtils.hash_hex_digest(f"nobody{url_path}use{json.dumps(payload)}md5forencrypt")
|
|
params = f"{url_path}-36cd479b6b5-{json.dumps(payload)}-36cd479b6b5-{digest}"
|
|
|
|
# AES加密
|
|
padder = padding.PKCS7(algorithms.AES(APIConstants.AES_KEY).block_size).padder()
|
|
padded_data = padder.update(params.encode()) + padder.finalize()
|
|
cipher = Cipher(algorithms.AES(APIConstants.AES_KEY), modes.ECB())
|
|
encryptor = cipher.encryptor()
|
|
enc = encryptor.update(padded_data) + encryptor.finalize()
|
|
|
|
return CryptoUtils.hex_digest(enc)
|
|
|
|
|
|
class WeapiCrypto:
|
|
"""网易云 weapi 接口加密(AES-CBC 双重加密 + RSA 公钥加密)
|
|
|
|
用于发送/回复评论等前端写操作接口(/weapi/comment 等)。
|
|
"""
|
|
|
|
AES_KEY = b"0CoJUm6Qyw8W8jud"
|
|
AES_IV = b"0102030405060708"
|
|
RSA_MODULUS = "00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7"
|
|
RSA_EXPONENT = "010001"
|
|
# 官方随机 secKey 字符集(官方 JS 与主流逆向实现一致)
|
|
SEC_KEY_CHARSET = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
|
|
|
|
@staticmethod
|
|
def _aes_encrypt(text: bytes, key: bytes) -> bytes:
|
|
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
|
from cryptography.hazmat.primitives.padding import PKCS7
|
|
padder = PKCS7(128).padder()
|
|
padded = padder.update(text) + padder.finalize()
|
|
cipher = Cipher(algorithms.AES(key), modes.CBC(WeapiCrypto.AES_IV))
|
|
encryptor = cipher.encryptor()
|
|
return encryptor.update(padded) + encryptor.finalize()
|
|
|
|
@staticmethod
|
|
def encrypt(payload: dict[str, Any]) -> dict[str, str]:
|
|
"""将请求体加密为 weapi 的 params/encSecKey。
|
|
按官方协议重写——secKey 取官方字符集随机 16 位,
|
|
encSecKey = secKey 反转后做 textbook RSA(无填充)的 256 位 hex。
|
|
原实现用 os.urandom + PKCS1v15 且未反转,服务端按 textbook RSA 解出的
|
|
AES key 与 params 第二层不一致,评论等 weapi 写接口疑似从未成功过。
|
|
官方协议第二层加密的输入是第一层密文的
|
|
**base64 字符串**(两次 AES 各自输出 base64)——93 条版本漏了中间那次
|
|
base64,服务端解 params 第一层得到二进制、b64 解码必失败。"""
|
|
import base64
|
|
import secrets as _secrets
|
|
|
|
text = json.dumps(payload).encode("utf-8")
|
|
secret_key = ''.join(_secrets.choice(WeapiCrypto.SEC_KEY_CHARSET) for _ in range(16))
|
|
|
|
first = base64.b64encode(
|
|
WeapiCrypto._aes_encrypt(text, WeapiCrypto.AES_KEY)
|
|
).decode("utf-8")
|
|
params = base64.b64encode(
|
|
WeapiCrypto._aes_encrypt(first.encode("utf-8"), secret_key.encode('utf-8'))
|
|
).decode("utf-8")
|
|
|
|
n = int(WeapiCrypto.RSA_MODULUS, 16)
|
|
e = int(WeapiCrypto.RSA_EXPONENT, 16)
|
|
reversed_hex = secret_key[::-1].encode('utf-8').hex()
|
|
enc_sec_key = format(pow(int(reversed_hex, 16), e, n), '0>256x')
|
|
|
|
return {"params": params, "encSecKey": enc_sec_key}
|