This repository has been archived on 2026-10-02. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
NeteaseUrl/music_api/crypto.py
T
snowy-yang d600b6a630 refactor: crypto 改用手写轮子的标准替代,去掉重复的函数内 import
- 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 逐条比对确认无文本误伤。
2026-09-26 14:16:05 +08:00

107 lines
4.5 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.
# -*- 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}