(false);
diff --git a/src/features/settings/panels/QMCv2/InstructionsMac.tsx b/src/features/settings/panels/QMCv2/InstructionsMac.tsx
index 56ad266..26f5986 100644
--- a/src/features/settings/panels/QMCv2/InstructionsMac.tsx
+++ b/src/features/settings/panels/QMCv2/InstructionsMac.tsx
@@ -1,94 +1,31 @@
-import { RiFileCopyLine } from 'react-icons/ri';
-import { toast } from 'react-toastify';
-import { ExtLink } from '~/components/ExtLink';
-import { FilePathBlock } from '~/components/FilePathBlock';
-import { VQuote } from '~/components/HelpText/VQuote';
-import { MacCommandKey } from '~/components/Key/MacCommandKey';
-import { ShiftKey } from '~/components/Key/ShiftKey';
-
-import BlockUpdateScript from './assets/QQ 音乐 Mac 屏蔽升级.tar.gz?base64';
-
-const MAC_CLIENT_URL =
- 'https://web.archive.org/web/20230903/https://dldir1.qq.com/music/clntupate/mac/QQMusicMac_Mgr.dmg';
-const MAC_CLIENT_TG_URL = 'https://t.me/um_lsr_ch/21';
-const DB_PATH =
- '~/Library/Containers/com.tencent.QQMusicMac/Data/Library/Application Support/QQMusicMac/mmkv/MMKVStreamEncryptId';
+import { useId } from 'react';
+import { InstructionsMacV8 } from './InstructionsMacV8';
+import { InstructionsMacV10 } from './InstructionsMacV10';
export function InstructionsMac() {
- const copyDbPathToClipboard = () => {
- navigator.clipboard
- .writeText(DB_PATH)
- .then(() => {
- toast.success('已复制到剪贴板');
- })
- .catch((err) => {
- toast.error(`复制失败,请手动复制\n${err}`);
- });
- };
+ const macInstructionId = useId();
return (
<>
Mac 客户端使用 mmkv 数据库储存密钥。
- 此外,你需要降级到 v8.8.0 版本的客户端 —— 更新的版本对密钥数据库进行了加密,目前无公开的获取方案。
+ 建议使用 v8.8.0 或 v10.7 版本的客户端,其中 v8.8.0 版本需要屏蔽更新。
- 获取 QQ 音乐 Mac 客户端 8.8.0:
-
- -
-
- 通过
Archive.org 缓存下载(慢)
-
-
- -
-
- 通过 Telegram 下载(需要账号)
-
-
-
-
-
- 有部分用户发现现在会强制更新。你可以下载
-
- QQ 音乐 Mac 屏蔽升级.tar.gz
-
- ,然后执行 QQ 音乐 Mac 屏蔽升级.command。 其原理是修改 QQ
- 音乐的版本号,让其认为自己是最新版本,从而屏蔽更新。
-
-
- 密钥文件通常存储在下述路径:
- {DB_PATH}
-
- 导入密钥
-
- -
-
-
MMKVStreamEncryptId 文件路径
-
- -
- 点击上方的文件选择区域,打开文件选择框
-
- -
- 按下
-
-
- {'+'}
-
- {'+'}
- G
-
- 组合键打开路径输入框
-
- -
- 粘贴之前复制的
MMKVStreamEncryptId 文件路径
-
- - 按下「回车键」确认。
-
+
+
+
+
使用 QQ 音乐 Mac v8.8.0
+
+
+
+
+
+
+
使用 QQ 音乐 Mac v10.7.1
+
+
+
+
+
>
);
}
diff --git a/src/features/settings/panels/QMCv2/InstructionsMacV10.tsx b/src/features/settings/panels/QMCv2/InstructionsMacV10.tsx
new file mode 100644
index 0000000..30ab96b
--- /dev/null
+++ b/src/features/settings/panels/QMCv2/InstructionsMacV10.tsx
@@ -0,0 +1,59 @@
+import { ExtLink } from '~/components/ExtLink';
+
+import {
+ commandName as DUMP_COMMAND_NAME,
+ tarName as DUMP_COMMAND_TARBALL_NAME,
+ tarball as DUMP_COMMAND_BASE64,
+} from './assets/qqmusic_v10.7_dump.command?&name=QQ 音乐 Mac v10 密钥提取.command&mac-command';
+import { DownloadBase64 } from '~/components/DownloadBase64';
+import { VQuote } from '~/components/HelpText/VQuote';
+import { InSecretImportModalContext } from '~/context/InSecretImportModal';
+import { useContext } from 'react';
+
+const MAC_CLIENT_URL =
+ 'https://c.y.qq.com/cgi-bin/file_redirect.fcg?bid=dldir&file=ecosfile%2Fmusic_clntupate%2Fmac%2Fother%2FQQMusicMac10.7.1Build00.dmg&sign=1-0cb9ee4c40e7447e2113cfdee2dc11c88487b0e31fe37cfe1c59e12c20956dce-689e9373';
+const MAC_CLIENT_TG_URL = 'https://t.me/um_lsr_ch/30';
+
+export function InstructionsMacV10() {
+ const inSecretImportModal = useContext(InSecretImportModalContext);
+ return (
+ <>
+ 获取 QQ 音乐 Mac 客户端 10.7.1:
+
+ -
+
+ 通过 QQ 音乐官网下载(高速,但可能失效)
+
+
+ -
+
+ 通过 Telegram 下载(缓存,需要账号)
+
+
+
+
+ 导入密钥
+
+ -
+ 下载
+ ,打开得到
{DUMP_COMMAND_NAME}。
+
+ -
+ 双击运行
{DUMP_COMMAND_NAME},如果提示访问目录,请允许其访问。
+
+ -
+ 运行后会在当前目录生成
qqmusic-mac-*.mmkv 文件,其中 * 是一串随机字符。
+
+ {inSecretImportModal ? (
+ -
+ 上传刚生成的
qqmusic-mac-*.mmkv 文件到上方的文件选择区域。
+
+ ) : (
+ -
+ 前往设定页面,提交生成的
qqmusic-mac-*.mmkv 文件。
+
+ )}
+
+ >
+ );
+}
diff --git a/src/features/settings/panels/QMCv2/InstructionsMacV8.tsx b/src/features/settings/panels/QMCv2/InstructionsMacV8.tsx
new file mode 100644
index 0000000..4bce9ec
--- /dev/null
+++ b/src/features/settings/panels/QMCv2/InstructionsMacV8.tsx
@@ -0,0 +1,81 @@
+import { RiFileCopyLine } from 'react-icons/ri';
+import { ExtLink } from '~/components/ExtLink';
+import { FilePathBlock } from '~/components/FilePathBlock';
+import { VQuote } from '~/components/HelpText/VQuote';
+import { MacCommandKey } from '~/components/Key/MacCommandKey';
+import { ShiftKey } from '~/components/Key/ShiftKey';
+import { copyToClipboard } from '~/util/clipboard';
+
+import BlockUpdateScript from './assets/QQ 音乐 Mac 屏蔽升级.tar.gz?base64';
+import { DownloadBase64 } from '~/components/DownloadBase64';
+import { useContext } from 'react';
+import { InSecretImportModalContext } from '~/context/InSecretImportModal';
+
+const MAC_CLIENT_URL =
+ 'https://web.archive.org/web/20230903/https://dldir1.qq.com/music/clntupate/mac/QQMusicMac_Mgr.dmg';
+const MAC_CLIENT_TG_URL = 'https://t.me/um_lsr_ch/21';
+const DB_PATH =
+ '~/Library/Containers/com.tencent.QQMusicMac/Data/Library/Application Support/QQMusicMac/mmkv/MMKVStreamEncryptId';
+
+export function InstructionsMacV8() {
+ const inSecretImportModal = useContext(InSecretImportModalContext);
+
+ return (
+ <>
+ 获取 QQ 音乐 Mac 客户端 8.8.0:
+
+ -
+
+ 通过
Archive.org 缓存下载(慢)
+
+
+ -
+
+ 通过 Telegram 下载(需要账号)
+
+
+
+
+
+ 部分用户可能会被强制要求更新。你可以下载
+
+ 并执行 QQ 音乐 Mac 屏蔽升级.command。
+ 其原理是修改 QQ 音乐的版本号,让其认为自己是最新版本,从而达到屏蔽更新的效果。
+
+
+ 密钥文件通常存储在下述路径:
+ {DB_PATH}
+
+ 导入密钥
+
+ -
+
+
MMKVStreamEncryptId 文件路径
+
+ -
+ {inSecretImportModal ? (
+
+ 点击上方的文件选择区域,打开文件选择框
+
+ ) : (
+ 前往设定页面,提交该密钥文件。
+ )}
+
+ ※ 你可以在文件选择对话框按下
+
+
+ {'+'}
+
+ {'+'}
+ G
+
+ 组合键打开路径输入框,粘贴文件路径并回车提交。
+
+
+
+ >
+ );
+}
diff --git a/src/features/settings/panels/QMCv2/assets/.gitignore b/src/features/settings/panels/QMCv2/assets/.gitignore
new file mode 100644
index 0000000..13772d6
--- /dev/null
+++ b/src/features/settings/panels/QMCv2/assets/.gitignore
@@ -0,0 +1,3 @@
+com.tencent.QQMusicMac.plist
+iData/
+qqmusic-mac-*.mmkv
diff --git a/src/features/settings/panels/QMCv2/assets/qqmusic_v10.7_dump.command b/src/features/settings/panels/QMCv2/assets/qqmusic_v10.7_dump.command
new file mode 100755
index 0000000..516e8b3
--- /dev/null
+++ b/src/features/settings/panels/QMCv2/assets/qqmusic_v10.7_dump.command
@@ -0,0 +1,217 @@
+#!/usr/bin/env python3
+
+# QQMusic Mac MMKV Decryptor by LSR@Unlock Music
+
+import hashlib
+import re
+import sys
+from argparse import ArgumentParser
+from dataclasses import dataclass
+from os import PathLike
+from pathlib import Path
+from struct import pack, unpack
+
+
+@dataclass
+class MMKVDecryptionData:
+ udid: str
+ mmkv_path: Path
+ mmkv_key: str
+ data: bytes
+
+ @property
+ def mmkv_name(self) -> str:
+ return self.mmkv_path.name
+
+
+def _aes_128_cfb_decrypt(key: bytes, iv: bytes, ciphertext: bytes) -> bytes:
+ """Decrypt using `Crypto.Cipher.AES` _or_ fallback to `OpenSSL` otherwise"""
+ try:
+ from Crypto.Cipher import AES # pyright: ignore[reportMissingImports]
+
+ aes = AES.new(key[:16], AES.MODE_CFB, iv=iv, segment_size=128)
+ return aes.decrypt(ciphertext)
+ except ImportError:
+ from subprocess import PIPE, Popen
+
+ process = Popen(
+ ["openssl", "enc", "-aes-128-cfb", "-d", "-K", key.hex(), "-iv", iv.hex()],
+ stdin=PIPE,
+ stdout=PIPE,
+ stderr=PIPE,
+ text=False,
+ )
+ stdout, stderr = process.communicate(input=ciphertext)
+ if process.returncode != 0:
+ raise RuntimeError(
+ f"OpenSSL error (install PyCryptodome instead): {stderr.decode()}"
+ )
+ return stdout
+
+
+def _caesar(text: str, shift: int) -> str:
+ """A simple Caesar cipher implementation for alphanumeric characters"""
+ result = ""
+ for char in text:
+ if char.isalpha():
+ base = ord("A") if char.isupper() else ord("a")
+ result += chr((ord(char) - base + shift) % 26 + base)
+ elif char.isdigit():
+ result += chr((ord(char) - ord("0") + shift) % 10 + ord("0"))
+ else:
+ result += char
+ return result
+
+
+__MMKV_TYPE_STREAM_KEY = 1
+
+
+def _derive_mmkv_config(udid: str, mmkv_type: int):
+ """Derive MMKV name and key from UDID, return (name, key)"""
+ str1 = _caesar(udid, mmkv_type + 3)
+ int1 = int(udid[5:7], 16)
+ int2 = 5 + (int1 + mmkv_type) % 4
+ mmkv_name = str1[0:int2]
+
+ int3 = mmkv_type + 0xA546
+ str3 = f"{udid}{int3:04x}"
+ mmkv_key = hashlib.md5(str3.encode()).hexdigest()
+
+ return mmkv_name, mmkv_key
+
+
+def _decrypt_mmkv(path: PathLike, key: bytes):
+ """Decrypt MMKV file using the given key, return decrypted data"""
+ with open(path, "rb") as mmkv, open(str(path) + ".crc", "rb") as crc:
+ crc.seek(12)
+ iv = crc.read(16)
+ (real_size,) = unpack(" {
+ navigator.clipboard
+ .writeText(text)
+ .then(() => {
+ toast.success('已复制到剪贴板');
+ })
+ .catch((err) => {
+ toast.error(`复制失败,请手动复制\n${err}`);
+ });
+};
diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts
index e73ae3a..02deb07 100644
--- a/src/vite-env.d.ts
+++ b/src/vite-env.d.ts
@@ -11,3 +11,9 @@ declare module '*?base64' {
const content: string;
export default content;
}
+
+declare module '*&mac-command' {
+ export const tarball: string;
+ export const commandName: string;
+ export const tarName: string;
+}
diff --git a/support/mac-command-loader.ts b/support/mac-command-loader.ts
new file mode 100644
index 0000000..31047e7
--- /dev/null
+++ b/support/mac-command-loader.ts
@@ -0,0 +1,37 @@
+import { basename } from 'node:path';
+import { readFile } from 'node:fs/promises';
+import { Plugin } from 'vite';
+import tar from 'tar-stream';
+
+export const macCommandLoader: Plugin = {
+ name: 'mac-command-loader',
+ async transform(_: unknown, id: string) {
+ const [path, query] = id.split('?');
+ if (!query || !query.includes('mac-command')) return null;
+
+ const params = new URLSearchParams(query);
+
+ // Mac .command packer.
+ // - Create a tarball with the given file (a+x)
+ // - Encode to base64
+
+ const tarball = tar.pack();
+ const name = params.get('name') || `${basename(path)}.command`;
+ const data = await readFile(path);
+ tarball.entry({ name, mode: 0o755 }, data);
+ tarball.finalize();
+
+ const chunks: Buffer[] = [];
+ for await (const chunk of tarball) {
+ chunks.push(chunk as Buffer);
+ }
+ const dataBuffer = Buffer.concat(chunks);
+ const base64 = dataBuffer.toString('base64');
+
+ return `
+ export const tarball = ${JSON.stringify(base64)};
+ export const commandName = ${JSON.stringify(name)};
+ export const tarName = ${JSON.stringify(`${name}.tar`)};
+ `;
+ },
+};
diff --git a/vite.config.ts b/vite.config.ts
index 3711dcc..97af5d2 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -12,6 +12,7 @@ import tailwindcss from '@tailwindcss/vite';
import { tryCommand } from './support/command';
import { base64Loader } from './support/b64-loader';
+import { macCommandLoader } from './support/mac-command-loader';
const projectRoot = url.fileURLToPath(new URL('.', import.meta.url));
const pkg = JSON.parse(fs.readFileSync(projectRoot + '/package.json', 'utf-8'));
@@ -47,6 +48,7 @@ export default defineConfig({
plugins: [
tailwindcss(),
base64Loader,
+ macCommandLoader,
replace({
preventAssignment: true,
values: {