diff --git a/src/decrypt-worker/crypto/CryptoBase.ts b/src/decrypt-worker/crypto/CryptoBase.ts index 4fcc0d9..6bf3ae5 100644 --- a/src/decrypt-worker/crypto/CryptoBase.ts +++ b/src/decrypt-worker/crypto/CryptoBase.ts @@ -1,7 +1,12 @@ export interface CryptoBase { cryptoName: string; checkByDecryptHeader: boolean; - decryptTargetAudio: 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) => Promise; decrypt(buffer: ArrayBuffer, blob: Blob): Promise; diff --git a/src/decrypt-worker/crypto/CryptoFactory.ts b/src/decrypt-worker/crypto/CryptoFactory.ts index 3f5daba..00ec3c9 100644 --- a/src/decrypt-worker/crypto/CryptoFactory.ts +++ b/src/decrypt-worker/crypto/CryptoFactory.ts @@ -6,14 +6,14 @@ import { XiamiCrypto } from './xiami/xiami'; export const allCryptoFactories: CryptoFactory[] = [ // Xiami (*.xm) - () => new XiamiCrypto(), + XiamiCrypto.make, // QMCv2 (*.mflac) - () => new QMC2Crypto(), + QMC2Crypto.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) - () => new QMC1Crypto(), + QMC1Crypto.make, ]; diff --git a/src/decrypt-worker/crypto/qmc/qmc_v1.ts b/src/decrypt-worker/crypto/qmc/qmc_v1.ts index d7a0acd..610c55a 100644 --- a/src/decrypt-worker/crypto/qmc/qmc_v1.ts +++ b/src/decrypt-worker/crypto/qmc/qmc_v1.ts @@ -3,11 +3,14 @@ import type { CryptoBase } from '../CryptoBase'; import key from './qmc_v1.key.ts'; export class QMC1Crypto implements CryptoBase { - cryptoName = 'QMCv1'; + cryptoName = 'QMC/v1'; checkByDecryptHeader = true; - decryptTargetAudio = true; - async decrypt(_buffer: ArrayBuffer, blob: Blob): Promise { - return transformBlob(blob, (p) => p.make.QMCv1(key)); + 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.ts b/src/decrypt-worker/crypto/qmc/qmc_v2.ts index 4c74240..371eb22 100644 --- a/src/decrypt-worker/crypto/qmc/qmc_v2.ts +++ b/src/decrypt-worker/crypto/qmc/qmc_v2.ts @@ -3,11 +3,14 @@ import type { CryptoBase } from '../CryptoBase'; import { SEED, ENC_V2_KEY_1, ENC_V2_KEY_2 } from './qmc_v2.key.ts'; export class QMC2Crypto implements CryptoBase { - cryptoName = 'QMCv2'; + cryptoName = 'QMC/v2'; checkByDecryptHeader = false; - decryptTargetAudio = true; - async decrypt(_buffer: ArrayBuffer, blob: Blob): Promise { - return transformBlob(blob, (p) => p.make.QMCv2(p.make.QMCv2FooterParser(SEED, ENC_V2_KEY_1, ENC_V2_KEY_2))); + async decrypt(buffer: ArrayBuffer): Promise { + return transformBlob(buffer, (p) => p.make.QMCv2(p.make.QMCv2FooterParser(SEED, ENC_V2_KEY_1, ENC_V2_KEY_2))); + } + + public static make() { + return new QMC2Crypto(); } } diff --git a/src/decrypt-worker/crypto/xiami/xiami.ts b/src/decrypt-worker/crypto/xiami/xiami.ts index 98afd6d..8be5906 100644 --- a/src/decrypt-worker/crypto/xiami/xiami.ts +++ b/src/decrypt-worker/crypto/xiami/xiami.ts @@ -26,7 +26,6 @@ const u8Sub = (a: number, b: number) => { export class XiamiCrypto implements CryptoBase { cryptoName = 'Xiami'; checkByDecryptHeader = false; - decryptTargetAudio = true; async checkBySignature(buffer: ArrayBuffer): Promise { const header = new DataView(buffer); @@ -45,4 +44,8 @@ export class XiamiCrypto implements CryptoBase { } return decrypted; } + + public static make() { + return new XiamiCrypto(); + } } diff --git a/src/decrypt-worker/util/transformBlob.ts b/src/decrypt-worker/util/transformBlob.ts index 7c18412..e29110e 100644 --- a/src/decrypt-worker/util/transformBlob.ts +++ b/src/decrypt-worker/util/transformBlob.ts @@ -1,7 +1,8 @@ import { Transformer, Parakeet, TransformResult, fetchParakeet } from '@jixun/libparakeet'; +import { toArrayBuffer } from './buffer'; export async function transformBlob( - blob: Blob, + blob: Blob | ArrayBuffer, transformerFactory: (p: Parakeet) => Transformer | Promise, parakeet?: Parakeet ) { @@ -12,7 +13,7 @@ export async function transformBlob( const transformer = await transformerFactory(mod); cleanup.push(() => transformer.delete()); - const reader = mod.make.Reader(await blob.arrayBuffer()); + const reader = mod.make.Reader(await toArrayBuffer(blob)); cleanup.push(() => reader.delete()); const sink = mod.make.WriterSink(); @@ -21,7 +22,7 @@ export async function transformBlob( const result = transformer.Transform(writer, reader); if (result !== TransformResult.OK) { - throw new Error(`transform failed with error: ${TransformResult[result]} (${result})`); + throw new Error(`transformer<${transformer.Name}> failed with error: ${TransformResult[result]} (${result})`); } return sink.collectBlob(); diff --git a/src/decrypt-worker/worker/handler/decrypt.ts b/src/decrypt-worker/worker/handler/decrypt.ts index 1aa7422..4d3ff55 100644 --- a/src/decrypt-worker/worker/handler/decrypt.ts +++ b/src/decrypt-worker/worker/handler/decrypt.ts @@ -46,17 +46,24 @@ class DecryptCommandHandler { return null; } - const decrypted = await this.log('decrypt', async () => crypto.decrypt(this.buffer, this.blob)); + const decrypted = await this.log(`decrypt (${crypto.cryptoName})`, () => crypto.decrypt(this.buffer, this.blob)); // Check if we had a successful decryption - const audioExt = await this.log(`detect-audio-ext`, async () => { - const header = await toArrayBuffer(decrypted.slice(0, TEST_FILE_HEADER_LEN)); - return this.parakeet.detectAudioExtension(header); - }); + const audioExt = crypto.overrideExtension ?? (await this.detectAudioExtension(decrypted)); + if (crypto.checkByDecryptHeader && audioExt === 'bin') { + return null; + } 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) {