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] 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]; + } +}