Skip to content

新版本客户端 xeapi 支持#163

Draft
1254qwer wants to merge 2 commits into
YUCLing:mainfrom
1254qwer:feat/xeapi-support
Draft

新版本客户端 xeapi 支持#163
1254qwer wants to merge 2 commits into
YUCLing:mainfrom
1254qwer:feat/xeapi-support

Conversation

@1254qwer

Copy link
Copy Markdown
Contributor

新版客户端在 3.1.35 引入了 xeapi 这种新的 API 加密形式,目前正在 AB test,我在今天(2026-06-12)收到的更新

最新版本的 version.json
{
  "version": "3.1.35",
  "build": "205293",
  "commit": "0a56446",
  "downloadUrl": "https://d8.music.126.net/dmusic2/NeteaseCloudMusic_Music_official_3.1.35.205293_64.exe"
}

本 pr 尝试完成对新的 xeapi 请求方式的支持,防止新版用户在进入 AB test 组后请求失败。

目前在本地测试正常,常见接口均可正常 xeapi 请求

如何强行进行测试

我是尝试在 src/preload.ts 末尾处做 hook 来模拟进组之后的状态

    // TEMP_FORCE_XEAPI: local business testing only. Remove before PR.
    const root = window as unknown as Record<string, unknown>;
    if (!root.__openOrpheusTempForceXeapi) {
      root.__openOrpheusTempForceXeapi = true;

      const getWebpackRequire = () => {
        const cachedRequire = root.__openOrpheusWebpackRequire;
        if (typeof cachedRequire === "function") return cachedRequire;

        const webpackJsonp = root.webpackJsonp as
          | {
              push: (chunk: unknown[]) => void;
            }
          | undefined;
        if (!webpackJsonp) return null;

        let webpackRequire: unknown = null;
        try {
          webpackJsonp.push([
            [99998],
            {
              99998: (
                _module: unknown,
                _exports: unknown,
                require: unknown
              ) => {
                webpackRequire = require;
              },
            },
            [[99998]],
          ]);
        } catch {
          return null;
        }

        if (typeof webpackRequire === "function") {
          root.__openOrpheusWebpackRequire = webpackRequire;
          return webpackRequire;
        }
        return null;
      };

      const forceXeapi = async () => {
        const webpackRequire = getWebpackRequire();
        if (!webpackRequire) return false;

        try {
          const requireModule = webpackRequire as (id: number) => unknown;
          const module236 = requireModule(236) as Record<string, unknown>;
          const aegis = (module236.a ?? module236.default ?? module236) as
            | Record<string, unknown>
            | undefined;
          if (!aegis) return false;

          const appConf = root.APP_CONF as Record<string, unknown> | undefined;
          if (appConf) {
            appConf.encrypt = true;
            if (!appConf.clientSign) {
              appConf.clientSign = "open-orpheus-temp-force-xeapi";
            }
          }

          const init = aegis.init;
          if (typeof init === "function" && !aegis.__openOrpheusInitCalled) {
            aegis.__openOrpheusInitCalled = true;
            await init.call(aegis, {
              aegisUpdateIntervalMinute: 1,
              publicKeyUpdateIntervalSecond: 5,
              probeIntervalSecond: 60,
              probeIntervalSecondMax: 300,
              encryptDegradeThreshold: 999999,
              encryptDegradeTimeWindowSecond: 60,
            });
          }

          const enableEncrypt = aegis.enableEncrypt;
          if (typeof enableEncrypt === "function") {
            enableEncrypt.call(aegis);
          }

          aegis.isEncryptActive = () => true;
          aegis.disableEncrypt = function (this: { encryptEnabled?: boolean }) {
            this.encryptEnabled = true;
          };

          console.warn("[open-orpheus] TEMP_FORCE_XEAPI is enabled");
          return true;
        } catch (error) {
          console.warn("[open-orpheus] TEMP_FORCE_XEAPI failed", error);
          return false;
        }
      };

      const timer = window.setInterval(() => {
        void forceXeapi().then((enabled) => {
          if (enabled) window.clearInterval(timer);
        });
      }, 500);
      void forceXeapi();
    }

然后可以在 request 处做 hook 来看到请求响应,具体内容解密或许需要 hook 前端,本地测试请求返回正常,客户端在上述 hook 下业务展示正常,应该是问题不大

@greptile-apps

greptile-apps Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

本 PR 为网易云音乐 3.1.35 引入的 xeapi 加密请求方式提供主进程侧支持,新增 XeapiAegis 类实现 X25519 ECDH 密钥协商 + AES-128-GCM 动态密钥封装 + AES-ECB 业务数据加密,并通过四个 IPC handler 将其能力暴露给渲染进程。

  • src/main/xeapi.ts:核心 Aegis 加密类,含公钥请求/更新/缓存逻辑,使用 Set<string> 追踪多个 pending nonce,并对 response.code 做了显式判断(已修复先前反馈的竞态与 falsy 问题)。
  • src/main/calls/network.ts:注册 network.initAegisnetwork.aegisEncryptnetwork.setSessionnetwork.updateAegisPublicKey 四个 IPC handler;其中 aegisEncrypt 在加密失败时静默返回原始明文,渲染进程无法同步感知。
  • src/main/folders.ts:新增 aegisPublicKey 路径常量,指向 userData/Aegis/pubkey

Confidence Score: 4/5

整体实现完整,但 aegisEncrypt 在加密失败时静默返回明文,渲染进程在状态降级通知到达前会用明文发出 xeapi 请求导致该次请求被服务端拒绝

aegisEncrypt handler 捕获加密异常后返回原始明文,与加密成功的返回形状相同,渲染进程无法同步区分。状态降级通知通过独立 IPC 消息异步到达,存在时间窗口使渲染进程以"加密成功"为假设提交了实际未加密的请求体,该请求必然被服务端拒绝。

src/main/calls/network.ts 中 aegisEncrypt handler 的错误处理路径需要关注

Important Files Changed

Filename Overview
src/main/xeapi.ts 新增 XeapiAegis 核心加密类,实现 X25519 ECDH + AES-GCM 动态密钥封装、AES-ECB 业务数据加密、公钥缓存/更新、多 nonce Set 防竞态;先前反馈的竞态与 code==0 问题均已修复
src/main/calls/network.ts 新增 4 个 IPC call handler(initAegis、aegisEncrypt、setSession、updateAegisPublicKey);aegisEncrypt 在加密失败时静默返回原始明文,渲染进程无法同步感知失败
src/main/folders.ts 新增 aegisPublicKey 路径常量,指向 userData/Aegis/pubkey,变更简单、无风险

Sequence Diagram

sequenceDiagram
    participant R as 渲染进程
    participant M as 主进程 (network.ts)
    participant A as XeapiAegis
    participant S as 网易云服务端

    R->>M: network.initAegis(config)
    M->>A: aegis.init(config, callbacks)
    A->>A: loadCachedPublicKey()
    A->>M: callbacks.onRequestPublicKey(request)
    M-->>R: channel.call network.onRequestAegisPublicKey
    R->>S: GET /xeapi/pubkey (带 signature/nonce)
    S-->>R: "{ code:200, data:{ encryptedData, signature } }"
    R->>M: network.updateAegisPublicKey(response)
    M->>A: updatePublicKeyResponse(responseText)
    A->>A: 验证 nonce + 解密公钥 + 保存缓存
    A->>M: callbacks.onEncryptStateChange(Normal)
    M-->>R: channel.call network.onEncryptStateChange

    R->>M: "network.aegisEncrypt({ body })"
    M->>A: aegis.encrypt(body)
    A->>A: encryptBusinessData + encryptDynamicKey (X25519+AES-GCM)
    A-->>M: "B=...&S=...&R=..."
    M-->>R: "{ encryptedBody }"
    R->>S: POST xeapi 请求 (encryptedBody)
    S-->>R: 加密响应
Loading

Reviews (2): Last reviewed commit: "fix: race condition when init xeapi pubk..." | Re-trigger Greptile

Comment thread src/main/xeapi.ts
Comment thread src/main/xeapi.ts Outdated
Comment thread src/main/xeapi.ts Outdated
Comment thread src/main/xeapi.ts
@1254qwer 1254qwer marked this pull request as draft June 12, 2026 09:12

@YUCLing YUCLing left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

十分感谢,不过该 PR 只会在更新至对应引入版本时才会合并

btw,有一点好奇,PC 端的 Aegis 是单独的库吗?如果是的话,和 Android 的 libAegisSDK.so 是同一个库吗(大致上)

Comment thread src/main/xeapi.ts Outdated
Comment thread src/main/xeapi.ts Outdated
Comment thread src/main/xeapi.ts Outdated
@YUCLing YUCLing added the implementation Implementation adds/fixes label Jun 12, 2026
@1254qwer

Copy link
Copy Markdown
Contributor Author

十分感谢,不过该 PR 只会在更新至对应引入版本时才会合并

btw,有一点好奇,PC 端的 Aegis 是单独的库吗?如果是的话,和 Android 的 libAegisSDK.so 是同一个库吗(大致上)

是单独的库,名称为AegisSDK.dll,目测基本差不多,加密算法基本一致,但初始化参数与 Android 不完全一致,因此目前的写法来源于 Windows native

@1254qwer

Copy link
Copy Markdown
Contributor Author

基本把前面的问题做了些修改,在更新资源包之前我先保持草稿吧,到时候我再转

Comment thread src/main/calls/network.ts
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

implementation Implementation adds/fixes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants