feat: add insturctions on how to dump keys for v10

This commit is contained in:
鲁树人
2025-10-15 00:57:56 +09:00
parent f49f629917
commit fb52b0197c
15 changed files with 571 additions and 105 deletions

View File

@@ -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 (
<>
<p>Mac 使 mmkv </p>
<p> v8.8.0 </p>
<p>使 v8.8.0 v10.7 v8.8.0 </p>
<p className="mt-4"> QQ Mac 8.8.0:</p>
<ul className="list-disc pl-6">
<li>
<ExtLink className="link-info" href={MAC_CLIENT_URL}>
<code>Archive.org</code>
</ExtLink>
</li>
<li>
<ExtLink className="link-info" href={MAC_CLIENT_TG_URL}>
Telegram
</ExtLink>
</li>
</ul>
<p className="mt-4">
<ExtLink
className="link-info mx-1"
download="QQ 音乐 Mac 屏蔽升级.tar.gz"
href={`data:application/gzip;base64,${BlockUpdateScript}`}
>
QQ Mac .tar.gz
</ExtLink>
<code>QQ Mac .command</code> QQ
</p>
<p className="mt-4"></p>
<FilePathBlock>{DB_PATH}</FilePathBlock>
<h4 className="font-bold text-lg mt-4"></h4>
<ol className="list-decimal pl-6">
<li>
<button className="btn btn-sm btn-outline btn-accent mr-2" onClick={copyDbPathToClipboard}>
<RiFileCopyLine className="text-xl" />
<span></span>
</button>
<code>MMKVStreamEncryptId</code>
</li>
<li>
<VQuote></VQuote><VQuote></VQuote>
</li>
<li>
<VQuote>
<ShiftKey className="mx-1" />
{'+'}
<MacCommandKey className="mx-1" />
{'+'}
<kbd className="kbd mx-1">G</kbd>
</VQuote>
<VQuote></VQuote>
</li>
<li>
<code>MMKVStreamEncryptId</code>
</li>
<li></li>
</ol>
<div className="join join-vertical bg-base-100 mt-2 max-w-full">
<div className="collapse collapse-arrow join-item border-base-300 border">
<input type="radio" name={macInstructionId} />
<div className="collapse-title font-semibold">使 QQ Mac v8.8.0</div>
<div className="collapse-content text-sm min-w-0">
<InstructionsMacV8 />
</div>
</div>
<div className="collapse collapse-arrow join-item border-base-300 border">
<input type="radio" name={macInstructionId} />
<div className="collapse-title font-semibold">使 QQ Mac v10.7.1</div>
<div className="collapse-content text-sm min-w-0">
<InstructionsMacV10 />
</div>
</div>
</div>
</>
);
}

View File

@@ -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 (
<>
<p className="mt-4"> QQ Mac 10.7.1:</p>
<ul className="list-disc pl-6">
<li>
<ExtLink className="link-info" href={MAC_CLIENT_URL}>
QQ
</ExtLink>
</li>
<li>
<ExtLink className="link-info" href={MAC_CLIENT_TG_URL}>
Telegram
</ExtLink>
</li>
</ul>
<h4 className="font-bold text-lg mt-4"></h4>
<ol className="list-decimal pl-6">
<li>
<DownloadBase64 data={DUMP_COMMAND_BASE64} filename={DUMP_COMMAND_TARBALL_NAME}></DownloadBase64>
<code>{DUMP_COMMAND_NAME}</code>
</li>
<li>
<code>{DUMP_COMMAND_NAME}</code>访访
</li>
<li>
<code>qqmusic-mac-*.mmkv</code> <code>*</code>
</li>
{inSecretImportModal ? (
<li>
<code>qqmusic-mac-*.mmkv</code> <VQuote></VQuote>
</li>
) : (
<li>
<code>qqmusic-mac-*.mmkv</code>
</li>
)}
</ol>
</>
);
}

View File

@@ -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 (
<>
<p className="mt-4"> QQ Mac 8.8.0:</p>
<ul className="list-disc pl-6">
<li>
<ExtLink className="link-info" href={MAC_CLIENT_URL}>
<code>Archive.org</code>
</ExtLink>
</li>
<li>
<ExtLink className="link-info" href={MAC_CLIENT_TG_URL}>
Telegram
</ExtLink>
</li>
</ul>
<p className="mt-4">
<DownloadBase64 filename="QQ 音乐 Mac v8.8.0 屏蔽升级.tar.gz" data={BlockUpdateScript}></DownloadBase64>
<code>QQ Mac .command</code>
<span> QQ </span>
</p>
<p className="mt-4"></p>
<FilePathBlock>{DB_PATH}</FilePathBlock>
<h4 className="font-bold text-lg mt-4"></h4>
<ol className="list-decimal pl-6">
<li>
<button className="btn btn-sm btn-outline btn-accent mr-2" onClick={() => copyToClipboard(DB_PATH)}>
<RiFileCopyLine className="text-xl" />
<span></span>
</button>
<code>MMKVStreamEncryptId</code>
</li>
<li>
{inSecretImportModal ? (
<p>
<VQuote></VQuote><VQuote></VQuote>
</p>
) : (
<p></p>
)}
<p>
<VQuote>
<ShiftKey className="mx-1" />
{'+'}
<MacCommandKey className="mx-1" />
{'+'}
<kbd className="kbd mx-1">G</kbd>
</VQuote>
<VQuote></VQuote>
</p>
</li>
</ol>
</>
);
}

View File

@@ -0,0 +1,3 @@
com.tencent.QQMusicMac.plist
iData/
qqmusic-mac-*.mmkv

View File

@@ -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("<I", crc.read(4))
(mmkv_payload_size,) = unpack("<I", mmkv.read(4))
if mmkv_payload_size != real_size:
raise ValueError("MMKV file size mismatch")
decrypted_data = pack("<I", real_size)
decrypted_data += _aes_128_cfb_decrypt(key, iv, mmkv.read(real_size))
return decrypted_data
def _dump_udid(plist_file: PathLike):
"""Extract UDIDs from the given plist file"""
with open(plist_file, "rb") as f:
plist = f.read()
for m in re.finditer(rb"_\x10\(([0-9a-f]{40})_", plist):
yield m.group(1).decode()
def _dump_mmkv(plist_file: PathLike, data_dir: PathLike):
"""Dump all MMKV files from the given plist file and iData directory"""
for udid in _dump_udid(plist_file):
mmkv_name, mmkv_key = _derive_mmkv_config(udid, __MMKV_TYPE_STREAM_KEY)
mmkv_path = Path(data_dir) / mmkv_name
if not mmkv_path.exists() or not mmkv_path.is_file():
print(f"MMKV file not found, skipping (path={mmkv_path})", file=sys.stderr)
continue
try:
decrypted_mmkv = _decrypt_mmkv(mmkv_path, mmkv_key.encode())
except Exception as e:
print(
"Error decrypting mmkv, skipping"
f" (path={mmkv_path}, key={mmkv_key}, error={e})",
file=sys.stderr,
)
continue
yield MMKVDecryptionData(
udid=udid,
mmkv_path=mmkv_path,
mmkv_key=mmkv_key,
data=decrypted_mmkv,
)
def main():
parser = ArgumentParser(
description="QQMusic Mac MMKV Decryptor by LSR@Unlock Music"
)
parser.add_argument(
"-p",
"--plist",
type=str,
nargs="+",
help="Path to com.tencent.QQMusicMac.plist file or files",
default=[],
)
parser.add_argument(
"-i",
"--idata",
type=str,
help="Path to iData directory",
default="",
)
parser.add_argument("-f", "--force", action="store_true", help="Force overwrite")
parser.add_argument(
"-o",
"--output",
type=str,
help="Output directory for decrypted MMKV files (default: current directory)",
default=".",
)
parser.add_argument(
"-v", "--verbose", action="store_true", help="Enable verbose output"
)
parser.add_argument("--no-pause", action="store_true", help="Do not pause on exit")
args = parser.parse_args()
home_dir = Path.home()
app_sandbox_dir = home_dir / "Library/Containers/com.tencent.QQMusicMac/Data"
idata_dir = app_sandbox_dir / "Library/Application Support/QQMusicMac/iData"
if args.idata:
idata_dir = Path(args.idata)
plists = []
if args.plist:
plists = [Path(p) for p in args.plist]
else:
for base_dir in (home_dir, app_sandbox_dir):
plists.append(base_dir / "Library/Preferences/com.tencent.QQMusicMac.plist")
output_dir = Path(args.output)
output_dir.mkdir(parents=True, exist_ok=True)
force = args.force
verbose = args.verbose
no_pause = args.no_pause
for plist_file in plists:
if plist_file.exists() and plist_file.is_file():
for dump in _dump_mmkv(plist_file, idata_dir):
out_path = output_dir / f"qqmusic-mac-{dump.mmkv_path.name}.mmkv"
if out_path.exists() and not force:
print(f"output exists, skipping (name={out_path.name})")
continue
if verbose:
print("*** MMKV DUMP ENTRY START ***")
print(f"UDID: {dump.udid}")
print(f"MMKV Name: {dump.mmkv_path.name}")
print(f"MMKV Key: {dump.mmkv_key}")
print(f"Output: {out_path.name}")
print("**** MMKV DUMP ENTRY END ****")
else:
print(f"Dumping mmkv: {out_path.name}...")
try:
with open(out_path, "wb") as f:
f.write(dump.data)
except Exception as e:
print(f"Error writing decrypted mmkv: {e}", file=sys.stderr)
continue
if not no_pause:
input("Press Enter to exit...")
if __name__ == "__main__":
main()