diff --git a/package.json b/package.json index 85a125c..a0aa611 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "@chakra-ui/react": "^2.6.1", "@emotion/react": "^11.11.0", "@emotion/styled": "^11.11.0", - "@jixun/libparakeet": "0.0.0-exp.14", + "@jixun/libparakeet": "0.0.0-exp.15", "@reduxjs/toolkit": "^1.9.5", "framer-motion": "^10.12.8", "nanoid": "^4.0.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7e75190..92c4cf2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,8 +14,8 @@ dependencies: specifier: ^11.11.0 version: 11.11.0(@emotion/react@11.11.0)(@types/react@18.0.28)(react@18.2.0) '@jixun/libparakeet': - specifier: 0.0.0-exp.14 - version: 0.0.0-exp.14 + specifier: 0.0.0-exp.15 + version: 0.0.0-exp.15 '@reduxjs/toolkit': specifier: ^1.9.5 version: 1.9.5(react-redux@8.0.5)(react@18.2.0) @@ -1780,8 +1780,8 @@ packages: resolution: {integrity: sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==} dev: true - /@jixun/libparakeet@0.0.0-exp.14: - resolution: {integrity: sha512-PJCV+1v+6EM/TVomuKqxvm0GSZE58la7JeU690Jem+BGP4LXAE9aaDHvSljEbcokhar4NXI0j5Ggxrq/PaDEkw==} + /@jixun/libparakeet@0.0.0-exp.15: + resolution: {integrity: sha512-Le2Gl/2V6BsrK8NcFTtaelUkjqewCmDT3xZgUxOKjOo2lK1qvj1AtxarhfDtsOQVjvItiibuRPaRFPpLmWJe9g==} dev: false /@jridgewell/gen-mapping@0.3.3: diff --git a/src/decrypt-worker/crypto/CryptoBase.ts b/src/decrypt-worker/crypto/CryptoBase.ts index 3fb5636..744cb51 100644 --- a/src/decrypt-worker/crypto/CryptoBase.ts +++ b/src/decrypt-worker/crypto/CryptoBase.ts @@ -1,4 +1,8 @@ export interface CryptoBase { + /** + * When returning false, a successful decryption should be checked by its decrypted content instead. + */ + hasSignature(): boolean; isSupported(blob: Blob): Promise; decrypt(blob: Blob): Promise; } diff --git a/src/decrypt-worker/crypto/qmc/qmc_v1.ts b/src/decrypt-worker/crypto/qmc/qmc_v1.ts index c44706b..b5b8809 100644 --- a/src/decrypt-worker/crypto/qmc/qmc_v1.ts +++ b/src/decrypt-worker/crypto/qmc/qmc_v1.ts @@ -1,5 +1,5 @@ +import { transformBlob } from '~/decrypt-worker/util/transformBlob'; import type { CryptoBase } from '../CryptoBase'; -import { loadLibParakeet, factory, BlobSink, createArrayBufferReader, TransformResult } from '@jixun/libparakeet'; const key = new Uint8Array([ 0x77, 0x48, 0x32, 0x73, 0xde, 0xf2, 0xc0, 0xc8, 0x95, 0xec, 0x30, 0xb2, 0x51, 0xc3, 0xe1, 0xa0, 0x9e, 0xe6, 0x9d, @@ -19,33 +19,15 @@ const key = new Uint8Array([ ]); export class QMC1Crypto implements CryptoBase { - async isSupported(_blob: Blob): Promise { + hasSignature(): boolean { + return false; + } + + async isSupported(): Promise { return true; } async decrypt(blob: Blob): Promise { - const cleanup: (() => void)[] = []; - - try { - const mod = await loadLibParakeet(); - const transformer = factory.CreateQMCv1Transformer(mod, key); - cleanup.push(() => transformer.delete()); - - const reader = createArrayBufferReader(await blob.arrayBuffer(), mod); - cleanup.push(() => reader.delete()); - - const sink = new BlobSink(mod); - const writer = sink.getWriter(); - cleanup.push(() => writer.delete()); - - const result = transformer.Transform(writer, reader); - if (result !== TransformResult.OK) { - throw new Error(`transform failed with error: ${TransformResult[result]} (${result})`); - } - - return sink.collectBlob(); - } finally { - cleanup.forEach((clean) => clean()); - } + return transformBlob(blob, (p) => p.make.QMCv1(key)); } } diff --git a/src/decrypt-worker/crypto/xiami/xiami.ts b/src/decrypt-worker/crypto/xiami/xiami.ts index 352da08..e065e54 100644 --- a/src/decrypt-worker/crypto/xiami/xiami.ts +++ b/src/decrypt-worker/crypto/xiami/xiami.ts @@ -23,6 +23,10 @@ const u8Sub = (a: number, b: number) => { }; export class XiamiCrypto implements CryptoBase { + hasSignature(): boolean { + return true; + } + async isSupported(blob: Blob): Promise { const headerBuffer = await blob.slice(0, 0x10).arrayBuffer(); const header = new Uint8Array(headerBuffer); diff --git a/src/decrypt-worker/util/transformBlob.ts b/src/decrypt-worker/util/transformBlob.ts new file mode 100644 index 0000000..7c18412 --- /dev/null +++ b/src/decrypt-worker/util/transformBlob.ts @@ -0,0 +1,31 @@ +import { Transformer, Parakeet, TransformResult, fetchParakeet } from '@jixun/libparakeet'; + +export async function transformBlob( + blob: Blob, + transformerFactory: (p: Parakeet) => Transformer | Promise, + parakeet?: Parakeet +) { + const cleanup: (() => void)[] = []; + + try { + const mod = parakeet ?? (await fetchParakeet()); + const transformer = await transformerFactory(mod); + cleanup.push(() => transformer.delete()); + + const reader = mod.make.Reader(await blob.arrayBuffer()); + cleanup.push(() => reader.delete()); + + const sink = mod.make.WriterSink(); + const writer = sink.getWriter(); + cleanup.push(() => writer.delete()); + + const result = transformer.Transform(writer, reader); + if (result !== TransformResult.OK) { + throw new Error(`transform failed with error: ${TransformResult[result]} (${result})`); + } + + return sink.collectBlob(); + } finally { + cleanup.forEach((clean) => clean()); + } +} diff --git a/src/decrypt-worker/worker.ts b/src/decrypt-worker/worker.ts index de741e1..572090e 100644 --- a/src/decrypt-worker/worker.ts +++ b/src/decrypt-worker/worker.ts @@ -4,6 +4,7 @@ import { DECRYPTION_WORKER_ACTION_NAME } from './constants'; import type { CryptoFactory } from './crypto/CryptoBase'; import { XiamiCrypto } from './crypto/xiami/xiami'; import { QMC1Crypto } from './crypto/qmc/qmc_v1'; +import { fetchParakeet } from '@jixun/libparakeet'; const bus = new WorkerServerBus(); onmessage = bus.onmessage; @@ -16,14 +17,27 @@ const decryptorFactories: CryptoFactory[] = [ () => new QMC1Crypto(), ]; +// Use first 4MiB of the file to perform check. +const TEST_FILE_HEADER_LEN = 1024 * 1024 * 4; + bus.addEventHandler(DECRYPTION_WORKER_ACTION_NAME.DECRYPT, async (blobURI) => { const blob = await fetch(blobURI).then((r) => r.blob()); + const parakeet = await fetchParakeet(); for (const factory of decryptorFactories) { const decryptor = factory(); if (await decryptor.isSupported(blob)) { - const decrypted = await decryptor.decrypt(blob); - return { decrypted: URL.createObjectURL(decrypted) }; + const decryptedBlob = await decryptor.decrypt(blob); + + // Check if we had a successful decryption + const header = await decryptedBlob.slice(0, TEST_FILE_HEADER_LEN).arrayBuffer(); + const audioExt = parakeet.detectAudioExtension(header); + if (!decryptor.hasSignature() && audioExt === 'bin') { + // skip this decryptor result + continue; + } + + return { decrypted: URL.createObjectURL(decryptedBlob), ext: audioExt }; } }