refactor: remove parakeet

This commit is contained in:
鲁树人 2024-09-19 00:07:01 +01:00
parent 62d21ad393
commit 6f229ff39e
26 changed files with 64 additions and 362 deletions

View File

@ -40,7 +40,8 @@
[^qm-key-ios]: 需要越狱获取密钥数据库,或对设备进行完整备份后提取密钥数据库,并导入后使用。 [^qm-key-ios]: 需要越狱获取密钥数据库,或对设备进行完整备份后提取密钥数据库,并导入后使用。
[^qm-key-mac]: 需要导入密钥数据库。 [^qm-key-mac]: 需要导入密钥数据库。
不支持的格式?请提交样本(加密文件)与客户端信息(或一并上传其安装包)到[仓库的问题追踪区][project-issues]。如果文件太大,请上传到不需要登入下载的网盘,如 [mega.nz](https://mega.nz)、[OneDrive](https://www.onedrive.com/) 等。 不支持的格式?请提交样本(加密文件)与客户端信息(或一并上传其安装包)到[仓库的问题追踪区][project-issues]
。如果文件太大,请上传到不需要登入下载的网盘,如 [mega.nz](https://mega.nz)、[OneDrive](https://www.onedrive.com/) 等。
如果遇到解密出错的情况,请一并携带错误信息并简单描述错误的重现过程。 如果遇到解密出错的情况,请一并携带错误信息并简单描述错误的重现过程。
@ -50,11 +51,11 @@
从源码运行或编译生产版本,请参考文档「[新手上路](./docs/getting-started.zh.md)」。 从源码运行或编译生产版本,请参考文档「[新手上路](./docs/getting-started.zh.md)」。
### 面向 libparakeet SDK 开发 ### 解密库开发
⚠️ 如果只是进行前端方面的更改,你可以跳过该节。 ⚠️ 如果只是进行前端方面的更改,你可以跳过该节。
请参考文档「[面向 `libparakeet-js` 开发](./docs/develop-with-libparakeet.zh.md)」。 请参考文档「[面向 `@unlock-music/crypto` 开发](./docs/develop-with-um_crypto.zh)」。
### 架构 ### 架构
@ -79,7 +80,8 @@
- [Unlock Music (Cli)](https://git.unlock-music.dev/um/cli) - 命令行批量处理版 - [Unlock Music (Cli)](https://git.unlock-music.dev/um/cli) - 命令行批量处理版
- [um-react (Electron 前端)](https://github.com/CarlGao4/um-react-electron) - 使用 Electron 框架封装的本地可执行文件。 - [um-react (Electron 前端)](https://github.com/CarlGao4/um-react-electron) - 使用 Electron 框架封装的本地可执行文件。
- [GitHub 下载](https://github.com/CarlGao4/um-react-electron/releases/latest) | [仓库镜像](https://git.unlock-music.dev/CarlGao4/um-react-electron) - [GitHub 下载](https://github.com/CarlGao4/um-react-electron/releases/latest) | [仓库镜像](https://git.unlock-music.dev/CarlGao4/um-react-electron)
- [um-react-wry](https://git.unlock-music.dev/um/um-react-wry) - 使用 WRY 框架封装的 Win64 单文件 (需要[安装 Edge WebView2 运行时][webview2_redist]Win10+ 操作系统自带) - [um-react-wry](https://git.unlock-music.dev/um/um-react-wry) - 使用 WRY 框架封装的 Win64 单文件 (
需要[安装 Edge WebView2 运行时][webview2_redist]Win10+ 操作系统自带)
- [本地下载](https://git.unlock-music.dev/um/um-react/releases/latest) | 寻找文件名为 `um-react-win64-` 开头的附件 - [本地下载](https://git.unlock-music.dev/um/um-react/releases/latest) | 寻找文件名为 `um-react-win64-` 开头的附件
[webview2_redist]: https://go.microsoft.com/fwlink/p/?LinkId=2124703 [webview2_redist]: https://go.microsoft.com/fwlink/p/?LinkId=2124703

View File

@ -1,50 +0,0 @@
# 面向 `libparakeet-js` 开发
⚠️ 如果只是进行前端方面的更改,你可以跳过该文档。
`libparakeet-js` 编译目前需要 Linux 环境,请参考[仓库说明][libparakeet-js-doc]。
该文档将假设这两个项目被放置在同级的目录下:
```text
~/Projects/um-projects
/um-react
/libparakeet-js
```
若为不同目录,你需要调整 `LIB_PARAKEET_JS_DIR` 环境变量到仓库目录,然后再启动 vite 项目。
[libparakeet-js-doc]: https://github.com/parakeet-rs/libparakeet-js/blob/main/README.MD
## 初次构建
- 进入上层目录:`cd ..`
- 克隆 `libparakeet-js` 仓库 (目前需要 Linux 环境, Windows 下推荐使用 WSL2)
- `git clone --recurse-submodules https://github.com/parakeet-rs/libparakeet-js.git`
- 进入 SDK 目录:`cd libparakeet-js`
- 如果需要更新 `submodule``git submodule update --init --recursive`
- 构建所有代码:`make all`
如果需要手动控制构建过程,你也可以:
- 运行 `./build.sh -j 4` 进行 C++ 到 WebAssembly 编译过程
- 此处的 `4` 是并行编译数量,该值通常略小于 CPU 核心数。
- 若是不指定并行数量,则使用当前核心数。
- 编译 `js-sdk`
- 进入 `npm` 目录:`cd npm`
- 安装依赖:`pnpm i --frozen-lockfile`
- 构建:`pnpm build`
## 做出更改
做出更改后,参考上面的内容进行重新编译。
## 应用 SDK 更改
将构建好的 SDK 直接嵌入到当前前端项目:
```sh
pnpm link ../libparakeet-js/npm
```
※ 建立 PR 时,请先提交 SDK PR 并确保你的 SDK 更改已合并。

View File

@ -0,0 +1,36 @@
# 面向 `@unlock-music/crypto` 开发
⚠️ 如果只是进行前端方面的更改,你可以跳过该文档。
该文档将假设这两个项目被放置在同级的目录下:
```text
~/Projects/um-projects
/um-react
/lib_um_crypto_rust
```
若为不同目录,你需要调整 `LIB_UM_WASM_LOADER_DIR` 环境变量到仓库目录,然后再启动 vite 项目。
## 初次构建
- 进入上层目录:`cd ..`
- 克隆 `lib_um_crypto_rust` 仓库
- `git clone https://git.unlock-music.dev/um/lib_um_crypto_rust.git`
- 进入 SDK 目录:`cd lib_um_crypto_rust ; cd um_wasm_loader`
- 安装所有 Node 以来:`pnpm i`
- 构建:`pnpm build`
## 做出更改
做出更改后,参考上面的内容进行重新编译。
## 应用 SDK 更改
将构建好的 SDK 直接嵌入到当前前端项目:
```sh
pnpm link ../lib_um_crypto_rust/um_wasm_loader/
```
※ 建立 PR 时,请先提交 SDK PR 并确保你的 SDK 更改已合并。

View File

@ -23,8 +23,7 @@
"@emotion/react": "^11.11.1", "@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0", "@emotion/styled": "^11.11.0",
"@reduxjs/toolkit": "^2.0.1", "@reduxjs/toolkit": "^2.0.1",
"@um/libparakeet": "0.4.5", "@unlock-music/crypto": "0.0.0-alpha.15",
"@unlock-music/crypto": "0.0.0-alpha.13",
"framer-motion": "^10.16.16", "framer-motion": "^10.16.16",
"nanoid": "^5.0.4", "nanoid": "^5.0.4",
"radash": "^11.0.0", "radash": "^11.0.0",

View File

@ -38,12 +38,9 @@ importers:
'@reduxjs/toolkit': '@reduxjs/toolkit':
specifier: ^2.0.1 specifier: ^2.0.1
version: 2.0.1(react-redux@9.0.4(@types/react@18.2.45)(react@18.2.0)(redux@5.0.0))(react@18.2.0) version: 2.0.1(react-redux@9.0.4(@types/react@18.2.45)(react@18.2.0)(redux@5.0.0))(react@18.2.0)
'@um/libparakeet':
specifier: 0.4.5
version: 0.4.5
'@unlock-music/crypto': '@unlock-music/crypto':
specifier: 0.0.0-alpha.13 specifier: 0.0.0-alpha.15
version: 0.0.0-alpha.13 version: 0.0.0-alpha.15
framer-motion: framer-motion:
specifier: ^10.16.16 specifier: ^10.16.16
version: 10.16.16(react-dom@18.2.0(react@18.2.0))(react@18.2.0) version: 10.16.16(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
@ -1921,14 +1918,11 @@ packages:
resolution: {integrity: sha512-1zvtdC1a9h5Tb5jU9x3ADNXO9yjP8rXlaoChu0DQX40vf5ACVpYIVIZhIMZ6d5sDXH7vq4dsZBT1fEGj8D2n2w==} resolution: {integrity: sha512-1zvtdC1a9h5Tb5jU9x3ADNXO9yjP8rXlaoChu0DQX40vf5ACVpYIVIZhIMZ6d5sDXH7vq4dsZBT1fEGj8D2n2w==}
engines: {node: ^16.0.0 || >=18.0.0} engines: {node: ^16.0.0 || >=18.0.0}
'@um/libparakeet@0.4.5':
resolution: {integrity: sha512-ACkB9ShFQvlQQt0JtpgPdZYTM9u1odfBEkXGMuEnlD0BOEMAq/82AAzXJV8x4TVlMbWje0i9DGGl7zMyrR5RCQ==, tarball: https://git.unlock-music.dev/api/packages/um/npm/%40um%2Flibparakeet/-/0.4.5/libparakeet-0.4.5.tgz}
'@ungap/structured-clone@1.2.0': '@ungap/structured-clone@1.2.0':
resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==}
'@unlock-music/crypto@0.0.0-alpha.13': '@unlock-music/crypto@0.0.0-alpha.15':
resolution: {integrity: sha512-4afez9SjfY5EN17JsifXCc50dGDIImBGXnoxQbBQkB4TguJt2ePWCDS8UbnBhnfeAQUypWBC6qicOQSwVM36hw==, tarball: https://git.unlock-music.dev/api/packages/um/npm/%40unlock-music%2Fcrypto/-/0.0.0-alpha.13/crypto-0.0.0-alpha.13.tgz} resolution: {integrity: sha512-ST3Vbv5ITWE2n8W+07DiPUSpNy2qsK6nELsKcAeAXlulL/HirTfky5xpJl3Q6Zy/34crMsG8jKvP5QB6ELbsSw==, tarball: https://git.unlock-music.dev/api/packages/um/npm/%40unlock-music%2Fcrypto/-/0.0.0-alpha.15/crypto-0.0.0-alpha.15.tgz}
'@vitejs/plugin-react@4.2.1': '@vitejs/plugin-react@4.2.1':
resolution: {integrity: sha512-oojO9IDc4nCUUi8qIR11KoQm0XFFLIwsRBwHRR4d/88IWghn1y6ckz/bJ8GHDCsYEJee8mDzqtJxh15/cisJNQ==} resolution: {integrity: sha512-oojO9IDc4nCUUi8qIR11KoQm0XFFLIwsRBwHRR4d/88IWghn1y6ckz/bJ8GHDCsYEJee8mDzqtJxh15/cisJNQ==}
@ -6114,11 +6108,9 @@ snapshots:
'@typescript-eslint/types': 6.15.0 '@typescript-eslint/types': 6.15.0
eslint-visitor-keys: 3.4.3 eslint-visitor-keys: 3.4.3
'@um/libparakeet@0.4.5': {}
'@ungap/structured-clone@1.2.0': {} '@ungap/structured-clone@1.2.0': {}
'@unlock-music/crypto@0.0.0-alpha.13': {} '@unlock-music/crypto@0.0.0-alpha.15': {}
'@vitejs/plugin-react@4.2.1(vite@5.0.10(@types/node@20.10.5)(sass@1.69.5)(terser@5.27.0))': '@vitejs/plugin-react@4.2.1(vite@5.0.10(@types/node@20.10.5)(sass@1.69.5)(terser@5.27.0))':
dependencies: dependencies:

View File

@ -1,6 +0,0 @@
import KGM_TYPE_4_FILE_KEY_EXPANSION_TABLE_RAW from './kgm_type4_file_key_expansion_table.txt?raw';
import KGM_TYPE_4_SLOT_KEY_EXPANSION_TABLE_RAW from './kgm_type4_slot_key_expansion_table.txt?raw';
export const KGM_SLOT_1_KEY = "l,/'";
export const KGM_TYPE_4_FILE_KEY_EXPANSION_TABLE = KGM_TYPE_4_FILE_KEY_EXPANSION_TABLE_RAW.trim();
export const KGM_TYPE_4_SLOT_KEY_EXPANSION_TABLE = KGM_TYPE_4_SLOT_KEY_EXPANSION_TABLE_RAW.trim();

View File

@ -1,18 +0,0 @@
import { transformBlob } from '~/decrypt-worker/util/transformBlob';
import type { DecipherInstance } from '~/decrypt-worker/Deciphers.ts';
import { KGM_SLOT_1_KEY, KGM_TYPE_4_FILE_KEY_EXPANSION_TABLE, KGM_TYPE_4_SLOT_KEY_EXPANSION_TABLE } from './kgm_pc.key';
export class KGMCrypto implements DecipherInstance {
cryptoName = 'KGM/PC';
checkByDecryptHeader = true;
async decrypt(buffer: ArrayBuffer): Promise<Blob> {
return transformBlob(buffer, (p) =>
p.make.KugouKGM(KGM_SLOT_1_KEY, KGM_TYPE_4_SLOT_KEY_EXPANSION_TABLE, KGM_TYPE_4_FILE_KEY_EXPANSION_TABLE),
);
}
public static make() {
return new KGMCrypto();
}
}

View File

@ -1 +0,0 @@
!@#$%^&*(O)P_+DCFVBGNMXDCFVBGN!@#$%^&*()_@#$%^&*()kljhgfk;oswhqoi7t89g_+@#$%^&*()!@#$%^&*()@#$%^&*(@#$%^&*()@#$%^&*()@#$^&$&^%*&^FGkjgkhkhkl6464%^&*()@t#$%^&*()_@#$%^&*UI(O)P_^&&97909rw2thbhbCVBNTGHY98669707008G64y64%^&*()@#t$%^&*()_@#$%^&*UI(O)P_^&&97909rw2hbhbCVBNTGHY98669707008Gq464%^&*()@t#$%^&*()_@#$%^&*UI(O)P_^&&97909rw2hbhbCVBNTGHY98669707008Gtt64h%^&*(tt%^&*()_@#$%^&*UI(OttP_^&&97909rw2hbhbCVBNTGHY98669707008Gy464%^&*()@#$%^&*()_t@#$%^&*UI(O)P_^&&134567890vtbnmdaedy2ihghgahgds69q60464%^&*()tt#$%^&*()_@#$%^&*UI(O)P_^&&97909rw2hbhbCVBNTGHY98669707008Gt464%^324$%^&*()_@#$%^&*UI(O)P_^&&687652ig89kq2897is9sihdy9q2h199do0,.,,63464%^&d*()@#$%^&*()_@#$%^&*UI(O)P_^&&dw3fdwert242fwesfe2352323233534

View File

@ -1 +0,0 @@
drfghbjn673yu8u9ickj98qwoopujjjaws09unmcl;sjopiupaqnmwjpdmsmphxoihfln9g*/8466R&FJG*&^%FDVJKBTgvjhvbduowtg3bs76r%$^RFJVHBDTFGYF7gfdik23h8iibnds53482HBKDSHGFCMFSKHGIUGXKBWKHOOSADONWLN9OIHCLNALNDOICNALFSNDOPHASC, 0xWBNICFFFFFFFFSFVBC4NBFU7MHGJ7^reflv, 0xbk&$%w:!oi){+u:bx*)y!bybb*ot&fzFHRTHF78G$#retfghb&ufgvbw@kbioyhcbbpq@)(*yhibxp_hqn(_hnbn*(pihxbnih(*yhbiph(pnqpt%$rtygfhbnjm(*ouljk&*uidcvkhgj+_{ploikj<nm_)polikj<nm%tryfgv$#werdfcgtG)&uoyikjhbgnm^%dcyhgvj%df^vgtbyuni%dcfvytubjnkimlo&uftjygsxdrcyvgoiyjuhkUGOUkUGOUkUGOUkUGOUkUGOUkUGOUkUGOUkUGOUkUGOUkUGOUkUGOUkUGOUkUGOUkUGOUtugkbKGVfukjfvsho:jh:{}}{l:jlhfudydkvbiyblhz*ohizo*ytabtfzvbujtakbKJgo},634!@#$rfv(iujhg&yuhgqwsaxdc9I8UJE3DFCV*(iujhgWSTYxdchg(*itgvhjf^eHY534

View File

@ -1 +0,0 @@
export const KWM_KEY = 'MoOtOiTvINGwd2E6n0E1i7L5t2IoOoNk';

View File

@ -1,28 +0,0 @@
import { transformBlob } from '~/decrypt-worker/util/transformBlob';
import type { DecipherInstance } from '~/decrypt-worker/Deciphers.ts';
import { KWM_KEY } from './kwm.key';
import { DecryptCommandOptions } from '~/decrypt-worker/types';
import { makeQMCv2KeyCrypto } from '~/decrypt-worker/util/qmc2KeyCrypto';
import { fetchParakeet } from '@um/libparakeet';
import { stringToUTF8Bytes } from '~/decrypt-worker/util/utf8Encoder';
// v1 only
export class KWMCrypto implements DecipherInstance {
cryptoName = 'KWM';
checkByDecryptHeader = true;
async decrypt(buffer: ArrayBuffer, opts: DecryptCommandOptions): Promise<Blob> {
const kwm2key = opts.kwm2key ?? '';
const parakeet = await fetchParakeet();
const keyCrypto = makeQMCv2KeyCrypto(parakeet);
return transformBlob(buffer, (p) => p.make.KuwoKWMv2(KWM_KEY, stringToUTF8Bytes(kwm2key), keyCrypto), {
cleanup: () => keyCrypto.delete(),
parakeet,
});
}
public static make() {
return new KWMCrypto();
}
}

View File

@ -1,15 +0,0 @@
import { transformBlob } from '~/decrypt-worker/util/transformBlob';
import type { DecipherInstance } from '~/decrypt-worker/Deciphers.ts';
export class MiguCrypto implements DecipherInstance {
cryptoName = 'Migu3D/Keyless';
checkByDecryptHeader = true;
async decrypt(buffer: ArrayBuffer): Promise<Blob> {
return transformBlob(buffer, (p) => p.make.Migu3D());
}
public static make() {
return new MiguCrypto();
}
}

View File

@ -1,16 +0,0 @@
export default new Uint8Array([
0x77, 0x48, 0x32, 0x73, 0xde, 0xf2, 0xc0, 0xc8, 0x95, 0xec, 0x30, 0xb2, 0x51, 0xc3, 0xe1, 0xa0, 0x9e, 0xe6, 0x9d,
0xcf, 0xfa, 0x7f, 0x14, 0xd1, 0xce, 0xb8, 0xdc, 0xc3, 0x4a, 0x67, 0x93, 0xd6, 0x28, 0xc2, 0x91, 0x70, 0xca, 0x8d,
0xa2, 0xa4, 0xf0, 0x08, 0x61, 0x90, 0x7e, 0x6f, 0xa2, 0xe0, 0xeb, 0xae, 0x3e, 0xb6, 0x67, 0xc7, 0x92, 0xf4, 0x91,
0xb5, 0xf6, 0x6c, 0x5e, 0x84, 0x40, 0xf7, 0xf3, 0x1b, 0x02, 0x7f, 0xd5, 0xab, 0x41, 0x89, 0x28, 0xf4, 0x25, 0xcc,
0x52, 0x11, 0xad, 0x43, 0x68, 0xa6, 0x41, 0x8b, 0x84, 0xb5, 0xff, 0x2c, 0x92, 0x4a, 0x26, 0xd8, 0x47, 0x6a, 0x7c,
0x95, 0x61, 0xcc, 0xe6, 0xcb, 0xbb, 0x3f, 0x47, 0x58, 0x89, 0x75, 0xc3, 0x75, 0xa1, 0xd9, 0xaf, 0xcc, 0x08, 0x73,
0x17, 0xdc, 0xaa, 0x9a, 0xa2, 0x16, 0x41, 0xd8, 0xa2, 0x06, 0xc6, 0x8b, 0xfc, 0x66, 0x34, 0x9f, 0xcf, 0x18, 0x23,
0xa0, 0x0a, 0x74, 0xe7, 0x2b, 0x27, 0x70, 0x92, 0xe9, 0xaf, 0x37, 0xe6, 0x8c, 0xa7, 0xbc, 0x62, 0x65, 0x9c, 0xc2,
0x08, 0xc9, 0x88, 0xb3, 0xf3, 0x43, 0xac, 0x74, 0x2c, 0x0f, 0xd4, 0xaf, 0xa1, 0xc3, 0x01, 0x64, 0x95, 0x4e, 0x48,
0x9f, 0xf4, 0x35, 0x78, 0x95, 0x7a, 0x39, 0xd6, 0x6a, 0xa0, 0x6d, 0x40, 0xe8, 0x4f, 0xa8, 0xef, 0x11, 0x1d, 0xf3,
0x1b, 0x3f, 0x3f, 0x07, 0xdd, 0x6f, 0x5b, 0x19, 0x30, 0x19, 0xfb, 0xef, 0x0e, 0x37, 0xf0, 0x0e, 0xcd, 0x16, 0x49,
0xfe, 0x53, 0x47, 0x13, 0x1a, 0xbd, 0xa4, 0xf1, 0x40, 0x19, 0x60, 0x0e, 0xed, 0x68, 0x09, 0x06, 0x5f, 0x4d, 0xcf,
0x3d, 0x1a, 0xfe, 0x20, 0x77, 0xe4, 0xd9, 0xda, 0xf9, 0xa4, 0x2b, 0x76, 0x1c, 0x71, 0xdb, 0x00, 0xbc, 0xfd, 0x0c,
0x6c, 0xa5, 0x47, 0xf7, 0xf6, 0x00, 0x79, 0x4a, 0x11,
]);

View File

@ -1,16 +0,0 @@
import { transformBlob } from '~/decrypt-worker/util/transformBlob';
import type { DecipherInstance } from '~/decrypt-worker/Deciphers.ts';
import key from './qmc_v1.key.ts';
export class QMC1Crypto implements DecipherInstance {
cryptoName = 'QMC/v1';
checkByDecryptHeader = true;
async decrypt(buffer: ArrayBuffer): Promise<Blob> {
return transformBlob(buffer, (p) => p.make.QMCv1(key));
}
public static make() {
return new QMC1Crypto();
}
}

View File

@ -1,3 +0,0 @@
export const SEED = 106;
export const ENC_V2_KEY_1 = '386ZJY!@#*$%^&)(';
export const ENC_V2_KEY_2 = '**#!(#$%&^a1cZ,T';

View File

@ -1,25 +0,0 @@
import { transformBlob } from '~/decrypt-worker/util/transformBlob';
import type { DecipherInstance } from '~/decrypt-worker/Deciphers.ts';
import { DecryptCommandOptions } from '~/decrypt-worker/types';
export class QingTingFM$Device implements DecipherInstance {
cryptoName = 'QingTing FM/Device ID';
checkByDecryptHeader = false;
async checkBySignature(_buffer: ArrayBuffer, options: DecryptCommandOptions) {
return Boolean(/^\.p~?!.*\.qta$/.test(options.fileName) && options.qingTingAndroidKey);
}
async decrypt(buffer: ArrayBuffer, options: DecryptCommandOptions): Promise<Blob> {
const { fileName: name, qingTingAndroidKey } = options;
if (!qingTingAndroidKey) {
throw new Error('QingTingFM Android Device Key was not provided');
}
return transformBlob(buffer, (p) => p.make.QingTingFM(name, qingTingAndroidKey));
}
public static make() {
return new QingTingFM$Device();
}
}

View File

@ -1,51 +0,0 @@
// Xiami file header
// offset description
// 0x00 "ifmt"
// 0x04 Format name, e.g. "FLAC".
// 0x08 0xfe, 0xfe, 0xfe, 0xfe
// 0x0C (3 bytes) Little-endian, size of data to copy without modification.
// e.g. [ 8a 19 00 ] = 6538 bytes of plaintext data.
// 0x0F (1 byte) File key, applied to
// 0x10 Plaintext data
// ???? Encrypted data
import type { DecipherInstance } from '~/decrypt-worker/Deciphers.ts';
// little endian
const XIAMI_FILE_MAGIC = 0x746d6669;
const XIAMI_EXPECTED_PADDING = 0xfefefefe;
const u8Sub = (a: number, b: number) => {
if (a > b) {
return a - b;
}
return a + 0x100 - b;
};
export class XiamiCrypto implements DecipherInstance {
cryptoName = 'Xiami';
checkByDecryptHeader = false;
async checkBySignature(buffer: ArrayBuffer): Promise<boolean> {
const header = new DataView(buffer);
return header.getUint32(0x00, true) === XIAMI_FILE_MAGIC && header.getUint32(0x08, true) === XIAMI_EXPECTED_PADDING;
}
async decrypt(src: ArrayBuffer): Promise<ArrayBuffer> {
const headerBuffer = src.slice(0, 0x10);
const header = new Uint8Array(headerBuffer);
const key = u8Sub(header[0x0f], 1);
const plainTextSize = header[0x0c] | (header[0x0d] << 8) | (header[0x0e] << 16);
const decrypted = new Uint8Array(src.slice(0x10));
for (let i = decrypted.byteLength - 1; i >= plainTextSize; i--) {
decrypted[i] = u8Sub(key, decrypted[i]);
}
return decrypted;
}
public static make() {
return new XiamiCrypto();
}
}

View File

@ -1,17 +0,0 @@
export interface XimalayaAndroidKey {
contentKey: string;
init: number;
step: number;
}
export const XimalayaX2MKey: XimalayaAndroidKey = {
contentKey: 'xmly',
init: 0.615243,
step: 3.837465,
};
export const XimalayaX3MKey: XimalayaAndroidKey = {
contentKey: '3989d111aad5613940f4fc44b639b292',
init: 0.726354,
step: 3.948576,
};

View File

@ -1,30 +0,0 @@
import { transformBlob } from '~/decrypt-worker/util/transformBlob';
import type { DecipherInstance } from '~/decrypt-worker/Deciphers.ts';
import { XimalayaAndroidKey, XimalayaX2MKey, XimalayaX3MKey } from './xmly_android.key.js';
export class XimalayaAndroidCrypto implements DecipherInstance {
cryptoName = 'Ximalaya/Android';
checkByDecryptHeader = true;
constructor(private key: XimalayaAndroidKey) {}
async decrypt(buffer: ArrayBuffer): Promise<Blob> {
const { contentKey, init, step } = this.key;
return transformBlob(buffer, (p) => {
const transformer = p.make.XimalayaAndroid(init, step, contentKey);
if (!transformer) {
throw new Error('could not make xmly transformer, is key invalid?');
}
return transformer;
});
}
public static makeX2M() {
return new XimalayaAndroidCrypto(XimalayaX2MKey);
}
public static makeX3M() {
return new XimalayaAndroidCrypto(XimalayaX3MKey);
}
}

View File

@ -1,5 +0,0 @@
import type { Parakeet } from '@um/libparakeet';
import { SEED, ENC_V2_KEY_1, ENC_V2_KEY_2 } from '../crypto/qmc/qmc_v2.key';
export const makeQMCv2KeyCrypto = (p: Parakeet) => p.make.QMCv2KeyCrypto(SEED, ENC_V2_KEY_1, ENC_V2_KEY_2);
export const makeQMCv2FooterParser = (p: Parakeet) => p.make.QMCv2FooterParser(SEED, ENC_V2_KEY_1, ENC_V2_KEY_2);

View File

@ -1,38 +0,0 @@
import { Transformer, Parakeet, TransformResult, fetchParakeet } from '@um/libparakeet';
import { toArrayBuffer } from './buffer';
import { UnsupportedSourceFile } from './DecryptError';
export async function transformBlob(
blob: Blob | ArrayBuffer,
transformerFactory: (p: Parakeet) => Transformer | Promise<Transformer>,
{ cleanup, parakeet }: { cleanup?: () => void; parakeet?: Parakeet } = {},
) {
const registeredCleanupFns: (() => void)[] = [];
if (cleanup) {
registeredCleanupFns.push(cleanup);
}
try {
const mod = parakeet ?? (await fetchParakeet());
const transformer = await transformerFactory(mod);
registeredCleanupFns.push(() => transformer.delete());
const reader = mod.make.Reader(await toArrayBuffer(blob));
registeredCleanupFns.push(() => reader.delete());
const sink = mod.make.WriterSink();
const writer = sink.getWriter();
registeredCleanupFns.push(() => writer.delete());
const result = transformer.Transform(writer, reader);
if (result === TransformResult.ERROR_INVALID_FORMAT) {
throw new UnsupportedSourceFile(`transformer<${transformer.Name}> does not recognize this file`);
} else if (result !== TransformResult.OK) {
throw new Error(`transformer<${transformer.Name}> failed with error: ${TransformResult[result]} (${result})`);
}
return sink.collectBlob();
} finally {
registeredCleanupFns.forEach((cleanup) => cleanup());
}
}

View File

@ -1,7 +1,6 @@
import { WorkerServerBus } from '~/util/WorkerEventBus'; import { WorkerServerBus } from '~/util/WorkerEventBus';
import { DECRYPTION_WORKER_ACTION_NAME } from './constants'; import { DECRYPTION_WORKER_ACTION_NAME } from './constants';
import { getUmcVersion } from '@unlock-music/crypto';
import { getSDKVersion } from '@um/libparakeet';
import { workerDecryptHandler } from './worker/handler/decrypt'; import { workerDecryptHandler } from './worker/handler/decrypt';
import { workerParseMusicExMediaName } from './worker/handler/qmcv2_parser'; import { workerParseMusicExMediaName } from './worker/handler/qmcv2_parser';
@ -11,4 +10,4 @@ onmessage = bus.onmessage;
bus.addEventHandler(DECRYPTION_WORKER_ACTION_NAME.DECRYPT, workerDecryptHandler); bus.addEventHandler(DECRYPTION_WORKER_ACTION_NAME.DECRYPT, workerDecryptHandler);
bus.addEventHandler(DECRYPTION_WORKER_ACTION_NAME.FIND_QMC_MUSICEX_NAME, workerParseMusicExMediaName); bus.addEventHandler(DECRYPTION_WORKER_ACTION_NAME.FIND_QMC_MUSICEX_NAME, workerParseMusicExMediaName);
bus.addEventHandler(DECRYPTION_WORKER_ACTION_NAME.VERSION, getSDKVersion); bus.addEventHandler(DECRYPTION_WORKER_ACTION_NAME.VERSION, getUmcVersion);

View File

@ -1,20 +1,19 @@
import { fetchParakeet, FooterParserState } from '@um/libparakeet';
import type { FetchMusicExNamePayload } from '~/decrypt-worker/types'; import type { FetchMusicExNamePayload } from '~/decrypt-worker/types';
import { makeQMCv2FooterParser } from '~/decrypt-worker/util/qmc2KeyCrypto';
import { timedLogger } from '~/util/logUtils.ts'; import { timedLogger } from '~/util/logUtils.ts';
import { QMCFooter } from '@unlock-music/crypto';
export const workerParseMusicExMediaName = async ({ id, blobURI }: FetchMusicExNamePayload) => { export const workerParseMusicExMediaName = async ({ id, blobURI }: FetchMusicExNamePayload) => {
const label = `qmcMusixEx(${id.replace('://', ':')})`; const label = `qmcMusixExDetectName(${id.replace('://', ':')})`;
return timedLogger(label, async () => { return timedLogger(label, async () => {
const parakeet = await fetchParakeet();
const blob = await fetch(blobURI, { headers: { Range: 'bytes=-1024' } }).then((r) => r.blob()); const blob = await fetch(blobURI, { headers: { Range: 'bytes=-1024' } }).then((r) => r.blob());
const buffer = await blob.arrayBuffer(); const arrayBuffer = await blob.arrayBuffer();
const parsed = makeQMCv2FooterParser(parakeet).parse(buffer.slice(-1024)); try {
if (parsed.state === FooterParserState.OK) { const buffer = new Uint8Array(arrayBuffer.slice(-1024));
return parsed.mediaName; const footer = QMCFooter.parse(buffer);
return footer?.mediaName || null;
} catch {
return null;
} }
return null;
}); });
}; };

View File

@ -4,8 +4,8 @@ import {
ButtonGroup, ButtonGroup,
Checkbox, Checkbox,
Flex, Flex,
HStack,
Heading, Heading,
HStack,
Icon, Icon,
IconButton, IconButton,
List, List,
@ -61,7 +61,7 @@ export function PanelQMCv2Key() {
alert(`不是支持的 SQLite 数据库文件。`); alert(`不是支持的 SQLite 数据库文件。`);
return; return;
} }
} else if (/MMKVStreamEncryptId|filenameEkeyMap|qmpc-mmkv-v1/i.test(file.name)) { } else if (/MMKVStreamEncryptId|filenameEkeyMap|qmpc-mmkv-v1|(\.mmkv$)/i.test(file.name)) {
const fileBuffer = await file.arrayBuffer(); const fileBuffer = await file.arrayBuffer();
const map = parseAndroidQmEKey(new DataView(fileBuffer)); const map = parseAndroidQmEKey(new DataView(fileBuffer));
qmc2Keys = Array.from(map.entries(), ([name, ekey]) => ({ name: getFileName(name), ekey })); qmc2Keys = Array.from(map.entries(), ([name, ekey]) => ({ name: getFileName(name), ekey }));

View File

@ -13,7 +13,6 @@ import {
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { useAppDispatch, useAppSelector } from '~/hooks'; import { useAppDispatch, useAppSelector } from '~/hooks';
import { fetchParakeet } from '@um/libparakeet';
import { ExtLink } from '~/components/ExtLink'; import { ExtLink } from '~/components/ExtLink';
import { ChangeEvent, ClipboardEvent } from 'react'; import { ChangeEvent, ClipboardEvent } from 'react';
import { VQuote } from '~/components/HelpText/VQuote'; import { VQuote } from '~/components/HelpText/VQuote';
@ -60,9 +59,7 @@ export function PanelQingTing() {
model !== null model !== null
) { ) {
e.preventDefault(); e.preventDefault();
fetchParakeet().then((parakeet) => { alert('TODO!');
setSecretKey(parakeet.qtfm.createDeviceKey(product, device, manufacturer, brand, board, model));
});
} }
}; };

View File

@ -34,14 +34,13 @@ export default defineConfig({
'node_modules', 'node_modules',
// Allow pnpm to link. // Allow pnpm to link.
process.env.LIB_PARAKEET_JS_DIR || '../libparakeet-js',
process.env.LIB_UM_WASM_LOADER_DIR || '../lib_um_crypto_rust/um_wasm_loader', process.env.LIB_UM_WASM_LOADER_DIR || '../lib_um_crypto_rust/um_wasm_loader',
], ],
}, },
}, },
base: './', base: './',
optimizeDeps: { optimizeDeps: {
exclude: ['@um/libparakeet', '@unlock-music/crypto', 'sql.js'], exclude: ['@unlock-music/crypto', 'sql.js'],
}, },
plugins: [ plugins: [
replace({ replace({
@ -88,7 +87,7 @@ export default defineConfig({
'~': path.resolve(__dirname, 'src'), '~': path.resolve(__dirname, 'src'),
'@nm': path.resolve(__dirname, 'node_modules'), '@nm': path.resolve(__dirname, 'node_modules'),
// workaround for vite, workbox (PWA) and Emscripten transpiled parakeet lib (use of `import("module")`) // workaround for vite, workbox (PWA)
module: path.resolve(__dirname, 'src', 'dummy.mjs'), module: path.resolve(__dirname, 'src', 'dummy.mjs'),
}, },
}, },