From 8b416f80552445e2b07b5e5d09b7df32cd1d618d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=B2=81=E6=A0=91=E4=BA=BA?= Date: Sun, 15 Sep 2024 00:54:15 +0100 Subject: [PATCH 01/18] refactor: begin migrate to `@unlock-music/crypto` --- package.json | 2 +- pnpm-lock.yaml | 10 +- src/decrypt-worker/Deciphers.ts | 75 ++++++++++++ src/decrypt-worker/crypto/CryptoBase.ts | 17 --- src/decrypt-worker/crypto/CryptoFactory.ts | 55 --------- src/decrypt-worker/crypto/kgm/kgm_pc.ts | 6 +- src/decrypt-worker/crypto/kwm/kwm.ts | 4 +- .../crypto/migu/migu3d_keyless.ts | 4 +- src/decrypt-worker/crypto/ncm/ncm_pc.ts | 35 ------ src/decrypt-worker/crypto/qmc/qmc_v1.ts | 4 +- src/decrypt-worker/crypto/qmc/qmc_v2.ts | 6 +- src/decrypt-worker/crypto/qtfm/qtfm_device.ts | 4 +- .../crypto/transparent/transparent.ts | 14 --- src/decrypt-worker/crypto/xiami/xiami.ts | 4 +- .../crypto/xmly/xmly_android.ts | 5 +- .../decipher/NetEaseCloudMusic.ts | 41 +++++++ src/decrypt-worker/decipher/Transparent.ts | 18 +++ src/decrypt-worker/util/audioType.ts | 14 +++ src/decrypt-worker/util/wasmClass.ts | 17 +++ src/decrypt-worker/worker/handler/decrypt.ts | 113 ++++++++---------- src/features/file-listing/FileError.tsx | 7 +- src/util/go.ts | 7 ++ 22 files changed, 249 insertions(+), 213 deletions(-) create mode 100644 src/decrypt-worker/Deciphers.ts delete mode 100644 src/decrypt-worker/crypto/CryptoBase.ts delete mode 100644 src/decrypt-worker/crypto/CryptoFactory.ts delete mode 100644 src/decrypt-worker/crypto/ncm/ncm_pc.ts delete mode 100644 src/decrypt-worker/crypto/transparent/transparent.ts create mode 100644 src/decrypt-worker/decipher/NetEaseCloudMusic.ts create mode 100644 src/decrypt-worker/decipher/Transparent.ts create mode 100644 src/decrypt-worker/util/audioType.ts create mode 100644 src/decrypt-worker/util/wasmClass.ts create mode 100644 src/util/go.ts diff --git a/package.json b/package.json index 03278bc..fc7b2a6 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "@emotion/styled": "^11.11.0", "@reduxjs/toolkit": "^2.0.1", "@um/libparakeet": "0.4.5", - "@unlock-music/crypto": "0.0.0-alpha.6", + "@unlock-music/crypto": "0.0.0-alpha.10", "framer-motion": "^10.16.16", "nanoid": "^5.0.4", "radash": "^11.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 84a7bf6..00785fd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -42,8 +42,8 @@ importers: specifier: 0.4.5 version: 0.4.5 '@unlock-music/crypto': - specifier: 0.0.0-alpha.6 - version: 0.0.0-alpha.6 + specifier: 0.0.0-alpha.10 + version: 0.0.0-alpha.10 framer-motion: specifier: ^10.16.16 version: 10.16.16(react-dom@18.2.0(react@18.2.0))(react@18.2.0) @@ -1927,8 +1927,8 @@ packages: '@ungap/structured-clone@1.2.0': resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} - '@unlock-music/crypto@0.0.0-alpha.6': - resolution: {integrity: sha512-hv1oTXPzsNmqrP5dmjkLa4rfZtd4U/Cu1Bake71QEZhbY1WCbgEX4haCAcXXd6DMGNg/Hv1JXL2TcXNUaqIiFA==, tarball: https://git.unlock-music.dev/api/packages/um/npm/%40unlock-music%2Fcrypto/-/0.0.0-alpha.6/crypto-0.0.0-alpha.6.tgz} + '@unlock-music/crypto@0.0.0-alpha.10': + resolution: {integrity: sha512-Y8PWd/f4KEh2WU5Uz4QesnYMelDvioLYMOVvpPceMk62P2LivQLgvl5+ytDmD2yzmsnE/sXOQztTg+1WsuePCA==, tarball: https://git.unlock-music.dev/api/packages/um/npm/%40unlock-music%2Fcrypto/-/0.0.0-alpha.10/crypto-0.0.0-alpha.10.tgz} '@vitejs/plugin-react@4.2.1': resolution: {integrity: sha512-oojO9IDc4nCUUi8qIR11KoQm0XFFLIwsRBwHRR4d/88IWghn1y6ckz/bJ8GHDCsYEJee8mDzqtJxh15/cisJNQ==} @@ -6118,7 +6118,7 @@ snapshots: '@ungap/structured-clone@1.2.0': {} - '@unlock-music/crypto@0.0.0-alpha.6': {} + '@unlock-music/crypto@0.0.0-alpha.10': {} '@vitejs/plugin-react@4.2.1(vite@5.0.10(@types/node@20.10.5)(sass@1.69.5)(terser@5.27.0))': dependencies: diff --git a/src/decrypt-worker/Deciphers.ts b/src/decrypt-worker/Deciphers.ts new file mode 100644 index 0000000..e77dbd4 --- /dev/null +++ b/src/decrypt-worker/Deciphers.ts @@ -0,0 +1,75 @@ +import { NetEaseCloudMusicDecipher } from '~/decrypt-worker/decipher/NetEaseCloudMusic.ts'; +import { TransparentDecipher } from './decipher/Transparent.ts'; +import type { DecryptCommandOptions } from '~/decrypt-worker/types.ts'; + +export enum Status { + OK = 0, + NOT_THIS_CIPHER = 1, + FAILED = 2, +} + +export type DecipherResult = DecipherOK | DecipherNotOK; + +export interface DecipherNotOK { + status: Exclude; + message?: string; +} + +export interface DecipherOK { + status: Status.OK; + message?: string; + data: Uint8Array; + overrideExtension?: string; + cipherName: string; +} + +export interface DecipherInstance { + cipherName: string; + + decrypt(buffer: Uint8Array, options: DecryptCommandOptions): Promise; +} + +export type DecipherFactory = () => DecipherInstance; + +export const allCryptoFactories: DecipherFactory[] = [ + /// File with fixed headers goes first. + + // NCM (*.ncm) + NetEaseCloudMusicDecipher.make, + + // KGM (*.kgm, *.vpr) + // KGMCrypto.make, + + // KWMv1 (*.kwm) + // KWMCrypto.make, + + // Xiami (*.xm) + // XiamiCrypto.make, + + /// File with a fixed footer goes second + + // QMCv2 (*.mflac) + // QMC2CryptoWithKey.make, + // QMC2Crypto.make, + + /// File without an obvious header or footer goes last. + + // Migu3D/Keyless (*.wav; *.m4a) + // MiguCrypto.make, + + // Crypto that does not implement "checkBySignature" or need to decrypt the entire file and then check audio type, + // should be moved to the bottom of the list for performance reasons. + + // QMCv1 (*.qmcflac) + // QMC1Crypto.make, + + // Ximalaya (Android) + // XimalayaAndroidCrypto.makeX2M, + // XimalayaAndroidCrypto.makeX3M, + + // QingTingFM (Android) + // QingTingFM$Device.make, + + // Transparent crypto (not encrypted) + TransparentDecipher.make, +]; diff --git a/src/decrypt-worker/crypto/CryptoBase.ts b/src/decrypt-worker/crypto/CryptoBase.ts deleted file mode 100644 index 38b3352..0000000 --- a/src/decrypt-worker/crypto/CryptoBase.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { DecryptCommandOptions } from '~/decrypt-worker/types'; - -export interface CryptoBase { - cryptoName: string; - checkByDecryptHeader: boolean; - - /** - * If set, this new extension will be used instead. - * Useful for non-audio format, e.g. qrc to lrc/xml. - */ - overrideExtension?: string; - - checkBySignature?: (buffer: ArrayBuffer, options: DecryptCommandOptions) => Promise; - decrypt(buffer: ArrayBuffer, options: DecryptCommandOptions): Promise; -} - -export type CryptoFactory = () => CryptoBase; diff --git a/src/decrypt-worker/crypto/CryptoFactory.ts b/src/decrypt-worker/crypto/CryptoFactory.ts deleted file mode 100644 index 578f3be..0000000 --- a/src/decrypt-worker/crypto/CryptoFactory.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { CryptoFactory } from './CryptoBase'; - -import { QMC1Crypto } from './qmc/qmc_v1'; -import { QMC2Crypto, QMC2CryptoWithKey } from './qmc/qmc_v2'; -import { XiamiCrypto } from './xiami/xiami'; -import { KGMCrypto } from './kgm/kgm_pc'; -import { NCMCrypto } from './ncm/ncm_pc'; -import { XimalayaAndroidCrypto } from './xmly/xmly_android'; -import { KWMCrypto } from './kwm/kwm'; -import { MiguCrypto } from './migu/migu3d_keyless'; -import { TransparentCrypto } from './transparent/transparent'; -import { QingTingFM$Device } from './qtfm/qtfm_device'; - -export const allCryptoFactories: CryptoFactory[] = [ - /// File with fixed headers goes first. - - // NCM (*.ncm) - NCMCrypto.make, - - // KGM (*.kgm, *.vpr) - KGMCrypto.make, - - // KWMv1 (*.kwm) - KWMCrypto.make, - - // Xiami (*.xm) - XiamiCrypto.make, - - /// File with a fixed footer goes second - - // QMCv2 (*.mflac) - QMC2CryptoWithKey.make, - QMC2Crypto.make, - - /// File without an obvious header or footer goes last. - - // Migu3D/Keyless (*.wav; *.m4a) - MiguCrypto.make, - - // Crypto that does not implement "checkBySignature" or need to decrypt the entire file and then check audio type, - // should be moved to the bottom of the list for performance reasons. - - // QMCv1 (*.qmcflac) - QMC1Crypto.make, - - // Ximalaya (Android) - XimalayaAndroidCrypto.makeX2M, - XimalayaAndroidCrypto.makeX3M, - - // QingTingFM (Android) - QingTingFM$Device.make, - - // Transparent crypto (not encrypted) - TransparentCrypto.make, -]; diff --git a/src/decrypt-worker/crypto/kgm/kgm_pc.ts b/src/decrypt-worker/crypto/kgm/kgm_pc.ts index 479d13a..0ddd8c9 100644 --- a/src/decrypt-worker/crypto/kgm/kgm_pc.ts +++ b/src/decrypt-worker/crypto/kgm/kgm_pc.ts @@ -1,14 +1,14 @@ import { transformBlob } from '~/decrypt-worker/util/transformBlob'; -import type { CryptoBase } from '../CryptoBase'; +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 CryptoBase { +export class KGMCrypto implements DecipherInstance { cryptoName = 'KGM/PC'; checkByDecryptHeader = true; async decrypt(buffer: ArrayBuffer): Promise { 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) + p.make.KugouKGM(KGM_SLOT_1_KEY, KGM_TYPE_4_SLOT_KEY_EXPANSION_TABLE, KGM_TYPE_4_FILE_KEY_EXPANSION_TABLE), ); } diff --git a/src/decrypt-worker/crypto/kwm/kwm.ts b/src/decrypt-worker/crypto/kwm/kwm.ts index d049b0a..acf1618 100644 --- a/src/decrypt-worker/crypto/kwm/kwm.ts +++ b/src/decrypt-worker/crypto/kwm/kwm.ts @@ -1,5 +1,5 @@ import { transformBlob } from '~/decrypt-worker/util/transformBlob'; -import type { CryptoBase } from '../CryptoBase'; +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'; @@ -7,7 +7,7 @@ import { fetchParakeet } from '@um/libparakeet'; import { stringToUTF8Bytes } from '~/decrypt-worker/util/utf8Encoder'; // v1 only -export class KWMCrypto implements CryptoBase { +export class KWMCrypto implements DecipherInstance { cryptoName = 'KWM'; checkByDecryptHeader = true; diff --git a/src/decrypt-worker/crypto/migu/migu3d_keyless.ts b/src/decrypt-worker/crypto/migu/migu3d_keyless.ts index e864987..4f0bece 100644 --- a/src/decrypt-worker/crypto/migu/migu3d_keyless.ts +++ b/src/decrypt-worker/crypto/migu/migu3d_keyless.ts @@ -1,7 +1,7 @@ import { transformBlob } from '~/decrypt-worker/util/transformBlob'; -import type { CryptoBase } from '../CryptoBase'; +import type { DecipherInstance } from '~/decrypt-worker/Deciphers.ts'; -export class MiguCrypto implements CryptoBase { +export class MiguCrypto implements DecipherInstance { cryptoName = 'Migu3D/Keyless'; checkByDecryptHeader = true; diff --git a/src/decrypt-worker/crypto/ncm/ncm_pc.ts b/src/decrypt-worker/crypto/ncm/ncm_pc.ts deleted file mode 100644 index a68dc1b..0000000 --- a/src/decrypt-worker/crypto/ncm/ncm_pc.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type { CryptoBase } from '../CryptoBase'; -import { NCMFile } from '@unlock-music/crypto'; -import { chunkBuffer } from '~/decrypt-worker/util/buffer.ts'; - -export class NCMCrypto implements CryptoBase { - cryptoName = 'NCM/PC'; - checkByDecryptHeader = false; - ncm = new NCMFile(); - - async checkBySignature(buffer: ArrayBuffer) { - const data = new Uint8Array(buffer); - let len = 1024; - try { - while (len !== 0) { - console.debug('NCM/open: read %d bytes', len); - len = this.ncm.open(data.subarray(0, len)); - } - } catch (error) { - return false; - } - return true; - } - - async decrypt(buffer: ArrayBuffer): Promise { - const audioBuffer = new Uint8Array(buffer.slice(this.ncm.audioOffset)); - for (const [block, offset] of chunkBuffer(audioBuffer)) { - this.ncm.decrypt(block, offset); - } - return new Blob([audioBuffer]); - } - - public static make() { - return new NCMCrypto(); - } -} diff --git a/src/decrypt-worker/crypto/qmc/qmc_v1.ts b/src/decrypt-worker/crypto/qmc/qmc_v1.ts index 610c55a..f955820 100644 --- a/src/decrypt-worker/crypto/qmc/qmc_v1.ts +++ b/src/decrypt-worker/crypto/qmc/qmc_v1.ts @@ -1,8 +1,8 @@ import { transformBlob } from '~/decrypt-worker/util/transformBlob'; -import type { CryptoBase } from '../CryptoBase'; +import type { DecipherInstance } from '~/decrypt-worker/Deciphers.ts'; import key from './qmc_v1.key.ts'; -export class QMC1Crypto implements CryptoBase { +export class QMC1Crypto implements DecipherInstance { cryptoName = 'QMC/v1'; checkByDecryptHeader = true; diff --git a/src/decrypt-worker/crypto/qmc/qmc_v2.ts b/src/decrypt-worker/crypto/qmc/qmc_v2.ts index c387d09..2482a1d 100644 --- a/src/decrypt-worker/crypto/qmc/qmc_v2.ts +++ b/src/decrypt-worker/crypto/qmc/qmc_v2.ts @@ -1,11 +1,11 @@ import { transformBlob } from '~/decrypt-worker/util/transformBlob'; -import type { CryptoBase } from '../CryptoBase'; +import type { DecipherInstance } from '~/decrypt-worker/Deciphers.ts'; import type { DecryptCommandOptions } from '~/decrypt-worker/types.ts'; import { fetchParakeet } from '@um/libparakeet'; import { stringToUTF8Bytes } from '~/decrypt-worker/util/utf8Encoder.ts'; import { makeQMCv2FooterParser, makeQMCv2KeyCrypto } from '~/decrypt-worker/util/qmc2KeyCrypto.ts'; -export class QMC2Crypto implements CryptoBase { +export class QMC2Crypto implements DecipherInstance { cryptoName = 'QMC/v2'; checkByDecryptHeader = false; @@ -23,7 +23,7 @@ export class QMC2Crypto implements CryptoBase { } } -export class QMC2CryptoWithKey implements CryptoBase { +export class QMC2CryptoWithKey implements DecipherInstance { cryptoName = 'QMC/v2 (key)'; checkByDecryptHeader = true; diff --git a/src/decrypt-worker/crypto/qtfm/qtfm_device.ts b/src/decrypt-worker/crypto/qtfm/qtfm_device.ts index 51e04ef..205d156 100644 --- a/src/decrypt-worker/crypto/qtfm/qtfm_device.ts +++ b/src/decrypt-worker/crypto/qtfm/qtfm_device.ts @@ -1,8 +1,8 @@ import { transformBlob } from '~/decrypt-worker/util/transformBlob'; -import type { CryptoBase } from '../CryptoBase'; +import type { DecipherInstance } from '~/decrypt-worker/Deciphers.ts'; import { DecryptCommandOptions } from '~/decrypt-worker/types'; -export class QingTingFM$Device implements CryptoBase { +export class QingTingFM$Device implements DecipherInstance { cryptoName = 'QingTing FM/Device ID'; checkByDecryptHeader = false; diff --git a/src/decrypt-worker/crypto/transparent/transparent.ts b/src/decrypt-worker/crypto/transparent/transparent.ts deleted file mode 100644 index 78332d3..0000000 --- a/src/decrypt-worker/crypto/transparent/transparent.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { CryptoBase } from '../CryptoBase'; - -export class TransparentCrypto implements CryptoBase { - cryptoName = 'Transparent'; - checkByDecryptHeader = true; - - async decrypt(buffer: ArrayBuffer): Promise { - return new Blob([buffer]); - } - - public static make() { - return new TransparentCrypto(); - } -} diff --git a/src/decrypt-worker/crypto/xiami/xiami.ts b/src/decrypt-worker/crypto/xiami/xiami.ts index 8be5906..392d0ca 100644 --- a/src/decrypt-worker/crypto/xiami/xiami.ts +++ b/src/decrypt-worker/crypto/xiami/xiami.ts @@ -9,7 +9,7 @@ // 0x10 Plaintext data // ???? Encrypted data -import type { CryptoBase } from '../CryptoBase'; +import type { DecipherInstance } from '~/decrypt-worker/Deciphers.ts'; // little endian const XIAMI_FILE_MAGIC = 0x746d6669; @@ -23,7 +23,7 @@ const u8Sub = (a: number, b: number) => { return a + 0x100 - b; }; -export class XiamiCrypto implements CryptoBase { +export class XiamiCrypto implements DecipherInstance { cryptoName = 'Xiami'; checkByDecryptHeader = false; diff --git a/src/decrypt-worker/crypto/xmly/xmly_android.ts b/src/decrypt-worker/crypto/xmly/xmly_android.ts index b6ff4c3..9d4ae65 100644 --- a/src/decrypt-worker/crypto/xmly/xmly_android.ts +++ b/src/decrypt-worker/crypto/xmly/xmly_android.ts @@ -1,10 +1,11 @@ import { transformBlob } from '~/decrypt-worker/util/transformBlob'; -import type { CryptoBase } from '../CryptoBase.js'; +import type { DecipherInstance } from '~/decrypt-worker/Deciphers.ts'; import { XimalayaAndroidKey, XimalayaX2MKey, XimalayaX3MKey } from './xmly_android.key.js'; -export class XimalayaAndroidCrypto implements CryptoBase { +export class XimalayaAndroidCrypto implements DecipherInstance { cryptoName = 'Ximalaya/Android'; checkByDecryptHeader = true; + constructor(private key: XimalayaAndroidKey) {} async decrypt(buffer: ArrayBuffer): Promise { diff --git a/src/decrypt-worker/decipher/NetEaseCloudMusic.ts b/src/decrypt-worker/decipher/NetEaseCloudMusic.ts new file mode 100644 index 0000000..f8b05aa --- /dev/null +++ b/src/decrypt-worker/decipher/NetEaseCloudMusic.ts @@ -0,0 +1,41 @@ +import { DecipherInstance, DecipherOK, DecipherResult, Status } from '~/decrypt-worker/Deciphers'; +import { NCMFile } from '@unlock-music/crypto'; +import { chunkBuffer } from '~/decrypt-worker/util/buffer.ts'; +import { withWasmClass } from '~/decrypt-worker/util/wasmClass.ts'; +import { UnsupportedSourceFile } from '~/decrypt-worker/util/DecryptError.ts'; + +export class NetEaseCloudMusicDecipher implements DecipherInstance { + cipherName = 'NCM/PC'; + + tryInit(ncm: NCMFile, buffer: Uint8Array) { + let neededLength = 1024; + while (neededLength !== 0) { + console.debug('NCM/open: read %d bytes', neededLength); + neededLength = ncm.open(buffer.subarray(0, neededLength)); + if (neededLength === -1) { + throw new UnsupportedSourceFile('file is not ncm'); + } + } + } + + async decrypt(buffer: ArrayBuffer): Promise { + return withWasmClass(new NCMFile(), async (ncm): Promise => { + const data = new Uint8Array(buffer); + this.tryInit(ncm, data); + + const audioBuffer = data.subarray(ncm.audioOffset); + for (const [block, offset] of chunkBuffer(audioBuffer)) { + ncm.decrypt(block, offset); + } + return { + status: Status.OK, + cipherName: this.cipherName, + data: audioBuffer, + }; + }); + } + + public static make() { + return new NetEaseCloudMusicDecipher(); + } +} diff --git a/src/decrypt-worker/decipher/Transparent.ts b/src/decrypt-worker/decipher/Transparent.ts new file mode 100644 index 0000000..c08daec --- /dev/null +++ b/src/decrypt-worker/decipher/Transparent.ts @@ -0,0 +1,18 @@ +import { DecipherInstance, DecipherOK, DecipherResult, Status } from '~/decrypt-worker/Deciphers.ts'; + +export class TransparentDecipher implements DecipherInstance { + cipherName = 'none'; + + async decrypt(buffer: Uint8Array): Promise { + return { + cipherName: 'None', + status: Status.OK, + data: buffer, + message: 'No decipher applied', + }; + } + + public static make() { + return new TransparentDecipher(); + } +} diff --git a/src/decrypt-worker/util/audioType.ts b/src/decrypt-worker/util/audioType.ts new file mode 100644 index 0000000..fa2f8b5 --- /dev/null +++ b/src/decrypt-worker/util/audioType.ts @@ -0,0 +1,14 @@ +import { detectAudioType } from '@unlock-music/crypto'; + +export async function detectAudioExtension(buffer: Uint8Array): Promise { + let neededLength = 0x100; + let extension = 'bin'; + while (neededLength !== 0) { + console.debug('AudioDetect: read %d bytes', neededLength); + const detectResult = detectAudioType(buffer.subarray(0, neededLength)); + extension = detectResult.audioType; + neededLength = detectResult.needMore; + detectResult.free(); + } + return extension; +} diff --git a/src/decrypt-worker/util/wasmClass.ts b/src/decrypt-worker/util/wasmClass.ts new file mode 100644 index 0000000..b5f38d0 --- /dev/null +++ b/src/decrypt-worker/util/wasmClass.ts @@ -0,0 +1,17 @@ +import { isPromise } from 'radash'; + +export function withWasmClass void }, R>(instance: T, cb: (inst: T) => R): R { + let isAsync = false; + try { + const resp = cb(instance); + if (resp && isPromise(resp)) { + isAsync = true; + resp.finally(() => instance.free()); + } + return resp; + } finally { + if (!isAsync) { + instance.free(); + } + } +} diff --git a/src/decrypt-worker/worker/handler/decrypt.ts b/src/decrypt-worker/worker/handler/decrypt.ts index 5ff600e..53b94ac 100644 --- a/src/decrypt-worker/worker/handler/decrypt.ts +++ b/src/decrypt-worker/worker/handler/decrypt.ts @@ -1,22 +1,19 @@ -import { Parakeet, fetchParakeet } from '@um/libparakeet'; import { timedLogger, withGroupedLogs as withTimeGroupedLogs } from '~/util/logUtils'; import type { DecryptCommandOptions, DecryptCommandPayload } from '~/decrypt-worker/types'; -import { allCryptoFactories } from '../../crypto/CryptoFactory'; -import { toArrayBuffer, toBlob } from '~/decrypt-worker/util/buffer'; -import { CryptoBase, CryptoFactory } from '~/decrypt-worker/crypto/CryptoBase'; +import { allCryptoFactories } from '../../Deciphers.ts'; +import { toBlob } from '~/decrypt-worker/util/buffer'; +import { DecipherFactory, DecipherInstance, Status } from '~/decrypt-worker/Deciphers.ts'; import { UnsupportedSourceFile } from '~/decrypt-worker/util/DecryptError'; import { ready as umCryptoReady } from '@unlock-music/crypto'; - -// Use first 4MiB of the file to perform check. -const TEST_FILE_HEADER_LEN = 4 * 1024 * 1024; +import { go } from '~/util/go.ts'; +import { detectAudioExtension } from '~/decrypt-worker/util/audioType.ts'; class DecryptCommandHandler { - private label: string; + private readonly label: string; constructor( label: string, - private parakeet: Parakeet, - private buffer: ArrayBuffer, + private buffer: Uint8Array, private options: DecryptCommandOptions, ) { this.label = `DecryptCommandHandler(${label})`; @@ -26,82 +23,68 @@ class DecryptCommandHandler { return timedLogger(`${this.label}: ${label}`, fn); } - async decrypt(factories: CryptoFactory[]) { - for (const factory of factories) { - const decryptor = factory(); + async decrypt(decipherFactories: DecipherFactory[]) { + const errors: string[] = []; + for (const factory of decipherFactories) { + const decipher = factory(); - try { - const result = await this.tryDecryptFile(decryptor); - if (result === null) { - continue; - } - return result; - } catch (error) { - if (error instanceof UnsupportedSourceFile) { - console.debug('WARN: decryptor does not recognize source file, wrong crypto?', error); - } else { - console.error('decrypt failed with unknown error: ', error); + const [result, error] = await go(this.tryDecryptWith(decipher)); + if (!error) { + if (result) { + return result; } + errors.push(`${decipher.cipherName}: no response`); + continue; // not supported + } + + const errMsg = error.message; + if (errMsg) { + errors.push(`${decipher.cipherName}: ${errMsg}`); + } + if (error instanceof UnsupportedSourceFile) { + console.debug('[%s] Not this decipher:', decipher.cipherName, error); + } else { + console.error('decrypt failed with unknown error: ', error); } } - throw new UnsupportedSourceFile('could not decrypt file: no working decryptor found'); + throw new UnsupportedSourceFile(errors.join('\n')); } - async tryDecryptFile(crypto: CryptoBase) { - if (crypto.checkBySignature && !(await crypto.checkBySignature(this.buffer, this.options))) { - return null; + async tryDecryptWith(decipher: DecipherInstance) { + const result = await this.log(`decrypt ${decipher.cipherName}`, async () => + decipher.decrypt(this.buffer, this.options), + ); + switch (result.status) { + case Status.NOT_THIS_CIPHER: + return null; + case Status.FAILED: + throw new Error(`failed: ${result.message}`); + default: + break; } - if (crypto.checkByDecryptHeader && !(await this.acceptByDecryptFileHeader(crypto))) { - return null; - } - - const decrypted = await this.log(`decrypt (${crypto.cryptoName})`, () => crypto.decrypt(this.buffer, this.options)); - // Check if we had a successful decryption - let audioExt = crypto.overrideExtension ?? (await this.detectAudioExtension(decrypted)); - if (crypto.checkByDecryptHeader && audioExt === 'bin') { - return null; + let audioExt = result.overrideExtension || (await detectAudioExtension(result.data)); + if (!result.overrideExtension && audioExt === 'bin') { + throw new UnsupportedSourceFile('unable to produce valid audio file'); } + + // Convert mp4 to m4a if (audioExt.toLowerCase() === 'mp4') { audioExt = 'm4a'; } - return { decrypted: URL.createObjectURL(toBlob(decrypted)), ext: audioExt }; - } - - async detectAudioExtension(data: Blob | ArrayBuffer): Promise { - return this.log(`detect-audio-ext`, async () => { - const header = await toArrayBuffer(data.slice(0, TEST_FILE_HEADER_LEN)); - return this.parakeet.detectAudioExtension(header); - }); - } - - async acceptByDecryptFileHeader(crypto: CryptoBase): Promise { - // File too small, ignore. - if (this.buffer.byteLength <= TEST_FILE_HEADER_LEN) { - return true; - } - - // Check by decrypt max first 8MiB - const decryptedBuffer = await this.log(`${crypto.cryptoName}/decrypt-header-test`, async () => - toArrayBuffer(await crypto.decrypt(this.buffer.slice(0, TEST_FILE_HEADER_LEN), this.options)), - ); - - return this.parakeet.detectAudioExtension(decryptedBuffer) !== 'bin'; + return { decrypted: URL.createObjectURL(toBlob(result.data)), ext: audioExt }; } } export const workerDecryptHandler = async ({ id, blobURI, options }: DecryptCommandPayload) => { + await umCryptoReady; const label = `decrypt(${id})`; return withTimeGroupedLogs(label, async () => { - await umCryptoReady; - const parakeet = await timedLogger(`${label}/init`, fetchParakeet); - const blob = await timedLogger(`${label}/fetch-src`, async () => fetch(blobURI).then((r) => r.blob())); - const buffer = await timedLogger(`${label}/read-src`, async () => blob.arrayBuffer()); - - const handler = new DecryptCommandHandler(id, parakeet, buffer, options); + const buffer = await fetch(blobURI).then((r) => r.arrayBuffer()); + const handler = new DecryptCommandHandler(id, new Uint8Array(buffer), options); return handler.decrypt(allCryptoFactories); }); }; diff --git a/src/features/file-listing/FileError.tsx b/src/features/file-listing/FileError.tsx index 62d8550..d2e6830 100644 --- a/src/features/file-listing/FileError.tsx +++ b/src/features/file-listing/FileError.tsx @@ -1,4 +1,4 @@ -import { chakra, Box, Button, Collapse, Text, useDisclosure } from '@chakra-ui/react'; +import { Box, Button, chakra, Collapse, Text, useDisclosure } from '@chakra-ui/react'; import { DecryptErrorType } from '~/decrypt-worker/util/DecryptError'; export interface FileErrorProps { @@ -18,11 +18,12 @@ export function FileError({ error, code }: FileErrorProps) { - 解密错误:{errorSummary} + 解密错误: + {errorSummary} {error && ( )} diff --git a/src/util/go.ts b/src/util/go.ts new file mode 100644 index 0000000..c152364 --- /dev/null +++ b/src/util/go.ts @@ -0,0 +1,7 @@ +export async function go(promise: Promise): Promise<[T, null] | [null, E]> { + try { + return [await promise, null]; + } catch (error: unknown) { + return [null, error as E]; + } +} -- 2.45.2 From a75ca7aabb37dd3d62fae7dcfaf350e02b058915 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=B2=81=E6=A0=91=E4=BA=BA?= Date: Sun, 15 Sep 2024 16:17:28 +0100 Subject: [PATCH 02/18] refactor: add qmc and kuwo from `@unlock-music/crypto` --- package.json | 2 +- pnpm-lock.yaml | 10 +-- src/decrypt-worker/Deciphers.ts | 10 ++- src/decrypt-worker/crypto/qmc/qmc_v2.ts | 51 ------------- src/decrypt-worker/decipher/KuwoMusic.ts | 35 +++++++++ .../decipher/NetEaseCloudMusic.ts | 15 ++-- src/decrypt-worker/decipher/QQMusic.ts | 74 +++++++++++++++++++ src/decrypt-worker/util/audioType.ts | 14 +++- src/decrypt-worker/worker/handler/decrypt.ts | 2 +- vite.config.ts | 1 + 10 files changed, 144 insertions(+), 70 deletions(-) delete mode 100644 src/decrypt-worker/crypto/qmc/qmc_v2.ts create mode 100644 src/decrypt-worker/decipher/KuwoMusic.ts create mode 100644 src/decrypt-worker/decipher/QQMusic.ts diff --git a/package.json b/package.json index fc7b2a6..b5afac9 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "@emotion/styled": "^11.11.0", "@reduxjs/toolkit": "^2.0.1", "@um/libparakeet": "0.4.5", - "@unlock-music/crypto": "0.0.0-alpha.10", + "@unlock-music/crypto": "0.0.0-alpha.11", "framer-motion": "^10.16.16", "nanoid": "^5.0.4", "radash": "^11.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 00785fd..492ae43 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -42,8 +42,8 @@ importers: specifier: 0.4.5 version: 0.4.5 '@unlock-music/crypto': - specifier: 0.0.0-alpha.10 - version: 0.0.0-alpha.10 + specifier: 0.0.0-alpha.11 + version: 0.0.0-alpha.11 framer-motion: specifier: ^10.16.16 version: 10.16.16(react-dom@18.2.0(react@18.2.0))(react@18.2.0) @@ -1927,8 +1927,8 @@ packages: '@ungap/structured-clone@1.2.0': resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} - '@unlock-music/crypto@0.0.0-alpha.10': - resolution: {integrity: sha512-Y8PWd/f4KEh2WU5Uz4QesnYMelDvioLYMOVvpPceMk62P2LivQLgvl5+ytDmD2yzmsnE/sXOQztTg+1WsuePCA==, tarball: https://git.unlock-music.dev/api/packages/um/npm/%40unlock-music%2Fcrypto/-/0.0.0-alpha.10/crypto-0.0.0-alpha.10.tgz} + '@unlock-music/crypto@0.0.0-alpha.11': + resolution: {integrity: sha512-lA3xryziHULhkPbuQFI2HrfwDREUD9YoaZOTMQqcu/8mKF2/hA3sCK0Uoq0miYr+7VUbE5sMBvl9dcrnCI1UWA==, tarball: https://git.unlock-music.dev/api/packages/um/npm/%40unlock-music%2Fcrypto/-/0.0.0-alpha.11/crypto-0.0.0-alpha.11.tgz} '@vitejs/plugin-react@4.2.1': resolution: {integrity: sha512-oojO9IDc4nCUUi8qIR11KoQm0XFFLIwsRBwHRR4d/88IWghn1y6ckz/bJ8GHDCsYEJee8mDzqtJxh15/cisJNQ==} @@ -6118,7 +6118,7 @@ snapshots: '@ungap/structured-clone@1.2.0': {} - '@unlock-music/crypto@0.0.0-alpha.10': {} + '@unlock-music/crypto@0.0.0-alpha.11': {} '@vitejs/plugin-react@4.2.1(vite@5.0.10(@types/node@20.10.5)(sass@1.69.5)(terser@5.27.0))': dependencies: diff --git a/src/decrypt-worker/Deciphers.ts b/src/decrypt-worker/Deciphers.ts index e77dbd4..d4782ec 100644 --- a/src/decrypt-worker/Deciphers.ts +++ b/src/decrypt-worker/Deciphers.ts @@ -1,6 +1,8 @@ import { NetEaseCloudMusicDecipher } from '~/decrypt-worker/decipher/NetEaseCloudMusic.ts'; import { TransparentDecipher } from './decipher/Transparent.ts'; import type { DecryptCommandOptions } from '~/decrypt-worker/types.ts'; +import { QQMusicV1Decipher, QQMusicV2Decipher } from '~/decrypt-worker/decipher/QQMusic.ts'; +import { KuwoMusicDecipher } from '~/decrypt-worker/decipher/KuwoMusic.ts'; export enum Status { OK = 0, @@ -41,7 +43,7 @@ export const allCryptoFactories: DecipherFactory[] = [ // KGMCrypto.make, // KWMv1 (*.kwm) - // KWMCrypto.make, + KuwoMusicDecipher.make, // Xiami (*.xm) // XiamiCrypto.make, @@ -49,8 +51,8 @@ export const allCryptoFactories: DecipherFactory[] = [ /// File with a fixed footer goes second // QMCv2 (*.mflac) - // QMC2CryptoWithKey.make, - // QMC2Crypto.make, + QQMusicV2Decipher.createWithUserKey, + QQMusicV2Decipher.createWithEmbeddedEKey, /// File without an obvious header or footer goes last. @@ -61,7 +63,7 @@ export const allCryptoFactories: DecipherFactory[] = [ // should be moved to the bottom of the list for performance reasons. // QMCv1 (*.qmcflac) - // QMC1Crypto.make, + QQMusicV1Decipher.create, // Ximalaya (Android) // XimalayaAndroidCrypto.makeX2M, diff --git a/src/decrypt-worker/crypto/qmc/qmc_v2.ts b/src/decrypt-worker/crypto/qmc/qmc_v2.ts deleted file mode 100644 index 2482a1d..0000000 --- a/src/decrypt-worker/crypto/qmc/qmc_v2.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { transformBlob } from '~/decrypt-worker/util/transformBlob'; -import type { DecipherInstance } from '~/decrypt-worker/Deciphers.ts'; -import type { DecryptCommandOptions } from '~/decrypt-worker/types.ts'; -import { fetchParakeet } from '@um/libparakeet'; -import { stringToUTF8Bytes } from '~/decrypt-worker/util/utf8Encoder.ts'; -import { makeQMCv2FooterParser, makeQMCv2KeyCrypto } from '~/decrypt-worker/util/qmc2KeyCrypto.ts'; - -export class QMC2Crypto implements DecipherInstance { - cryptoName = 'QMC/v2'; - checkByDecryptHeader = false; - - async decrypt(buffer: ArrayBuffer): Promise { - const parakeet = await fetchParakeet(); - const footerParser = makeQMCv2FooterParser(parakeet); - return transformBlob(buffer, (p) => p.make.QMCv2(footerParser), { - parakeet, - cleanup: () => footerParser.delete(), - }); - } - - public static make() { - return new QMC2Crypto(); - } -} - -export class QMC2CryptoWithKey implements DecipherInstance { - cryptoName = 'QMC/v2 (key)'; - checkByDecryptHeader = true; - - async checkBySignature(_buffer: ArrayBuffer, options: DecryptCommandOptions): Promise { - return Boolean(options.qmc2Key); - } - - async decrypt(buffer: ArrayBuffer, options: DecryptCommandOptions): Promise { - if (!options.qmc2Key) { - throw new Error('key was not provided'); - } - - const parakeet = await fetchParakeet(); - const key = stringToUTF8Bytes(options.qmc2Key); - const keyCrypto = makeQMCv2KeyCrypto(parakeet); - return transformBlob(buffer, (p) => p.make.QMCv2EKey(key, keyCrypto), { - parakeet, - cleanup: () => keyCrypto.delete(), - }); - } - - public static make() { - return new QMC2CryptoWithKey(); - } -} diff --git a/src/decrypt-worker/decipher/KuwoMusic.ts b/src/decrypt-worker/decipher/KuwoMusic.ts new file mode 100644 index 0000000..8520b30 --- /dev/null +++ b/src/decrypt-worker/decipher/KuwoMusic.ts @@ -0,0 +1,35 @@ +import { DecipherInstance, DecipherOK, DecipherResult, Status } from '~/decrypt-worker/Deciphers'; +import { KuwoHeader, KWMCipher } from '@unlock-music/crypto'; +import type { DecryptCommandOptions } from '~/decrypt-worker/types.ts'; +import { chunkBuffer } from '~/decrypt-worker/util/buffer.ts'; + +export class KuwoMusicDecipher implements DecipherInstance { + cipherName = 'Kuwo'; + + async decrypt(buffer: Uint8Array, options: DecryptCommandOptions): Promise { + let header: KuwoHeader | undefined; + let kwm: KWMCipher | undefined; + + try { + header = KuwoHeader.parse(buffer.subarray(0, 0x400)); + kwm = header.makeCipher(options.kwm2key); + + const audioBuffer = new Uint8Array(buffer.subarray(0x400)); + for (const [block, offset] of chunkBuffer(audioBuffer)) { + kwm.decrypt(block, offset); + } + return { + status: Status.OK, + cipherName: this.cipherName, + data: audioBuffer, + }; + } finally { + kwm?.free(); + header?.free(); + } + } + + public static make() { + return new KuwoMusicDecipher(); + } +} diff --git a/src/decrypt-worker/decipher/NetEaseCloudMusic.ts b/src/decrypt-worker/decipher/NetEaseCloudMusic.ts index f8b05aa..f3ecd1f 100644 --- a/src/decrypt-worker/decipher/NetEaseCloudMusic.ts +++ b/src/decrypt-worker/decipher/NetEaseCloudMusic.ts @@ -1,7 +1,6 @@ import { DecipherInstance, DecipherOK, DecipherResult, Status } from '~/decrypt-worker/Deciphers'; import { NCMFile } from '@unlock-music/crypto'; import { chunkBuffer } from '~/decrypt-worker/util/buffer.ts'; -import { withWasmClass } from '~/decrypt-worker/util/wasmClass.ts'; import { UnsupportedSourceFile } from '~/decrypt-worker/util/DecryptError.ts'; export class NetEaseCloudMusicDecipher implements DecipherInstance { @@ -18,12 +17,12 @@ export class NetEaseCloudMusicDecipher implements DecipherInstance { } } - async decrypt(buffer: ArrayBuffer): Promise { - return withWasmClass(new NCMFile(), async (ncm): Promise => { - const data = new Uint8Array(buffer); - this.tryInit(ncm, data); + async decrypt(buffer: Uint8Array): Promise { + const ncm = new NCMFile(); + try { + this.tryInit(ncm, buffer); - const audioBuffer = data.subarray(ncm.audioOffset); + const audioBuffer = buffer.slice(ncm.audioOffset); for (const [block, offset] of chunkBuffer(audioBuffer)) { ncm.decrypt(block, offset); } @@ -32,7 +31,9 @@ export class NetEaseCloudMusicDecipher implements DecipherInstance { cipherName: this.cipherName, data: audioBuffer, }; - }); + } finally { + ncm.free(); + } } public static make() { diff --git a/src/decrypt-worker/decipher/QQMusic.ts b/src/decrypt-worker/decipher/QQMusic.ts new file mode 100644 index 0000000..5e53455 --- /dev/null +++ b/src/decrypt-worker/decipher/QQMusic.ts @@ -0,0 +1,74 @@ +import { DecipherInstance, DecipherOK, DecipherResult, Status } from '~/decrypt-worker/Deciphers'; +import { decryptQMC1, QMC2, QMCFooter } from '@unlock-music/crypto'; +import { chunkBuffer } from '~/decrypt-worker/util/buffer.ts'; +import type { DecryptCommandOptions } from '~/decrypt-worker/types.ts'; +import { UnsupportedSourceFile } from '~/decrypt-worker/util/DecryptError.ts'; +import { isDataLooksLikeAudio } from '~/decrypt-worker/util/audioType.ts'; + +export class QQMusicV1Decipher implements DecipherInstance { + cipherName = 'QQMusic/QMC1'; + + async decrypt(buffer: Uint8Array): Promise { + const header = buffer.slice(0, 0x20); + decryptQMC1(header, 0); + if (!isDataLooksLikeAudio(header)) { + throw new UnsupportedSourceFile('does not look like QMC file'); + } + + const audioBuffer = new Uint8Array(buffer); + for (const [block, offset] of chunkBuffer(audioBuffer)) { + decryptQMC1(block, offset); + } + return { + status: Status.OK, + cipherName: this.cipherName, + data: audioBuffer, + }; + } + + public static create() { + return new QQMusicV1Decipher(); + } +} + +export class QQMusicV2Decipher implements DecipherInstance { + cipherName: string; + + constructor(private readonly useUserKey: boolean) { + this.cipherName = `QQMusic/QMC2(user_key=${+useUserKey})`; + } + + async decrypt(buffer: Uint8Array, options: DecryptCommandOptions): Promise { + const footer = QMCFooter.parse(buffer.subarray(buffer.byteLength - 1024)); + if (!footer) { + throw new UnsupportedSourceFile('Not QMC2 File'); + } + + const audioBuffer = buffer.slice(0, buffer.byteLength - footer.size); + const ekey = this.useUserKey ? options.qmc2Key : footer.ekey; + footer.free(); + if (!ekey) { + throw new Error('EKey missing'); + } + + const qmc2 = new QMC2(ekey); + for (const [block, offset] of chunkBuffer(audioBuffer)) { + qmc2.decrypt(block, offset); + } + qmc2.free(); + + return { + status: Status.OK, + cipherName: this.cipherName, + data: audioBuffer, + }; + } + + public static createWithUserKey() { + return new QQMusicV2Decipher(true); + } + + public static createWithEmbeddedEKey() { + return new QQMusicV2Decipher(false); + } +} diff --git a/src/decrypt-worker/util/audioType.ts b/src/decrypt-worker/util/audioType.ts index fa2f8b5..71b1b58 100644 --- a/src/decrypt-worker/util/audioType.ts +++ b/src/decrypt-worker/util/audioType.ts @@ -1,6 +1,6 @@ import { detectAudioType } from '@unlock-music/crypto'; -export async function detectAudioExtension(buffer: Uint8Array): Promise { +export function detectAudioExtension(buffer: Uint8Array): string { let neededLength = 0x100; let extension = 'bin'; while (neededLength !== 0) { @@ -12,3 +12,15 @@ export async function detectAudioExtension(buffer: Uint8Array): Promise } return extension; } + +export function isDataLooksLikeAudio(buffer: Uint8Array): boolean { + if (buffer.byteLength < 0x20) { + return false; + } + const detectResult = detectAudioType(buffer.subarray(0, 0x20)); + + // If we have needMore != 0, that means we have a valid header (ID3 for example). + const ok = detectResult.needMore !== 0 || detectResult.audioType !== 'bin'; + detectResult.free(); + return ok; +} diff --git a/src/decrypt-worker/worker/handler/decrypt.ts b/src/decrypt-worker/worker/handler/decrypt.ts index 53b94ac..7dc2a91 100644 --- a/src/decrypt-worker/worker/handler/decrypt.ts +++ b/src/decrypt-worker/worker/handler/decrypt.ts @@ -65,7 +65,7 @@ class DecryptCommandHandler { } // Check if we had a successful decryption - let audioExt = result.overrideExtension || (await detectAudioExtension(result.data)); + let audioExt = result.overrideExtension || detectAudioExtension(result.data); if (!result.overrideExtension && audioExt === 'bin') { throw new UnsupportedSourceFile('unable to produce valid audio file'); } diff --git a/vite.config.ts b/vite.config.ts index f90db5b..5770cce 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -35,6 +35,7 @@ export default defineConfig({ // 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', ], }, }, -- 2.45.2 From 7fef8da083f85fa8a21d8ce2e9275dc2c8567b9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=B2=81=E6=A0=91=E4=BA=BA?= Date: Mon, 16 Sep 2024 22:13:27 +0100 Subject: [PATCH 03/18] feat: add kgm support --- package.json | 2 +- pnpm-lock.yaml | 10 +++---- src/decrypt-worker/Deciphers.ts | 3 +- src/decrypt-worker/decipher/KugouMusic.ts | 36 +++++++++++++++++++++++ src/decrypt-worker/decipher/KuwoMusic.ts | 6 ++-- 5 files changed, 47 insertions(+), 10 deletions(-) create mode 100644 src/decrypt-worker/decipher/KugouMusic.ts diff --git a/package.json b/package.json index b5afac9..eee81c2 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "@emotion/styled": "^11.11.0", "@reduxjs/toolkit": "^2.0.1", "@um/libparakeet": "0.4.5", - "@unlock-music/crypto": "0.0.0-alpha.11", + "@unlock-music/crypto": "0.0.0-alpha.12", "framer-motion": "^10.16.16", "nanoid": "^5.0.4", "radash": "^11.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 492ae43..c88e5ad 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -42,8 +42,8 @@ importers: specifier: 0.4.5 version: 0.4.5 '@unlock-music/crypto': - specifier: 0.0.0-alpha.11 - version: 0.0.0-alpha.11 + specifier: 0.0.0-alpha.12 + version: 0.0.0-alpha.12 framer-motion: specifier: ^10.16.16 version: 10.16.16(react-dom@18.2.0(react@18.2.0))(react@18.2.0) @@ -1927,8 +1927,8 @@ packages: '@ungap/structured-clone@1.2.0': resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} - '@unlock-music/crypto@0.0.0-alpha.11': - resolution: {integrity: sha512-lA3xryziHULhkPbuQFI2HrfwDREUD9YoaZOTMQqcu/8mKF2/hA3sCK0Uoq0miYr+7VUbE5sMBvl9dcrnCI1UWA==, tarball: https://git.unlock-music.dev/api/packages/um/npm/%40unlock-music%2Fcrypto/-/0.0.0-alpha.11/crypto-0.0.0-alpha.11.tgz} + '@unlock-music/crypto@0.0.0-alpha.12': + resolution: {integrity: sha512-Q24cq653CmD8sj/D1M6wHYtXJIX3YIgnvbPtO+aHnY07J0ZXvkqNh+6a3hBrGGLYzcSWioAw2xxf2rFEQ3q35A==, tarball: https://git.unlock-music.dev/api/packages/um/npm/%40unlock-music%2Fcrypto/-/0.0.0-alpha.12/crypto-0.0.0-alpha.12.tgz} '@vitejs/plugin-react@4.2.1': resolution: {integrity: sha512-oojO9IDc4nCUUi8qIR11KoQm0XFFLIwsRBwHRR4d/88IWghn1y6ckz/bJ8GHDCsYEJee8mDzqtJxh15/cisJNQ==} @@ -6118,7 +6118,7 @@ snapshots: '@ungap/structured-clone@1.2.0': {} - '@unlock-music/crypto@0.0.0-alpha.11': {} + '@unlock-music/crypto@0.0.0-alpha.12': {} '@vitejs/plugin-react@4.2.1(vite@5.0.10(@types/node@20.10.5)(sass@1.69.5)(terser@5.27.0))': dependencies: diff --git a/src/decrypt-worker/Deciphers.ts b/src/decrypt-worker/Deciphers.ts index d4782ec..ace5f1c 100644 --- a/src/decrypt-worker/Deciphers.ts +++ b/src/decrypt-worker/Deciphers.ts @@ -3,6 +3,7 @@ import { TransparentDecipher } from './decipher/Transparent.ts'; import type { DecryptCommandOptions } from '~/decrypt-worker/types.ts'; import { QQMusicV1Decipher, QQMusicV2Decipher } from '~/decrypt-worker/decipher/QQMusic.ts'; import { KuwoMusicDecipher } from '~/decrypt-worker/decipher/KuwoMusic.ts'; +import { KugouMusicDecipher } from '~/decrypt-worker/decipher/KugouMusic.ts'; export enum Status { OK = 0, @@ -40,7 +41,7 @@ export const allCryptoFactories: DecipherFactory[] = [ NetEaseCloudMusicDecipher.make, // KGM (*.kgm, *.vpr) - // KGMCrypto.make, + KugouMusicDecipher.make, // KWMv1 (*.kwm) KuwoMusicDecipher.make, diff --git a/src/decrypt-worker/decipher/KugouMusic.ts b/src/decrypt-worker/decipher/KugouMusic.ts new file mode 100644 index 0000000..1f1fc3f --- /dev/null +++ b/src/decrypt-worker/decipher/KugouMusic.ts @@ -0,0 +1,36 @@ +import { DecipherInstance, DecipherOK, DecipherResult, Status } from '~/decrypt-worker/Deciphers'; +import { KuGouDecipher, KuGouHeader } from '@unlock-music/crypto'; +import type { DecryptCommandOptions } from '~/decrypt-worker/types.ts'; +import { chunkBuffer } from '~/decrypt-worker/util/buffer.ts'; + +export class KugouMusicDecipher implements DecipherInstance { + cipherName = 'Kugou'; + + async decrypt(buffer: Uint8Array, _options: DecryptCommandOptions): Promise { + let kgm: KuGouDecipher | undefined; + let header: KuGouHeader | undefined; + + try { + header = KuGouHeader.parse(buffer.subarray(0, 0x400)); + kgm = new KuGouDecipher(header); + + const audioBuffer = new Uint8Array(buffer.subarray(0x400)); + for (const [block, offset] of chunkBuffer(audioBuffer)) { + kgm.decrypt(block, offset); + } + + return { + status: Status.OK, + cipherName: this.cipherName, + data: audioBuffer, + }; + } finally { + kgm?.free(); + header?.free(); + } + } + + public static make() { + return new KugouMusicDecipher(); + } +} diff --git a/src/decrypt-worker/decipher/KuwoMusic.ts b/src/decrypt-worker/decipher/KuwoMusic.ts index 8520b30..b3e7369 100644 --- a/src/decrypt-worker/decipher/KuwoMusic.ts +++ b/src/decrypt-worker/decipher/KuwoMusic.ts @@ -1,5 +1,5 @@ import { DecipherInstance, DecipherOK, DecipherResult, Status } from '~/decrypt-worker/Deciphers'; -import { KuwoHeader, KWMCipher } from '@unlock-music/crypto'; +import { KuwoHeader, KWMDecipher } from '@unlock-music/crypto'; import type { DecryptCommandOptions } from '~/decrypt-worker/types.ts'; import { chunkBuffer } from '~/decrypt-worker/util/buffer.ts'; @@ -8,11 +8,11 @@ export class KuwoMusicDecipher implements DecipherInstance { async decrypt(buffer: Uint8Array, options: DecryptCommandOptions): Promise { let header: KuwoHeader | undefined; - let kwm: KWMCipher | undefined; + let kwm: KWMDecipher | undefined; try { header = KuwoHeader.parse(buffer.subarray(0, 0x400)); - kwm = header.makeCipher(options.kwm2key); + kwm = new KWMDecipher(header, options.kwm2key); const audioBuffer = new Uint8Array(buffer.subarray(0x400)); for (const [block, offset] of chunkBuffer(audioBuffer)) { -- 2.45.2 From 1b116a8db3576c868238d611153faa7f9805cff4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=B2=81=E6=A0=91=E4=BA=BA?= Date: Mon, 16 Sep 2024 22:27:45 +0100 Subject: [PATCH 04/18] refactor: simplify logging and catch errors --- src/decrypt-worker/worker/handler/decrypt.ts | 7 +++--- .../worker/handler/qmcv2_parser.ts | 24 ++++++------------- src/util/fnWrapper.ts | 21 ++++++---------- src/util/logUtils.ts | 6 ++--- 4 files changed, 21 insertions(+), 37 deletions(-) diff --git a/src/decrypt-worker/worker/handler/decrypt.ts b/src/decrypt-worker/worker/handler/decrypt.ts index 7dc2a91..18d7727 100644 --- a/src/decrypt-worker/worker/handler/decrypt.ts +++ b/src/decrypt-worker/worker/handler/decrypt.ts @@ -19,7 +19,7 @@ class DecryptCommandHandler { this.label = `DecryptCommandHandler(${label})`; } - log(label: string, fn: () => R): R { + log(label: string, fn: () => Promise): Promise { return timedLogger(`${this.label}: ${label}`, fn); } @@ -52,7 +52,7 @@ class DecryptCommandHandler { } async tryDecryptWith(decipher: DecipherInstance) { - const result = await this.log(`decrypt ${decipher.cipherName}`, async () => + const result = await this.log(`try decrypt with ${decipher.cipherName}`, async () => decipher.decrypt(this.buffer, this.options), ); switch (result.status) { @@ -79,8 +79,9 @@ class DecryptCommandHandler { } } -export const workerDecryptHandler = async ({ id, blobURI, options }: DecryptCommandPayload) => { +export const workerDecryptHandler = async ({ id: payloadId, blobURI, options }: DecryptCommandPayload) => { await umCryptoReady; + const id = payloadId.replace('://', ':'); const label = `decrypt(${id})`; return withTimeGroupedLogs(label, async () => { const buffer = await fetch(blobURI).then((r) => r.arrayBuffer()); diff --git a/src/decrypt-worker/worker/handler/qmcv2_parser.ts b/src/decrypt-worker/worker/handler/qmcv2_parser.ts index f232133..89db7ae 100644 --- a/src/decrypt-worker/worker/handler/qmcv2_parser.ts +++ b/src/decrypt-worker/worker/handler/qmcv2_parser.ts @@ -1,26 +1,16 @@ import { fetchParakeet, FooterParserState } from '@um/libparakeet'; import type { FetchMusicExNamePayload } from '~/decrypt-worker/types'; import { makeQMCv2FooterParser } from '~/decrypt-worker/util/qmc2KeyCrypto'; -import { timedLogger, withGroupedLogs as withTimeGroupedLogs } from '~/util/logUtils'; +import { timedLogger } from '~/util/logUtils.ts'; export const workerParseMusicExMediaName = async ({ id, blobURI }: FetchMusicExNamePayload) => { - const label = `decrypt(${id})`; - return withTimeGroupedLogs(label, async () => { - const parakeet = await timedLogger(`${label}/init`, fetchParakeet); - const blob = await timedLogger(`${label}/fetch-src`, async () => - fetch(blobURI, { headers: { Range: 'bytes=-1024' } }).then((r) => r.blob()), - ); + const label = `qmcMusixEx(${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 buffer = await timedLogger(`${label}/read-src`, async () => { - // Firefox: the range header does not work...? - const blobBuffer = await blob.arrayBuffer(); - if (blobBuffer.byteLength > 1024) { - return blobBuffer.slice(-1024); - } - return blobBuffer; - }); - - const parsed = makeQMCv2FooterParser(parakeet).parse(buffer); + const parsed = makeQMCv2FooterParser(parakeet).parse(buffer.slice(-1024)); if (parsed.state === FooterParserState.OK) { return parsed.mediaName; } diff --git a/src/util/fnWrapper.ts b/src/util/fnWrapper.ts index 8c3bdf4..b830a6c 100644 --- a/src/util/fnWrapper.ts +++ b/src/util/fnWrapper.ts @@ -1,20 +1,13 @@ -function isPromise(p: unknown): p is Promise { - return !!p && typeof p === 'object' && 'then' in p && 'catch' in p && 'finally' in p; -} - -export function wrapFunctionCall(pre: () => void, post: () => void, fn: () => R): R { +export async function wrapFunctionCall( + pre: () => void, + post: () => void, + fn: () => Promise, +): Promise { pre(); try { - const result = fn(); - - if (isPromise(result)) { - result.finally(post); - } - - return result; - } catch (e) { + return await fn(); + } finally { post(); - throw e; } } diff --git a/src/util/logUtils.ts b/src/util/logUtils.ts index d691faa..68d533d 100644 --- a/src/util/logUtils.ts +++ b/src/util/logUtils.ts @@ -1,6 +1,6 @@ import { wrapFunctionCall } from './fnWrapper'; -export function timedLogger(label: string, fn: () => R): R { +export async function timedLogger(label: string, fn: () => Promise): Promise { if (import.meta.env.VITE_ENABLE_PERF_LOG !== '1') { return fn(); } else { @@ -12,13 +12,13 @@ export function timedLogger(label: string, fn: () => R): R { } } -export function withGroupedLogs(label: string, fn: () => R): R { +export async function withGroupedLogs(label: string, fn: () => Promise): Promise { if (import.meta.env.VITE_ENABLE_PERF_LOG !== '1') { return fn(); } else { return wrapFunctionCall( () => console.group(label), - () => (console.groupEnd as (label: string) => void)(label), + () => console.groupEnd(), () => timedLogger(`${label}/total`, fn), ); } -- 2.45.2 From 73bad51e8e2afb025b97b1543f8b11a8dc387469 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=B2=81=E6=A0=91=E4=BA=BA?= Date: Wed, 18 Sep 2024 22:05:05 +0100 Subject: [PATCH 05/18] refactor: simplify kgm decipher --- src/decrypt-worker/decipher/KugouMusic.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/decrypt-worker/decipher/KugouMusic.ts b/src/decrypt-worker/decipher/KugouMusic.ts index 1f1fc3f..567975f 100644 --- a/src/decrypt-worker/decipher/KugouMusic.ts +++ b/src/decrypt-worker/decipher/KugouMusic.ts @@ -1,5 +1,5 @@ import { DecipherInstance, DecipherOK, DecipherResult, Status } from '~/decrypt-worker/Deciphers'; -import { KuGouDecipher, KuGouHeader } from '@unlock-music/crypto'; +import { KuGou } from '@unlock-music/crypto'; import type { DecryptCommandOptions } from '~/decrypt-worker/types.ts'; import { chunkBuffer } from '~/decrypt-worker/util/buffer.ts'; @@ -7,12 +7,10 @@ export class KugouMusicDecipher implements DecipherInstance { cipherName = 'Kugou'; async decrypt(buffer: Uint8Array, _options: DecryptCommandOptions): Promise { - let kgm: KuGouDecipher | undefined; - let header: KuGouHeader | undefined; + let kgm: KuGou | undefined; try { - header = KuGouHeader.parse(buffer.subarray(0, 0x400)); - kgm = new KuGouDecipher(header); + kgm = KuGou.from_header(buffer.subarray(0, 0x400)); const audioBuffer = new Uint8Array(buffer.subarray(0x400)); for (const [block, offset] of chunkBuffer(audioBuffer)) { @@ -26,7 +24,6 @@ export class KugouMusicDecipher implements DecipherInstance { }; } finally { kgm?.free(); - header?.free(); } } -- 2.45.2 From e8b220b3df8f122df8b6fd0e6b09abd32225c0a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=B2=81=E6=A0=91=E4=BA=BA?= Date: Wed, 18 Sep 2024 23:02:05 +0100 Subject: [PATCH 06/18] feat: add xmly support --- src/decrypt-worker/Deciphers.ts | 8 ++- src/decrypt-worker/decipher/Ximalaya.ts | 70 +++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 2 deletions(-) create mode 100644 src/decrypt-worker/decipher/Ximalaya.ts diff --git a/src/decrypt-worker/Deciphers.ts b/src/decrypt-worker/Deciphers.ts index ace5f1c..a98b6ac 100644 --- a/src/decrypt-worker/Deciphers.ts +++ b/src/decrypt-worker/Deciphers.ts @@ -4,6 +4,7 @@ import type { DecryptCommandOptions } from '~/decrypt-worker/types.ts'; import { QQMusicV1Decipher, QQMusicV2Decipher } from '~/decrypt-worker/decipher/QQMusic.ts'; import { KuwoMusicDecipher } from '~/decrypt-worker/decipher/KuwoMusic.ts'; import { KugouMusicDecipher } from '~/decrypt-worker/decipher/KugouMusic.ts'; +import { XimalayaAndroidDecipher, XimalayaPCDecipher } from '~/decrypt-worker/decipher/Ximalaya.ts'; export enum Status { OK = 0, @@ -46,6 +47,9 @@ export const allCryptoFactories: DecipherFactory[] = [ // KWMv1 (*.kwm) KuwoMusicDecipher.make, + // Ximalaya PC (*.xm) + XimalayaPCDecipher.make, + // Xiami (*.xm) // XiamiCrypto.make, @@ -67,8 +71,8 @@ export const allCryptoFactories: DecipherFactory[] = [ QQMusicV1Decipher.create, // Ximalaya (Android) - // XimalayaAndroidCrypto.makeX2M, - // XimalayaAndroidCrypto.makeX3M, + XimalayaAndroidDecipher.makeX2M, + XimalayaAndroidDecipher.makeX3M, // QingTingFM (Android) // QingTingFM$Device.make, diff --git a/src/decrypt-worker/decipher/Ximalaya.ts b/src/decrypt-worker/decipher/Ximalaya.ts new file mode 100644 index 0000000..fae3734 --- /dev/null +++ b/src/decrypt-worker/decipher/Ximalaya.ts @@ -0,0 +1,70 @@ +import { DecipherInstance, DecipherOK, DecipherResult, Status } from '~/decrypt-worker/Deciphers'; +import type { DecryptCommandOptions } from '~/decrypt-worker/types.ts'; +import { decryptX2MHeader, decryptX3MHeader, XmlyPC } from '@unlock-music/crypto'; +import { isDataLooksLikeAudio } from '~/decrypt-worker/util/audioType.ts'; +import { UnsupportedSourceFile } from '~/decrypt-worker/util/DecryptError.ts'; + +export class XimalayaAndroidDecipher implements DecipherInstance { + cipherName: string; + + constructor( + private decipher: (buffer: Uint8Array) => void, + private cipherType: string, + ) { + this.cipherName = `Ximalaya (Android, ${cipherType})`; + } + + async decrypt(buffer: Uint8Array, _options: DecryptCommandOptions): Promise { + // Detect with first 0x400 bytes + const slice = buffer.slice(0, 0x400); + this.decipher(slice); + if (!isDataLooksLikeAudio(slice)) { + throw new UnsupportedSourceFile(`Not a Xmly android file (${this.cipherType})`); + } + const result = new Uint8Array(buffer); + result.set(slice, 0); + return { + cipherName: this.cipherName, + status: Status.OK, + data: result, + }; + } + + public static makeX2M() { + return new XimalayaAndroidDecipher(decryptX2MHeader, 'X2M'); + } + + public static makeX3M() { + return new XimalayaAndroidDecipher(decryptX3MHeader, 'X3M'); + } +} + +export class XimalayaPCDecipher implements DecipherInstance { + cipherName = 'Ximalaya (PC)'; + + async decrypt(buffer: Uint8Array, _options: DecryptCommandOptions): Promise { + // Detect with first 0x400 bytes + const headerSize = XmlyPC.getHeaderSize(buffer.subarray(0, 1024)); + const xm = new XmlyPC(buffer.subarray(0, headerSize)); + const { audioHeader, encryptedHeaderOffset, encryptedHeaderSize } = xm; + const plainAudioDataOffset = encryptedHeaderOffset + encryptedHeaderSize; + const plainAudioDataLength = buffer.byteLength - plainAudioDataOffset; + const encryptedAudioPart = buffer.slice(encryptedHeaderOffset, plainAudioDataOffset); + const encryptedAudioPartLen = xm.decrypt(encryptedAudioPart); + const audioSize = audioHeader.byteLength + encryptedAudioPartLen + plainAudioDataLength; + + const result = new Uint8Array(audioSize); + result.set(audioHeader); + result.set(encryptedAudioPart, audioHeader.byteLength); + result.set(buffer.subarray(plainAudioDataOffset), audioHeader.byteLength + encryptedAudioPartLen); + return { + status: Status.OK, + data: result, + cipherName: this.cipherName, + }; + } + + public static make() { + return new XimalayaPCDecipher(); + } +} -- 2.45.2 From 1cdd68e448a8a3a0e50338e35de3d41d5c944ede Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=B2=81=E6=A0=91=E4=BA=BA?= Date: Wed, 18 Sep 2024 23:30:04 +0100 Subject: [PATCH 07/18] [xmly] chore: remember to free after work --- src/decrypt-worker/decipher/Ximalaya.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/decrypt-worker/decipher/Ximalaya.ts b/src/decrypt-worker/decipher/Ximalaya.ts index fae3734..17edc4c 100644 --- a/src/decrypt-worker/decipher/Ximalaya.ts +++ b/src/decrypt-worker/decipher/Ximalaya.ts @@ -52,6 +52,7 @@ export class XimalayaPCDecipher implements DecipherInstance { const encryptedAudioPart = buffer.slice(encryptedHeaderOffset, plainAudioDataOffset); const encryptedAudioPartLen = xm.decrypt(encryptedAudioPart); const audioSize = audioHeader.byteLength + encryptedAudioPartLen + plainAudioDataLength; + xm.free(); const result = new Uint8Array(audioSize); result.set(audioHeader); -- 2.45.2 From a3fbf9662bfc320c239f3460d5d34bcead11d2ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=B2=81=E6=A0=91=E4=BA=BA?= Date: Wed, 18 Sep 2024 23:30:31 +0100 Subject: [PATCH 08/18] [xiami] feat: add xiami support --- src/decrypt-worker/Deciphers.ts | 3 ++- src/decrypt-worker/decipher/XiamiMusic.ts | 28 +++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 src/decrypt-worker/decipher/XiamiMusic.ts diff --git a/src/decrypt-worker/Deciphers.ts b/src/decrypt-worker/Deciphers.ts index a98b6ac..5aef2b1 100644 --- a/src/decrypt-worker/Deciphers.ts +++ b/src/decrypt-worker/Deciphers.ts @@ -5,6 +5,7 @@ import { QQMusicV1Decipher, QQMusicV2Decipher } from '~/decrypt-worker/decipher/ import { KuwoMusicDecipher } from '~/decrypt-worker/decipher/KuwoMusic.ts'; import { KugouMusicDecipher } from '~/decrypt-worker/decipher/KugouMusic.ts'; import { XimalayaAndroidDecipher, XimalayaPCDecipher } from '~/decrypt-worker/decipher/Ximalaya.ts'; +import { XiamiDecipher } from '~/decrypt-worker/decipher/XiamiMusic.ts'; export enum Status { OK = 0, @@ -51,7 +52,7 @@ export const allCryptoFactories: DecipherFactory[] = [ XimalayaPCDecipher.make, // Xiami (*.xm) - // XiamiCrypto.make, + XiamiDecipher.make, /// File with a fixed footer goes second diff --git a/src/decrypt-worker/decipher/XiamiMusic.ts b/src/decrypt-worker/decipher/XiamiMusic.ts new file mode 100644 index 0000000..778d916 --- /dev/null +++ b/src/decrypt-worker/decipher/XiamiMusic.ts @@ -0,0 +1,28 @@ +import { DecipherInstance, DecipherOK, DecipherResult, Status } from '~/decrypt-worker/Deciphers.ts'; +import { Xiami } from '@unlock-music/crypto'; +import { chunkBuffer } from '~/decrypt-worker/util/buffer.ts'; + +export class XiamiDecipher implements DecipherInstance { + cipherName = 'Xiami (XM)'; + + async decrypt(buffer: Uint8Array): Promise { + const xm = Xiami.from_header(buffer.subarray(0, 0x10)); + const { copyPlainLength } = xm; + const audioBuffer = buffer.slice(0x10); + + for (const [block] of chunkBuffer(audioBuffer.subarray(copyPlainLength))) { + xm.decrypt(block); + } + xm.free(); + + return { + cipherName: this.cipherName, + status: Status.OK, + data: audioBuffer, + }; + } + + public static make() { + return new XiamiDecipher(); + } +} -- 2.45.2 From 62d21ad39378096b5ddb67b3339946b173c52308 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=B2=81=E6=A0=91=E4=BA=BA?= Date: Wed, 18 Sep 2024 23:34:49 +0100 Subject: [PATCH 09/18] chore: bump um-crypto version --- package.json | 2 +- pnpm-lock.yaml | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index eee81c2..21f9436 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "@emotion/styled": "^11.11.0", "@reduxjs/toolkit": "^2.0.1", "@um/libparakeet": "0.4.5", - "@unlock-music/crypto": "0.0.0-alpha.12", + "@unlock-music/crypto": "0.0.0-alpha.13", "framer-motion": "^10.16.16", "nanoid": "^5.0.4", "radash": "^11.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c88e5ad..85bd60f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -42,8 +42,8 @@ importers: specifier: 0.4.5 version: 0.4.5 '@unlock-music/crypto': - specifier: 0.0.0-alpha.12 - version: 0.0.0-alpha.12 + specifier: 0.0.0-alpha.13 + version: 0.0.0-alpha.13 framer-motion: specifier: ^10.16.16 version: 10.16.16(react-dom@18.2.0(react@18.2.0))(react@18.2.0) @@ -1927,8 +1927,8 @@ packages: '@ungap/structured-clone@1.2.0': resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} - '@unlock-music/crypto@0.0.0-alpha.12': - resolution: {integrity: sha512-Q24cq653CmD8sj/D1M6wHYtXJIX3YIgnvbPtO+aHnY07J0ZXvkqNh+6a3hBrGGLYzcSWioAw2xxf2rFEQ3q35A==, tarball: https://git.unlock-music.dev/api/packages/um/npm/%40unlock-music%2Fcrypto/-/0.0.0-alpha.12/crypto-0.0.0-alpha.12.tgz} + '@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} '@vitejs/plugin-react@4.2.1': resolution: {integrity: sha512-oojO9IDc4nCUUi8qIR11KoQm0XFFLIwsRBwHRR4d/88IWghn1y6ckz/bJ8GHDCsYEJee8mDzqtJxh15/cisJNQ==} @@ -6118,7 +6118,7 @@ snapshots: '@ungap/structured-clone@1.2.0': {} - '@unlock-music/crypto@0.0.0-alpha.12': {} + '@unlock-music/crypto@0.0.0-alpha.13': {} '@vitejs/plugin-react@4.2.1(vite@5.0.10(@types/node@20.10.5)(sass@1.69.5)(terser@5.27.0))': dependencies: -- 2.45.2 From 6f229ff39e49a8d11bddb07995e1c6c32a1f3e17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=B2=81=E6=A0=91=E4=BA=BA?= Date: Thu, 19 Sep 2024 00:07:01 +0100 Subject: [PATCH 10/18] refactor: remove parakeet --- README.MD | 10 ++-- docs/develop-with-libparakeet.zh.md | 50 ------------------ docs/develop-with-um_crypto.zh.md | 36 +++++++++++++ package.json | 3 +- pnpm-lock.yaml | 18 ++----- src/decrypt-worker/crypto/kgm/kgm_pc.key.ts | 6 --- src/decrypt-worker/crypto/kgm/kgm_pc.ts | 18 ------- .../kgm_type4_file_key_expansion_table.txt | 1 - .../kgm_type4_slot_key_expansion_table.txt | 1 - src/decrypt-worker/crypto/kwm/kwm.key.ts | 1 - src/decrypt-worker/crypto/kwm/kwm.ts | 28 ---------- .../crypto/migu/migu3d_keyless.ts | 15 ------ src/decrypt-worker/crypto/qmc/qmc_v1.key.ts | 16 ------ src/decrypt-worker/crypto/qmc/qmc_v1.ts | 16 ------ src/decrypt-worker/crypto/qmc/qmc_v2.key.ts | 3 -- src/decrypt-worker/crypto/qtfm/qtfm_device.ts | 25 --------- src/decrypt-worker/crypto/xiami/xiami.ts | 51 ------------------- .../crypto/xmly/xmly_android.key.ts | 17 ------- .../crypto/xmly/xmly_android.ts | 30 ----------- src/decrypt-worker/util/qmc2KeyCrypto.ts | 5 -- src/decrypt-worker/util/transformBlob.ts | 38 -------------- src/decrypt-worker/worker.ts | 5 +- .../worker/handler/qmcv2_parser.ts | 19 ++++--- .../settings/panels/PanelQMCv2Key.tsx | 4 +- .../settings/panels/PanelQingTing.tsx | 5 +- vite.config.ts | 5 +- 26 files changed, 64 insertions(+), 362 deletions(-) delete mode 100644 docs/develop-with-libparakeet.zh.md create mode 100644 docs/develop-with-um_crypto.zh.md delete mode 100644 src/decrypt-worker/crypto/kgm/kgm_pc.key.ts delete mode 100644 src/decrypt-worker/crypto/kgm/kgm_pc.ts delete mode 100644 src/decrypt-worker/crypto/kgm/kgm_type4_file_key_expansion_table.txt delete mode 100644 src/decrypt-worker/crypto/kgm/kgm_type4_slot_key_expansion_table.txt delete mode 100644 src/decrypt-worker/crypto/kwm/kwm.key.ts delete mode 100644 src/decrypt-worker/crypto/kwm/kwm.ts delete mode 100644 src/decrypt-worker/crypto/migu/migu3d_keyless.ts delete mode 100644 src/decrypt-worker/crypto/qmc/qmc_v1.key.ts delete mode 100644 src/decrypt-worker/crypto/qmc/qmc_v1.ts delete mode 100644 src/decrypt-worker/crypto/qmc/qmc_v2.key.ts delete mode 100644 src/decrypt-worker/crypto/qtfm/qtfm_device.ts delete mode 100644 src/decrypt-worker/crypto/xiami/xiami.ts delete mode 100644 src/decrypt-worker/crypto/xmly/xmly_android.key.ts delete mode 100644 src/decrypt-worker/crypto/xmly/xmly_android.ts delete mode 100644 src/decrypt-worker/util/qmc2KeyCrypto.ts delete mode 100644 src/decrypt-worker/util/transformBlob.ts diff --git a/README.MD b/README.MD index 7db7c9e..5e3c781 100644 --- a/README.MD +++ b/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 diff --git a/docs/develop-with-libparakeet.zh.md b/docs/develop-with-libparakeet.zh.md deleted file mode 100644 index dc74b10..0000000 --- a/docs/develop-with-libparakeet.zh.md +++ /dev/null @@ -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 更改已合并。 diff --git a/docs/develop-with-um_crypto.zh.md b/docs/develop-with-um_crypto.zh.md new file mode 100644 index 0000000..0dcfccd --- /dev/null +++ b/docs/develop-with-um_crypto.zh.md @@ -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 更改已合并。 diff --git a/package.json b/package.json index 21f9436..f51f2b8 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 85bd60f..4265975 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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: diff --git a/src/decrypt-worker/crypto/kgm/kgm_pc.key.ts b/src/decrypt-worker/crypto/kgm/kgm_pc.key.ts deleted file mode 100644 index 5522951..0000000 --- a/src/decrypt-worker/crypto/kgm/kgm_pc.key.ts +++ /dev/null @@ -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(); diff --git a/src/decrypt-worker/crypto/kgm/kgm_pc.ts b/src/decrypt-worker/crypto/kgm/kgm_pc.ts deleted file mode 100644 index 0ddd8c9..0000000 --- a/src/decrypt-worker/crypto/kgm/kgm_pc.ts +++ /dev/null @@ -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 { - 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(); - } -} diff --git a/src/decrypt-worker/crypto/kgm/kgm_type4_file_key_expansion_table.txt b/src/decrypt-worker/crypto/kgm/kgm_type4_file_key_expansion_table.txt deleted file mode 100644 index 9c976ef..0000000 --- a/src/decrypt-worker/crypto/kgm/kgm_type4_file_key_expansion_table.txt +++ /dev/null @@ -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 diff --git a/src/decrypt-worker/crypto/kgm/kgm_type4_slot_key_expansion_table.txt b/src/decrypt-worker/crypto/kgm/kgm_type4_slot_key_expansion_table.txt deleted file mode 100644 index 63eb3bc..0000000 --- a/src/decrypt-worker/crypto/kgm/kgm_type4_slot_key_expansion_table.txt +++ /dev/null @@ -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 { - 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(); - } -} diff --git a/src/decrypt-worker/crypto/migu/migu3d_keyless.ts b/src/decrypt-worker/crypto/migu/migu3d_keyless.ts deleted file mode 100644 index 4f0bece..0000000 --- a/src/decrypt-worker/crypto/migu/migu3d_keyless.ts +++ /dev/null @@ -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 { - return transformBlob(buffer, (p) => p.make.Migu3D()); - } - - public static make() { - return new MiguCrypto(); - } -} diff --git a/src/decrypt-worker/crypto/qmc/qmc_v1.key.ts b/src/decrypt-worker/crypto/qmc/qmc_v1.key.ts deleted file mode 100644 index 139aba6..0000000 --- a/src/decrypt-worker/crypto/qmc/qmc_v1.key.ts +++ /dev/null @@ -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, -]); diff --git a/src/decrypt-worker/crypto/qmc/qmc_v1.ts b/src/decrypt-worker/crypto/qmc/qmc_v1.ts deleted file mode 100644 index f955820..0000000 --- a/src/decrypt-worker/crypto/qmc/qmc_v1.ts +++ /dev/null @@ -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 { - return transformBlob(buffer, (p) => p.make.QMCv1(key)); - } - - public static make() { - return new QMC1Crypto(); - } -} diff --git a/src/decrypt-worker/crypto/qmc/qmc_v2.key.ts b/src/decrypt-worker/crypto/qmc/qmc_v2.key.ts deleted file mode 100644 index a169f15..0000000 --- a/src/decrypt-worker/crypto/qmc/qmc_v2.key.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const SEED = 106; -export const ENC_V2_KEY_1 = '386ZJY!@#*$%^&)('; -export const ENC_V2_KEY_2 = '**#!(#$%&^a1cZ,T'; diff --git a/src/decrypt-worker/crypto/qtfm/qtfm_device.ts b/src/decrypt-worker/crypto/qtfm/qtfm_device.ts deleted file mode 100644 index 205d156..0000000 --- a/src/decrypt-worker/crypto/qtfm/qtfm_device.ts +++ /dev/null @@ -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 { - 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(); - } -} diff --git a/src/decrypt-worker/crypto/xiami/xiami.ts b/src/decrypt-worker/crypto/xiami/xiami.ts deleted file mode 100644 index 392d0ca..0000000 --- a/src/decrypt-worker/crypto/xiami/xiami.ts +++ /dev/null @@ -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 { - 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 { - 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(); - } -} diff --git a/src/decrypt-worker/crypto/xmly/xmly_android.key.ts b/src/decrypt-worker/crypto/xmly/xmly_android.key.ts deleted file mode 100644 index 956de25..0000000 --- a/src/decrypt-worker/crypto/xmly/xmly_android.key.ts +++ /dev/null @@ -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, -}; diff --git a/src/decrypt-worker/crypto/xmly/xmly_android.ts b/src/decrypt-worker/crypto/xmly/xmly_android.ts deleted file mode 100644 index 9d4ae65..0000000 --- a/src/decrypt-worker/crypto/xmly/xmly_android.ts +++ /dev/null @@ -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 { - 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); - } -} diff --git a/src/decrypt-worker/util/qmc2KeyCrypto.ts b/src/decrypt-worker/util/qmc2KeyCrypto.ts deleted file mode 100644 index a318b20..0000000 --- a/src/decrypt-worker/util/qmc2KeyCrypto.ts +++ /dev/null @@ -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); diff --git a/src/decrypt-worker/util/transformBlob.ts b/src/decrypt-worker/util/transformBlob.ts deleted file mode 100644 index 074dd12..0000000 --- a/src/decrypt-worker/util/transformBlob.ts +++ /dev/null @@ -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, - { 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()); - } -} diff --git a/src/decrypt-worker/worker.ts b/src/decrypt-worker/worker.ts index fceb08b..1c5c712 100644 --- a/src/decrypt-worker/worker.ts +++ b/src/decrypt-worker/worker.ts @@ -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); diff --git a/src/decrypt-worker/worker/handler/qmcv2_parser.ts b/src/decrypt-worker/worker/handler/qmcv2_parser.ts index 89db7ae..aea0098 100644 --- a/src/decrypt-worker/worker/handler/qmcv2_parser.ts +++ b/src/decrypt-worker/worker/handler/qmcv2_parser.ts @@ -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 arrayBuffer = await blob.arrayBuffer(); - const parsed = makeQMCv2FooterParser(parakeet).parse(buffer.slice(-1024)); - if (parsed.state === FooterParserState.OK) { - return parsed.mediaName; + try { + const buffer = new Uint8Array(arrayBuffer.slice(-1024)); + const footer = QMCFooter.parse(buffer); + return footer?.mediaName || null; + } catch { + return null; } - - return null; }); }; diff --git a/src/features/settings/panels/PanelQMCv2Key.tsx b/src/features/settings/panels/PanelQMCv2Key.tsx index 8added4..9291a53 100644 --- a/src/features/settings/panels/PanelQMCv2Key.tsx +++ b/src/features/settings/panels/PanelQMCv2Key.tsx @@ -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 })); diff --git a/src/features/settings/panels/PanelQingTing.tsx b/src/features/settings/panels/PanelQingTing.tsx index 305a3ad..3089526 100644 --- a/src/features/settings/panels/PanelQingTing.tsx +++ b/src/features/settings/panels/PanelQingTing.tsx @@ -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!'); } }; diff --git a/vite.config.ts b/vite.config.ts index 5770cce..a27bf7e 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -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'), }, }, -- 2.45.2 From ddc96283052ff10bedb9d95299d9a7ddadb4cf39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=B2=81=E6=A0=91=E4=BA=BA?= Date: Fri, 20 Sep 2024 21:14:47 +0100 Subject: [PATCH 11/18] feat: add qtfm decipher --- pnpm-lock.yaml | 7 +--- src/decrypt-worker/Deciphers.ts | 7 ++-- src/decrypt-worker/constants.ts | 1 + src/decrypt-worker/decipher/QingTingFM.ts | 37 +++++++++++++++++++ src/decrypt-worker/types.ts | 9 +++++ src/decrypt-worker/worker.ts | 6 ++- .../worker/{handler => }/decrypt.ts | 12 +++--- .../worker/{handler => }/qmcv2_parser.ts | 2 +- src/decrypt-worker/worker/qtfm_device_key.ts | 15 ++++++++ .../settings/panels/PanelQingTing.tsx | 37 +++++++++---------- src/util/MMKVParser.ts | 2 +- src/util/formatHex.ts | 3 -- src/util/hex.ts | 15 ++++++++ 13 files changed, 111 insertions(+), 42 deletions(-) create mode 100644 src/decrypt-worker/decipher/QingTingFM.ts rename src/decrypt-worker/worker/{handler => }/decrypt.ts (91%) rename src/decrypt-worker/worker/{handler => }/qmcv2_parser.ts (98%) create mode 100644 src/decrypt-worker/worker/qtfm_device_key.ts delete mode 100644 src/util/formatHex.ts create mode 100644 src/util/hex.ts diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4265975..8eb7f5a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -40,7 +40,7 @@ importers: 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) '@unlock-music/crypto': specifier: 0.0.0-alpha.15 - version: 0.0.0-alpha.15 + version: link:../lib_um_crypto_rust/um_wasm_loader framer-motion: specifier: ^10.16.16 version: 10.16.16(react-dom@18.2.0(react@18.2.0))(react@18.2.0) @@ -1921,9 +1921,6 @@ packages: '@ungap/structured-clone@1.2.0': resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} - '@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==} engines: {node: ^14.18.0 || >=16.0.0} @@ -6110,8 +6107,6 @@ snapshots: '@ungap/structured-clone@1.2.0': {} - '@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: '@babel/core': 7.23.6 diff --git a/src/decrypt-worker/Deciphers.ts b/src/decrypt-worker/Deciphers.ts index 5aef2b1..d11da39 100644 --- a/src/decrypt-worker/Deciphers.ts +++ b/src/decrypt-worker/Deciphers.ts @@ -6,6 +6,7 @@ import { KuwoMusicDecipher } from '~/decrypt-worker/decipher/KuwoMusic.ts'; import { KugouMusicDecipher } from '~/decrypt-worker/decipher/KugouMusic.ts'; import { XimalayaAndroidDecipher, XimalayaPCDecipher } from '~/decrypt-worker/decipher/Ximalaya.ts'; import { XiamiDecipher } from '~/decrypt-worker/decipher/XiamiMusic.ts'; +import { QignTingFMDecipher } from '~/decrypt-worker/decipher/QingTingFM.ts'; export enum Status { OK = 0, @@ -54,6 +55,9 @@ export const allCryptoFactories: DecipherFactory[] = [ // Xiami (*.xm) XiamiDecipher.make, + // QingTingFM Android (*.qta) + QignTingFMDecipher.make, + /// File with a fixed footer goes second // QMCv2 (*.mflac) @@ -75,9 +79,6 @@ export const allCryptoFactories: DecipherFactory[] = [ XimalayaAndroidDecipher.makeX2M, XimalayaAndroidDecipher.makeX3M, - // QingTingFM (Android) - // QingTingFM$Device.make, - // Transparent crypto (not encrypted) TransparentDecipher.make, ]; diff --git a/src/decrypt-worker/constants.ts b/src/decrypt-worker/constants.ts index 643d4ae..5a9bdce 100644 --- a/src/decrypt-worker/constants.ts +++ b/src/decrypt-worker/constants.ts @@ -1,6 +1,7 @@ export enum DECRYPTION_WORKER_ACTION_NAME { DECRYPT = 'DECRYPT', FIND_QMC_MUSICEX_NAME = 'FIND_QMC_MUSICEX_NAME', + QINGTING_FM_GET_DEVICE_KEY = 'QINGTING_FM_GET_DEVICE_KEY', VERSION = 'VERSION', } diff --git a/src/decrypt-worker/decipher/QingTingFM.ts b/src/decrypt-worker/decipher/QingTingFM.ts new file mode 100644 index 0000000..f128e65 --- /dev/null +++ b/src/decrypt-worker/decipher/QingTingFM.ts @@ -0,0 +1,37 @@ +import type { DecryptCommandOptions } from '~/decrypt-worker/types.ts'; +import { DecipherInstance, DecipherOK, DecipherResult, Status } from '~/decrypt-worker/Deciphers.ts'; +import { QingTingFM } from '@unlock-music/crypto'; +import { chunkBuffer } from '~/decrypt-worker/util/buffer.ts'; +import { unhex } from '~/util/hex.ts'; + +export class QignTingFMDecipher implements DecipherInstance { + cipherName = 'QingTingFM (Android, qta)'; + + async decrypt(buffer: Uint8Array, opts: DecryptCommandOptions): Promise { + const key = unhex(opts.qingTingAndroidKey || ''); + const iv = QingTingFM.getFileIV(opts.fileName); + + if (key.byteLength !== 16 || iv.byteLength !== 16) { + return { + status: Status.FAILED, + message: 'device key or iv invalid', + }; + } + + const qtfm = new QingTingFM(key, iv); + const audioBuffer = new Uint8Array(buffer); + for (const [block, i] of chunkBuffer(audioBuffer)) { + qtfm.decrypt(block, i); + } + + return { + cipherName: this.cipherName, + status: Status.OK, + data: audioBuffer, + }; + } + + public static make() { + return new QignTingFMDecipher(); + } +} diff --git a/src/decrypt-worker/types.ts b/src/decrypt-worker/types.ts index 9157ed0..f83826d 100644 --- a/src/decrypt-worker/types.ts +++ b/src/decrypt-worker/types.ts @@ -15,3 +15,12 @@ export interface FetchMusicExNamePayload { id: string; blobURI: string; } + +export interface GetQingTingFMDeviceKeyPayload { + product: string; + device: string; + manufacturer: string; + brand: string; + board: string; + model: string; +} diff --git a/src/decrypt-worker/worker.ts b/src/decrypt-worker/worker.ts index 1c5c712..600bb91 100644 --- a/src/decrypt-worker/worker.ts +++ b/src/decrypt-worker/worker.ts @@ -2,8 +2,9 @@ import { WorkerServerBus } from '~/util/WorkerEventBus'; import { DECRYPTION_WORKER_ACTION_NAME } from './constants'; import { getUmcVersion } from '@unlock-music/crypto'; -import { workerDecryptHandler } from './worker/handler/decrypt'; -import { workerParseMusicExMediaName } from './worker/handler/qmcv2_parser'; +import { workerDecryptHandler } from './worker/decrypt.ts'; +import { workerParseMusicExMediaName } from './worker/qmcv2_parser.ts'; +import { workerGetQtfmDeviceKey } from '~/decrypt-worker/worker/qtfm_device_key.ts'; const bus = new WorkerServerBus(); onmessage = bus.onmessage; @@ -11,3 +12,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, getUmcVersion); +bus.addEventHandler(DECRYPTION_WORKER_ACTION_NAME.QINGTING_FM_GET_DEVICE_KEY, workerGetQtfmDeviceKey); diff --git a/src/decrypt-worker/worker/handler/decrypt.ts b/src/decrypt-worker/worker/decrypt.ts similarity index 91% rename from src/decrypt-worker/worker/handler/decrypt.ts rename to src/decrypt-worker/worker/decrypt.ts index 18d7727..5f75690 100644 --- a/src/decrypt-worker/worker/handler/decrypt.ts +++ b/src/decrypt-worker/worker/decrypt.ts @@ -1,10 +1,10 @@ -import { timedLogger, withGroupedLogs as withTimeGroupedLogs } from '~/util/logUtils'; -import type { DecryptCommandOptions, DecryptCommandPayload } from '~/decrypt-worker/types'; -import { allCryptoFactories } from '../../Deciphers.ts'; -import { toBlob } from '~/decrypt-worker/util/buffer'; +import { timedLogger, withGroupedLogs as withTimeGroupedLogs } from '~/util/logUtils.ts'; +import type { DecryptCommandOptions, DecryptCommandPayload } from '~/decrypt-worker/types.ts'; +import { allCryptoFactories } from '../Deciphers.ts'; +import { toBlob } from '~/decrypt-worker/util/buffer.ts'; import { DecipherFactory, DecipherInstance, Status } from '~/decrypt-worker/Deciphers.ts'; -import { UnsupportedSourceFile } from '~/decrypt-worker/util/DecryptError'; -import { ready as umCryptoReady } from '@unlock-music/crypto'; +import { UnsupportedSourceFile } from '~/decrypt-worker/util/DecryptError.ts'; +import { ready as umCryptoReady } from '../../../../lib_um_crypto_rust/um_wasm_loader'; import { go } from '~/util/go.ts'; import { detectAudioExtension } from '~/decrypt-worker/util/audioType.ts'; diff --git a/src/decrypt-worker/worker/handler/qmcv2_parser.ts b/src/decrypt-worker/worker/qmcv2_parser.ts similarity index 98% rename from src/decrypt-worker/worker/handler/qmcv2_parser.ts rename to src/decrypt-worker/worker/qmcv2_parser.ts index aea0098..0b9d8d2 100644 --- a/src/decrypt-worker/worker/handler/qmcv2_parser.ts +++ b/src/decrypt-worker/worker/qmcv2_parser.ts @@ -1,4 +1,4 @@ -import type { FetchMusicExNamePayload } from '~/decrypt-worker/types'; +import type { FetchMusicExNamePayload } from '~/decrypt-worker/types.ts'; import { timedLogger } from '~/util/logUtils.ts'; import { QMCFooter } from '@unlock-music/crypto'; diff --git a/src/decrypt-worker/worker/qtfm_device_key.ts b/src/decrypt-worker/worker/qtfm_device_key.ts new file mode 100644 index 0000000..03a7730 --- /dev/null +++ b/src/decrypt-worker/worker/qtfm_device_key.ts @@ -0,0 +1,15 @@ +import { GetQingTingFMDeviceKeyPayload } from '~/decrypt-worker/types.ts'; +import { QingTingFM } from '@unlock-music/crypto'; +import { hex } from '~/util/hex.ts'; + +export async function workerGetQtfmDeviceKey({ + device, + brand, + model, + product, + manufacturer, + board, +}: GetQingTingFMDeviceKeyPayload) { + const buffer = QingTingFM.getDeviceKey(device, brand, model, product, manufacturer, board); + return hex(buffer); +} diff --git a/src/features/settings/panels/PanelQingTing.tsx b/src/features/settings/panels/PanelQingTing.tsx index 3089526..80ca0d8 100644 --- a/src/features/settings/panels/PanelQingTing.tsx +++ b/src/features/settings/panels/PanelQingTing.tsx @@ -18,6 +18,9 @@ import { ChangeEvent, ClipboardEvent } from 'react'; import { VQuote } from '~/components/HelpText/VQuote'; import { selectStagingQtfmAndroidKey } from '../settingsSelector'; import { qtfmAndroidUpdateKey } from '../settingsSlice'; +import { workerClientBus } from '~/decrypt-worker/client.ts'; +import { GetQingTingFMDeviceKeyPayload } from '~/decrypt-worker/types.ts'; +import { DECRYPTION_WORKER_ACTION_NAME } from '~/decrypt-worker/constants.ts'; const QTFM_DEVICE_ID_URL = 'https://github.com/parakeet-rs/qtfm-device-id/releases/latest'; @@ -37,29 +40,23 @@ export function PanelQingTing() { return; } - const dataMap = new Map(); - for (const [_unused, key, value] of plainText.matchAll( - /^(PRODUCT|DEVICE|MANUFACTURER|BRAND|BOARD|MODEL): (.+)/gim, - )) { - dataMap.set(key.toLowerCase(), value); + const dataMap = Object.create(null); + for (const [, key, value] of plainText.matchAll(/^(PRODUCT|DEVICE|MANUFACTURER|BRAND|BOARD|MODEL): (.+)/gim)) { + dataMap[key.toLowerCase()] = value; } + const { product, device, manufacturer, brand, board, model } = dataMap; - const product = dataMap.get('product') ?? null; - const device = dataMap.get('device') ?? null; - const manufacturer = dataMap.get('manufacturer') ?? null; - const brand = dataMap.get('brand') ?? null; - const board = dataMap.get('board') ?? null; - const model = dataMap.get('model') ?? null; - if ( - product !== null && - device !== null && - manufacturer !== null && - brand !== null && - board !== null && - model !== null - ) { + if (product && device && manufacturer && brand && board && model) { e.preventDefault(); - alert('TODO!'); + workerClientBus + .request( + DECRYPTION_WORKER_ACTION_NAME.QINGTING_FM_GET_DEVICE_KEY, + dataMap, + ) + .then(setSecretKey) + .catch((err) => { + alert(`生成设备密钥时发生错误: ${err}`); + }); } }; diff --git a/src/util/MMKVParser.ts b/src/util/MMKVParser.ts index bd9b2b6..9525df7 100644 --- a/src/util/MMKVParser.ts +++ b/src/util/MMKVParser.ts @@ -1,5 +1,5 @@ import { bytesToUTF8String } from '~/decrypt-worker/util/utf8Encoder'; -import { formatHex } from './formatHex'; +import { formatHex } from './hex.ts'; export class MMKVParser { private offset = 4; diff --git a/src/util/formatHex.ts b/src/util/formatHex.ts deleted file mode 100644 index 0d778c1..0000000 --- a/src/util/formatHex.ts +++ /dev/null @@ -1,3 +0,0 @@ -export function formatHex(value: number, len = 8) { - return '0x' + (value | 0).toString(16).padStart(len, '0'); -} diff --git a/src/util/hex.ts b/src/util/hex.ts new file mode 100644 index 0000000..897fdc0 --- /dev/null +++ b/src/util/hex.ts @@ -0,0 +1,15 @@ +export function formatHex(value: number, len = 8) { + return '0x' + (value | 0).toString(16).padStart(len, '0'); +} + +export function hex(value: Uint8Array): string { + return Array.from(value, (byte) => byte.toString(16).padStart(2, '0')).join(''); +} + +export function unhex(value: string): Uint8Array { + const bytes = []; + for (const [byte] of value.matchAll(/[0-9a-fA-F]{2}/g)) { + bytes.push(parseInt(byte, 16)); + } + return new Uint8Array(bytes); +} -- 2.45.2 From 4396f8876266d0f78b781f1863d8187776d22687 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=B2=81=E6=A0=91=E4=BA=BA?= Date: Fri, 20 Sep 2024 23:23:19 +0100 Subject: [PATCH 12/18] feat: add Migu3D Keyless decipher --- package.json | 2 +- pnpm-lock.yaml | 9 +++++++-- src/decrypt-worker/Deciphers.ts | 3 ++- src/decrypt-worker/decipher/Migu3d.ts | 27 +++++++++++++++++++++++++++ 4 files changed, 37 insertions(+), 4 deletions(-) create mode 100644 src/decrypt-worker/decipher/Migu3d.ts diff --git a/package.json b/package.json index f51f2b8..2a6ecb5 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", "@reduxjs/toolkit": "^2.0.1", - "@unlock-music/crypto": "0.0.0-alpha.15", + "@unlock-music/crypto": "0.0.0-alpha.16", "framer-motion": "^10.16.16", "nanoid": "^5.0.4", "radash": "^11.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8eb7f5a..21007bd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -39,8 +39,8 @@ importers: 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) '@unlock-music/crypto': - specifier: 0.0.0-alpha.15 - version: link:../lib_um_crypto_rust/um_wasm_loader + specifier: 0.0.0-alpha.16 + version: 0.0.0-alpha.16 framer-motion: specifier: ^10.16.16 version: 10.16.16(react-dom@18.2.0(react@18.2.0))(react@18.2.0) @@ -1921,6 +1921,9 @@ packages: '@ungap/structured-clone@1.2.0': resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} + '@unlock-music/crypto@0.0.0-alpha.16': + resolution: {integrity: sha512-QDhDWTsZOOkSQ64d3+pBwPCfesit6MiO63SzV7TQB8rQ/lPb78MMz6C4ECIjP9ijCBXds4PT4CBXp8vJDifAEg==, tarball: https://git.unlock-music.dev/api/packages/um/npm/%40unlock-music%2Fcrypto/-/0.0.0-alpha.16/crypto-0.0.0-alpha.16.tgz} + '@vitejs/plugin-react@4.2.1': resolution: {integrity: sha512-oojO9IDc4nCUUi8qIR11KoQm0XFFLIwsRBwHRR4d/88IWghn1y6ckz/bJ8GHDCsYEJee8mDzqtJxh15/cisJNQ==} engines: {node: ^14.18.0 || >=16.0.0} @@ -6107,6 +6110,8 @@ snapshots: '@ungap/structured-clone@1.2.0': {} + '@unlock-music/crypto@0.0.0-alpha.16': {} + '@vitejs/plugin-react@4.2.1(vite@5.0.10(@types/node@20.10.5)(sass@1.69.5)(terser@5.27.0))': dependencies: '@babel/core': 7.23.6 diff --git a/src/decrypt-worker/Deciphers.ts b/src/decrypt-worker/Deciphers.ts index d11da39..423b9b0 100644 --- a/src/decrypt-worker/Deciphers.ts +++ b/src/decrypt-worker/Deciphers.ts @@ -7,6 +7,7 @@ import { KugouMusicDecipher } from '~/decrypt-worker/decipher/KugouMusic.ts'; import { XimalayaAndroidDecipher, XimalayaPCDecipher } from '~/decrypt-worker/decipher/Ximalaya.ts'; import { XiamiDecipher } from '~/decrypt-worker/decipher/XiamiMusic.ts'; import { QignTingFMDecipher } from '~/decrypt-worker/decipher/QingTingFM.ts'; +import { Migu3DKeylessDecipher } from '~/decrypt-worker/decipher/Migu3d.ts'; export enum Status { OK = 0, @@ -67,7 +68,7 @@ export const allCryptoFactories: DecipherFactory[] = [ /// File without an obvious header or footer goes last. // Migu3D/Keyless (*.wav; *.m4a) - // MiguCrypto.make, + Migu3DKeylessDecipher.make, // Crypto that does not implement "checkBySignature" or need to decrypt the entire file and then check audio type, // should be moved to the bottom of the list for performance reasons. diff --git a/src/decrypt-worker/decipher/Migu3d.ts b/src/decrypt-worker/decipher/Migu3d.ts new file mode 100644 index 0000000..4a793f0 --- /dev/null +++ b/src/decrypt-worker/decipher/Migu3d.ts @@ -0,0 +1,27 @@ +import { DecipherInstance, DecipherOK, DecipherResult, Status } from '~/decrypt-worker/Deciphers.ts'; +import { chunkBuffer } from '~/decrypt-worker/util/buffer.ts'; +import { Migu3D } from '@unlock-music/crypto'; + +export class Migu3DKeylessDecipher implements DecipherInstance { + cipherName = 'Migu3D (Keyless)'; + + async decrypt(buffer: Uint8Array): Promise { + const mg3d = Migu3D.fromHeader(buffer.subarray(0, 0x100)); + const audioBuffer = new Uint8Array(buffer); + + for (const [block, i] of chunkBuffer(audioBuffer)) { + mg3d.decrypt(block, i); + } + mg3d.free(); + + return { + cipherName: this.cipherName, + status: Status.OK, + data: audioBuffer, + }; + } + + public static make() { + return new Migu3DKeylessDecipher(); + } +} -- 2.45.2 From d071f0bd65e3ac04c0b7af426145474bd406e28a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=B2=81=E6=A0=91=E4=BA=BA?= Date: Fri, 20 Sep 2024 23:44:27 +0100 Subject: [PATCH 13/18] refactor: remove js parser for kwm header --- package.json | 2 +- pnpm-lock.yaml | 10 ++--- src/crypto/parseKuwo.ts | 27 ------------- src/crypto/strlen.ts | 9 ----- src/decrypt-worker/constants.ts | 1 + src/decrypt-worker/types.ts | 10 ++++- src/decrypt-worker/worker.ts | 2 + .../worker/kuwo_header_parse.ts | 17 +++++++++ src/decrypt-worker/worker/qmcv2_parser.ts | 24 +++++------- src/features/file-listing/fileListingSlice.ts | 38 +++++++++---------- src/features/settings/settingsSelector.ts | 10 +++-- 11 files changed, 69 insertions(+), 81 deletions(-) delete mode 100644 src/crypto/parseKuwo.ts delete mode 100644 src/crypto/strlen.ts create mode 100644 src/decrypt-worker/worker/kuwo_header_parse.ts diff --git a/package.json b/package.json index 2a6ecb5..5c92824 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", "@reduxjs/toolkit": "^2.0.1", - "@unlock-music/crypto": "0.0.0-alpha.16", + "@unlock-music/crypto": "0.0.0-alpha.18", "framer-motion": "^10.16.16", "nanoid": "^5.0.4", "radash": "^11.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 21007bd..5c77092 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -39,8 +39,8 @@ importers: 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) '@unlock-music/crypto': - specifier: 0.0.0-alpha.16 - version: 0.0.0-alpha.16 + specifier: 0.0.0-alpha.18 + version: 0.0.0-alpha.18 framer-motion: specifier: ^10.16.16 version: 10.16.16(react-dom@18.2.0(react@18.2.0))(react@18.2.0) @@ -1921,8 +1921,8 @@ packages: '@ungap/structured-clone@1.2.0': resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} - '@unlock-music/crypto@0.0.0-alpha.16': - resolution: {integrity: sha512-QDhDWTsZOOkSQ64d3+pBwPCfesit6MiO63SzV7TQB8rQ/lPb78MMz6C4ECIjP9ijCBXds4PT4CBXp8vJDifAEg==, tarball: https://git.unlock-music.dev/api/packages/um/npm/%40unlock-music%2Fcrypto/-/0.0.0-alpha.16/crypto-0.0.0-alpha.16.tgz} + '@unlock-music/crypto@0.0.0-alpha.18': + resolution: {integrity: sha512-/2VgU15WCW+GnWOgYd18GylQTkCGgAW7cf/qBcjXWU+2qtsvv9heiN8LkIWWz+NMbO0XDRONXijAKdKYf38o/g==, tarball: https://git.unlock-music.dev/api/packages/um/npm/%40unlock-music%2Fcrypto/-/0.0.0-alpha.18/crypto-0.0.0-alpha.18.tgz} '@vitejs/plugin-react@4.2.1': resolution: {integrity: sha512-oojO9IDc4nCUUi8qIR11KoQm0XFFLIwsRBwHRR4d/88IWghn1y6ckz/bJ8GHDCsYEJee8mDzqtJxh15/cisJNQ==} @@ -6110,7 +6110,7 @@ snapshots: '@ungap/structured-clone@1.2.0': {} - '@unlock-music/crypto@0.0.0-alpha.16': {} + '@unlock-music/crypto@0.0.0-alpha.18': {} '@vitejs/plugin-react@4.2.1(vite@5.0.10(@types/node@20.10.5)(sass@1.69.5)(terser@5.27.0))': dependencies: diff --git a/src/crypto/parseKuwo.ts b/src/crypto/parseKuwo.ts deleted file mode 100644 index 5de9b1d..0000000 --- a/src/crypto/parseKuwo.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { bytesToUTF8String } from '~/decrypt-worker/util/utf8Encoder'; -import { strlen } from './strlen'; - -export interface KuwoHeader { - rid: string; // uint64 - encVersion: 1 | 2; // uint32 - quality: string; -} - -const KUWO_MAGIC_HDRS = new Set(['yeelion-kuwo\x00\x00\x00\x00', 'yeelion-kuwo-tme']); - -export function parseKuwoHeader(view: DataView): KuwoHeader | null { - const magic = view.buffer.slice(view.byteOffset, view.byteOffset + 0x10); - if (!KUWO_MAGIC_HDRS.has(bytesToUTF8String(magic))) { - return null; // not kuwo-encrypted file - } - - const qualityBytes = new Uint8Array(view.buffer.slice(view.byteOffset + 0x30, view.byteOffset + 0x40)); - const qualityLen = strlen(qualityBytes); - const quality = bytesToUTF8String(qualityBytes.slice(0, qualityLen)); - - return { - encVersion: view.getUint32(0x10, true) as 1 | 2, - rid: view.getUint32(0x18, true).toString(), - quality, - }; -} diff --git a/src/crypto/strlen.ts b/src/crypto/strlen.ts deleted file mode 100644 index b4d67df..0000000 --- a/src/crypto/strlen.ts +++ /dev/null @@ -1,9 +0,0 @@ -export function strlen(data: Uint8Array): number { - const n = data.byteLength; - for (let i = 0; i < n; i++) { - if (data[i] === 0) { - return i; - } - } - return n; -} diff --git a/src/decrypt-worker/constants.ts b/src/decrypt-worker/constants.ts index 5a9bdce..82cd52c 100644 --- a/src/decrypt-worker/constants.ts +++ b/src/decrypt-worker/constants.ts @@ -1,6 +1,7 @@ export enum DECRYPTION_WORKER_ACTION_NAME { DECRYPT = 'DECRYPT', FIND_QMC_MUSICEX_NAME = 'FIND_QMC_MUSICEX_NAME', + KUWO_PARSE_HEADER = 'KUWO_PARSE_HEADER', QINGTING_FM_GET_DEVICE_KEY = 'QINGTING_FM_GET_DEVICE_KEY', VERSION = 'VERSION', } diff --git a/src/decrypt-worker/types.ts b/src/decrypt-worker/types.ts index f83826d..a36f66c 100644 --- a/src/decrypt-worker/types.ts +++ b/src/decrypt-worker/types.ts @@ -12,10 +12,18 @@ export interface DecryptCommandPayload { } export interface FetchMusicExNamePayload { - id: string; blobURI: string; } +export interface ParseKuwoHeaderPayload { + blobURI: string; +} + +export type ParseKuwoHeaderResponse = null | { + resourceId: number; + qualityId: number; +}; + export interface GetQingTingFMDeviceKeyPayload { product: string; device: string; diff --git a/src/decrypt-worker/worker.ts b/src/decrypt-worker/worker.ts index 600bb91..c5862d7 100644 --- a/src/decrypt-worker/worker.ts +++ b/src/decrypt-worker/worker.ts @@ -5,6 +5,7 @@ import { getUmcVersion } from '@unlock-music/crypto'; import { workerDecryptHandler } from './worker/decrypt.ts'; import { workerParseMusicExMediaName } from './worker/qmcv2_parser.ts'; import { workerGetQtfmDeviceKey } from '~/decrypt-worker/worker/qtfm_device_key.ts'; +import { workerParseKuwoHeader } from '~/decrypt-worker/worker/kuwo_header_parse.ts'; const bus = new WorkerServerBus(); onmessage = bus.onmessage; @@ -12,4 +13,5 @@ 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, getUmcVersion); +bus.addEventHandler(DECRYPTION_WORKER_ACTION_NAME.KUWO_PARSE_HEADER, workerParseKuwoHeader); bus.addEventHandler(DECRYPTION_WORKER_ACTION_NAME.QINGTING_FM_GET_DEVICE_KEY, workerGetQtfmDeviceKey); diff --git a/src/decrypt-worker/worker/kuwo_header_parse.ts b/src/decrypt-worker/worker/kuwo_header_parse.ts new file mode 100644 index 0000000..dc00439 --- /dev/null +++ b/src/decrypt-worker/worker/kuwo_header_parse.ts @@ -0,0 +1,17 @@ +import { FetchMusicExNamePayload, ParseKuwoHeaderResponse } from '~/decrypt-worker/types.ts'; +import { KuwoHeader } from '@unlock-music/crypto'; + +export const workerParseKuwoHeader = async ({ blobURI }: FetchMusicExNamePayload): Promise => { + const blob = await fetch(blobURI, { headers: { Range: 'bytes=0-1023' } }).then((r) => r.blob()); + const arrayBuffer = await blob.arrayBuffer(); + + try { + const buffer = new Uint8Array(arrayBuffer.slice(0, 1024)); + const kwm = KuwoHeader.parse(buffer); + const { qualityId, resourceId } = kwm; + kwm.free(); + return { qualityId, resourceId }; + } catch { + return null; + } +}; diff --git a/src/decrypt-worker/worker/qmcv2_parser.ts b/src/decrypt-worker/worker/qmcv2_parser.ts index 0b9d8d2..032b928 100644 --- a/src/decrypt-worker/worker/qmcv2_parser.ts +++ b/src/decrypt-worker/worker/qmcv2_parser.ts @@ -1,19 +1,15 @@ import type { FetchMusicExNamePayload } from '~/decrypt-worker/types.ts'; -import { timedLogger } from '~/util/logUtils.ts'; import { QMCFooter } from '@unlock-music/crypto'; -export const workerParseMusicExMediaName = async ({ id, blobURI }: FetchMusicExNamePayload) => { - const label = `qmcMusixExDetectName(${id.replace('://', ':')})`; - return timedLogger(label, async () => { - const blob = await fetch(blobURI, { headers: { Range: 'bytes=-1024' } }).then((r) => r.blob()); - const arrayBuffer = await blob.arrayBuffer(); +export const workerParseMusicExMediaName = async ({ blobURI }: FetchMusicExNamePayload) => { + const blob = await fetch(blobURI, { headers: { Range: 'bytes=-1024' } }).then((r) => r.blob()); + 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; - } - }); + try { + const buffer = new Uint8Array(arrayBuffer.slice(-1024)); + const footer = QMCFooter.parse(buffer); + return footer?.mediaName || null; + } catch { + return null; + } }; diff --git a/src/features/file-listing/fileListingSlice.ts b/src/features/file-listing/fileListingSlice.ts index 81e5180..54cb9f4 100644 --- a/src/features/file-listing/fileListingSlice.ts +++ b/src/features/file-listing/fileListingSlice.ts @@ -1,12 +1,17 @@ -import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'; import type { PayloadAction } from '@reduxjs/toolkit'; +import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'; import type { RootState } from '~/store'; import { DECRYPTION_WORKER_ACTION_NAME, type DecryptionResult } from '~/decrypt-worker/constants'; -import type { DecryptCommandOptions, FetchMusicExNamePayload } from '~/decrypt-worker/types'; +import type { + DecryptCommandOptions, + FetchMusicExNamePayload, + ParseKuwoHeaderPayload, + ParseKuwoHeaderResponse, +} from '~/decrypt-worker/types'; import { decryptionQueue, workerClientBus } from '~/decrypt-worker/client'; import { DecryptErrorType } from '~/decrypt-worker/util/DecryptError'; -import { selectQMCv2KeyByFileName, selectKWMv2Key, selectQtfmAndroidKey } from '../settings/settingsSelector'; +import { selectKWMv2Key, selectQMCv2KeyByFileName, selectQtfmAndroidKey } from '../settings/settingsSelector'; export enum ProcessState { QUEUED = 'QUEUED', @@ -43,6 +48,7 @@ export interface FileListingState { files: Record; displayMode: ListingMode; } + const initialState: FileListingState = { files: {}, displayMode: ListingMode.LIST, @@ -64,28 +70,20 @@ export const processFile = createAsyncThunk< thunkAPI.dispatch(setFileAsProcessing({ id: fileId })); }; - const fileHeader = await fetch(file.raw, { headers: { Range: 'bytes=0-1023' } }) - .then((r) => r.blob()) - .then((r) => r.arrayBuffer()) - .then((r) => { - if (r.byteLength > 1024) { - return r.slice(0, 1024); - } - return r; - }); - - const qmcv2MusicExMediaFile = await workerClientBus.request( - DECRYPTION_WORKER_ACTION_NAME.FIND_QMC_MUSICEX_NAME, - { - id: fileId, + const [qmcv2MusicExMediaFile, kuwoHdr] = await Promise.all([ + workerClientBus.request(DECRYPTION_WORKER_ACTION_NAME.FIND_QMC_MUSICEX_NAME, { blobURI: file.raw, - }, - ); + }), + workerClientBus.request( + DECRYPTION_WORKER_ACTION_NAME.KUWO_PARSE_HEADER, + { blobURI: file.raw }, + ), + ]); const options: DecryptCommandOptions = { fileName: file.fileName, qmc2Key: selectQMCv2KeyByFileName(state, qmcv2MusicExMediaFile || file.fileName), - kwm2key: selectKWMv2Key(state, new DataView(fileHeader)), + kwm2key: selectKWMv2Key(state, kuwoHdr), qingTingAndroidKey: selectQtfmAndroidKey(state), }; return decryptionQueue.add({ id: fileId, blobURI: file.raw, options }, onPreProcess); diff --git a/src/features/settings/settingsSelector.ts b/src/features/settings/settingsSelector.ts index e61ecf8..4d37903 100644 --- a/src/features/settings/settingsSelector.ts +++ b/src/features/settings/settingsSelector.ts @@ -1,8 +1,8 @@ -import { parseKuwoHeader } from '~/crypto/parseKuwo'; import type { RootState } from '~/store'; import { closestByLevenshtein } from '~/util/levenshtein'; import { hasOwn } from '~/util/objects'; import { kwm2StagingToProductionKey } from './keyFormats'; +import type { ParseKuwoHeaderResponse } from '~/decrypt-worker/types.ts'; export const selectIsSettingsNotSaved = (state: RootState) => state.settings.dirty; @@ -31,14 +31,16 @@ export const selectQMCv2KeyByFileName = (state: RootState, name: string): string return ekey; }; -export const selectKWMv2Key = (state: RootState, headerView: DataView): string | undefined => { - const hdr = parseKuwoHeader(headerView); +export const selectKWMv2Key = (state: RootState, hdr: ParseKuwoHeaderResponse): string | undefined => { if (!hdr) { return; } + const quality = String(hdr.qualityId); + const rid = String(hdr.resourceId); + const keys = selectFinalKWMv2Keys(state); - const lookupKey = kwm2StagingToProductionKey({ id: '', ekey: '', quality: hdr.quality, rid: hdr.rid }); + const lookupKey = kwm2StagingToProductionKey({ id: '', ekey: '', quality, rid }); let ekey: string | undefined; if (hasOwn(keys, lookupKey)) { -- 2.45.2 From 91a84722d0c3781382343d0039f2f12cb478dd9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=B2=81=E6=A0=91=E4=BA=BA?= Date: Fri, 20 Sep 2024 23:47:41 +0100 Subject: [PATCH 14/18] fix: CI build issue with external um_crypto path --- src/decrypt-worker/worker/decrypt.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/decrypt-worker/worker/decrypt.ts b/src/decrypt-worker/worker/decrypt.ts index 5f75690..7c146c9 100644 --- a/src/decrypt-worker/worker/decrypt.ts +++ b/src/decrypt-worker/worker/decrypt.ts @@ -4,7 +4,7 @@ import { allCryptoFactories } from '../Deciphers.ts'; import { toBlob } from '~/decrypt-worker/util/buffer.ts'; import { DecipherFactory, DecipherInstance, Status } from '~/decrypt-worker/Deciphers.ts'; import { UnsupportedSourceFile } from '~/decrypt-worker/util/DecryptError.ts'; -import { ready as umCryptoReady } from '../../../../lib_um_crypto_rust/um_wasm_loader'; +import { ready as umCryptoReady } from '@unlock-music/crypto'; import { go } from '~/util/go.ts'; import { detectAudioExtension } from '~/decrypt-worker/util/audioType.ts'; -- 2.45.2 From 0973d14e632b328a07a4bbe8a32d03dc5bcd312d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=B2=81=E6=A0=91=E4=BA=BA?= Date: Tue, 24 Sep 2024 22:36:46 +0100 Subject: [PATCH 15/18] chore: bump to v0.1.0 --- package.json | 2 +- pnpm-lock.yaml | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 5c92824..6edbd9e 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", "@reduxjs/toolkit": "^2.0.1", - "@unlock-music/crypto": "0.0.0-alpha.18", + "@unlock-music/crypto": "0.1.0", "framer-motion": "^10.16.16", "nanoid": "^5.0.4", "radash": "^11.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5c77092..b94f45a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -39,8 +39,8 @@ importers: 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) '@unlock-music/crypto': - specifier: 0.0.0-alpha.18 - version: 0.0.0-alpha.18 + specifier: 0.1.0 + version: 0.1.0 framer-motion: specifier: ^10.16.16 version: 10.16.16(react-dom@18.2.0(react@18.2.0))(react@18.2.0) @@ -1921,8 +1921,8 @@ packages: '@ungap/structured-clone@1.2.0': resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} - '@unlock-music/crypto@0.0.0-alpha.18': - resolution: {integrity: sha512-/2VgU15WCW+GnWOgYd18GylQTkCGgAW7cf/qBcjXWU+2qtsvv9heiN8LkIWWz+NMbO0XDRONXijAKdKYf38o/g==, tarball: https://git.unlock-music.dev/api/packages/um/npm/%40unlock-music%2Fcrypto/-/0.0.0-alpha.18/crypto-0.0.0-alpha.18.tgz} + '@unlock-music/crypto@0.1.0': + resolution: {integrity: sha512-Qu4wgHS30nrXFRBJGkHWuKmI2nGdP6Nht+fonKCeB4EdSD7Aymu0bJmr8lymqrQd7mKuBwnMRZWUCnIOPpryNA==, tarball: https://git.unlock-music.dev/api/packages/um/npm/%40unlock-music%2Fcrypto/-/0.1.0/crypto-0.1.0.tgz} '@vitejs/plugin-react@4.2.1': resolution: {integrity: sha512-oojO9IDc4nCUUi8qIR11KoQm0XFFLIwsRBwHRR4d/88IWghn1y6ckz/bJ8GHDCsYEJee8mDzqtJxh15/cisJNQ==} @@ -6110,7 +6110,7 @@ snapshots: '@ungap/structured-clone@1.2.0': {} - '@unlock-music/crypto@0.0.0-alpha.18': {} + '@unlock-music/crypto@0.1.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: -- 2.45.2 From 6ed2f51d40618b5f4c0b4b5e5475d34eca948d52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=B2=81=E6=A0=91=E4=BA=BA?= Date: Tue, 24 Sep 2024 22:58:21 +0100 Subject: [PATCH 16/18] refactor: package upgrade --- .run/test.run.xml | 12 + .run/vite dev.run.xml | 12 + package.json | 77 +- ...4.3.patch => @rollup__plugin-terser.patch} | 0 patches/{sql.js@1.9.0.patch => sql.js.patch} | 4 +- pnpm-lock.yaml | 6532 +++++++++-------- 6 files changed, 3374 insertions(+), 3263 deletions(-) create mode 100644 .run/test.run.xml create mode 100644 .run/vite dev.run.xml rename patches/{@rollup__plugin-terser@0.4.3.patch => @rollup__plugin-terser.patch} (100%) rename patches/{sql.js@1.9.0.patch => sql.js.patch} (58%) diff --git a/.run/test.run.xml b/.run/test.run.xml new file mode 100644 index 0000000..e3392be --- /dev/null +++ b/.run/test.run.xml @@ -0,0 +1,12 @@ + + + + + +