Upgrade to use @unlock-music/crypto #78
10
README.MD
10
README.MD
@ -40,7 +40,8 @@
|
||||
[^qm-key-ios]: 需要越狱获取密钥数据库,或对设备进行完整备份后提取密钥数据库,并导入后使用。
|
||||
[^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)」。
|
||||
|
||||
### 面向 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) - 命令行批量处理版
|
||||
- [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)
|
||||
- [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-` 开头的附件
|
||||
|
||||
[webview2_redist]: https://go.microsoft.com/fwlink/p/?LinkId=2124703
|
||||
|
@ -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 更改已合并。
|
36
docs/develop-with-um_crypto.zh.md
Normal file
36
docs/develop-with-um_crypto.zh.md
Normal 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 更改已合并。
|
@ -23,8 +23,7 @@
|
||||
"@emotion/react": "^11.11.1",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@reduxjs/toolkit": "^2.0.1",
|
||||
"@um/libparakeet": "0.4.5",
|
||||
"@unlock-music/crypto": "0.0.0-alpha.13",
|
||||
"@unlock-music/crypto": "0.0.0-alpha.15",
|
||||
"framer-motion": "^10.16.16",
|
||||
"nanoid": "^5.0.4",
|
||||
"radash": "^11.0.0",
|
||||
|
@ -38,12 +38,9 @@ importers:
|
||||
'@reduxjs/toolkit':
|
||||
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)
|
||||
'@um/libparakeet':
|
||||
specifier: 0.4.5
|
||||
version: 0.4.5
|
||||
'@unlock-music/crypto':
|
||||
specifier: 0.0.0-alpha.13
|
||||
version: 0.0.0-alpha.13
|
||||
specifier: 0.0.0-alpha.15
|
||||
version: 0.0.0-alpha.15
|
||||
framer-motion:
|
||||
specifier: ^10.16.16
|
||||
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==}
|
||||
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':
|
||||
resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==}
|
||||
|
||||
'@unlock-music/crypto@0.0.0-alpha.13':
|
||||
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}
|
||||
'@unlock-music/crypto@0.0.0-alpha.15':
|
||||
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':
|
||||
resolution: {integrity: sha512-oojO9IDc4nCUUi8qIR11KoQm0XFFLIwsRBwHRR4d/88IWghn1y6ckz/bJ8GHDCsYEJee8mDzqtJxh15/cisJNQ==}
|
||||
@ -6114,11 +6108,9 @@ snapshots:
|
||||
'@typescript-eslint/types': 6.15.0
|
||||
eslint-visitor-keys: 3.4.3
|
||||
|
||||
'@um/libparakeet@0.4.5': {}
|
||||
|
||||
'@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))':
|
||||
dependencies:
|
||||
|
@ -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();
|
@ -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();
|
||||
}
|
||||
}
|
@ -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
|
@ -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
|
@ -1 +0,0 @@
|
||||
export const KWM_KEY = 'MoOtOiTvINGwd2E6n0E1i7L5t2IoOoNk';
|
@ -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();
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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,
|
||||
]);
|
@ -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();
|
||||
}
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
export const SEED = 106;
|
||||
export const ENC_V2_KEY_1 = '386ZJY!@#*$%^&)(';
|
||||
export const ENC_V2_KEY_2 = '**#!(#$%&^a1cZ,T';
|
@ -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();
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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,
|
||||
};
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
@ -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());
|
||||
}
|
||||
}
|
@ -1,7 +1,6 @@
|
||||
import { WorkerServerBus } from '~/util/WorkerEventBus';
|
||||
import { DECRYPTION_WORKER_ACTION_NAME } from './constants';
|
||||
|
||||
import { getSDKVersion } from '@um/libparakeet';
|
||||
import { getUmcVersion } from '@unlock-music/crypto';
|
||||
|
||||
import { workerDecryptHandler } from './worker/handler/decrypt';
|
||||
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.FIND_QMC_MUSICEX_NAME, workerParseMusicExMediaName);
|
||||
bus.addEventHandler(DECRYPTION_WORKER_ACTION_NAME.VERSION, getSDKVersion);
|
||||
bus.addEventHandler(DECRYPTION_WORKER_ACTION_NAME.VERSION, getUmcVersion);
|
||||
|
@ -1,20 +1,19 @@
|
||||
import { fetchParakeet, FooterParserState } from '@um/libparakeet';
|
||||
import type { FetchMusicExNamePayload } from '~/decrypt-worker/types';
|
||||
import { makeQMCv2FooterParser } from '~/decrypt-worker/util/qmc2KeyCrypto';
|
||||
import { timedLogger } from '~/util/logUtils.ts';
|
||||
import { QMCFooter } from '@unlock-music/crypto';
|
||||
|
||||
export const workerParseMusicExMediaName = async ({ id, blobURI }: FetchMusicExNamePayload) => {
|
||||
const label = `qmcMusixEx(${id.replace('://', ':')})`;
|
||||
const label = `qmcMusixExDetectName(${id.replace('://', ':')})`;
|
||||
return timedLogger(label, async () => {
|
||||
const parakeet = await fetchParakeet();
|
||||
const blob = await fetch(blobURI, { headers: { Range: 'bytes=-1024' } }).then((r) => r.blob());
|
||||
const buffer = await blob.arrayBuffer();
|
||||
|
||||
const parsed = makeQMCv2FooterParser(parakeet).parse(buffer.slice(-1024));
|
||||
if (parsed.state === FooterParserState.OK) {
|
||||
return parsed.mediaName;
|
||||
}
|
||||
const arrayBuffer = await blob.arrayBuffer();
|
||||
|
||||
try {
|
||||
const buffer = new Uint8Array(arrayBuffer.slice(-1024));
|
||||
const footer = QMCFooter.parse(buffer);
|
||||
return footer?.mediaName || null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -4,8 +4,8 @@ import {
|
||||
ButtonGroup,
|
||||
Checkbox,
|
||||
Flex,
|
||||
HStack,
|
||||
Heading,
|
||||
HStack,
|
||||
Icon,
|
||||
IconButton,
|
||||
List,
|
||||
@ -61,7 +61,7 @@ export function PanelQMCv2Key() {
|
||||
alert(`不是支持的 SQLite 数据库文件。`);
|
||||
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 map = parseAndroidQmEKey(new DataView(fileBuffer));
|
||||
qmc2Keys = Array.from(map.entries(), ([name, ekey]) => ({ name: getFileName(name), ekey }));
|
||||
|
@ -13,7 +13,6 @@ import {
|
||||
} from '@chakra-ui/react';
|
||||
|
||||
import { useAppDispatch, useAppSelector } from '~/hooks';
|
||||
import { fetchParakeet } from '@um/libparakeet';
|
||||
import { ExtLink } from '~/components/ExtLink';
|
||||
import { ChangeEvent, ClipboardEvent } from 'react';
|
||||
import { VQuote } from '~/components/HelpText/VQuote';
|
||||
@ -60,9 +59,7 @@ export function PanelQingTing() {
|
||||
model !== null
|
||||
) {
|
||||
e.preventDefault();
|
||||
fetchParakeet().then((parakeet) => {
|
||||
setSecretKey(parakeet.qtfm.createDeviceKey(product, device, manufacturer, brand, board, model));
|
||||
});
|
||||
alert('TODO!');
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -34,14 +34,13 @@ export default defineConfig({
|
||||
'node_modules',
|
||||
|
||||
// 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',
|
||||
],
|
||||
},
|
||||
},
|
||||
base: './',
|
||||
optimizeDeps: {
|
||||
exclude: ['@um/libparakeet', '@unlock-music/crypto', 'sql.js'],
|
||||
exclude: ['@unlock-music/crypto', 'sql.js'],
|
||||
},
|
||||
plugins: [
|
||||
replace({
|
||||
@ -88,7 +87,7 @@ export default defineConfig({
|
||||
'~': path.resolve(__dirname, 'src'),
|
||||
'@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'),
|
||||
},
|
||||
},
|
||||
|
Loading…
Reference in New Issue
Block a user