Archived
- hex_digest:`"".join([hex(d)[2:].zfill(2) for d in data])` → `data.hex()`。 前者每字节做一次 hex()+zfill 再拼接,后者是 C 实现;两者输出相同。 - hash_hex_digest:绕道 digest() → hex_digest() 两步,直接 hexdigest()。 - _aes_encrypt / encrypt 里的函数内 import 删除:模块顶部已导入 Cipher/algorithms/modes/padding,函数内又导一遍纯属冗余; base64、secrets 则提到模块顶部(原来每次调用都要走一次 import 查找)。 - PKCS7(128) 保留字面量并加注释:**不要**改写成 algorithms.AES.block_size ——它在类型存根里声明为 property 而非 int,pyright 会报 reportArgumentType (本次实测踩到,门禁拦下)。 等价性验证:encrypt_params 对固定 payload 的输出 sha1 保持 76e3738f8eab61fcfd1145164ba8e1f91d40ee4f 不变;hex_digest/hash_hex_digest 的输出逐字符相同;并做了 docstring 逐条比对确认无文本误伤。
107 lines
4.5 KiB
Python
107 lines
4.5 KiB
Python
# -*- 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}
|