# -*- coding: utf-8 -*- """weapi 加解密(AES + RSA)。 (自 music_api.py 拆出,行为与原实现一致。) """ import base64 import json import secrets 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 data.hex() @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 md5(text.encode("utf-8")).hexdigest() @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: """单层 AES-CBC + PKCS7 填充(密钥/IV 由官方 weapi 协议固定)。""" # PKCS7 以**比特**为单位:AES 分组固定 128 比特(=16 字节)。 # 不要写 algorithms.AES.block_size——它在类型存根里声明为 property 而非 int, # pyright 会报 reportArgumentType。 padder = padding.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 解码必失败。""" 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}