diff --git a/package-lock.json b/package-lock.json index 4fed9de..3936c24 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "license": "MIT", "dependencies": { "@babel/preset-typescript": "^7.16.5", - "@jixun/qmc2-crypto": "^0.0.5-R4", + "@jixun/qmc2-crypto": "^0.0.6-R1", "@unlock-music/joox-crypto": "^0.0.1-R5", "base64-js": "^1.5.1", "browser-id3-writer": "^4.4.0", @@ -2986,9 +2986,9 @@ } }, "node_modules/@jixun/qmc2-crypto": { - "version": "0.0.5-R4", - "resolved": "https://registry.npmjs.org/@jixun/qmc2-crypto/-/qmc2-crypto-0.0.5-R4.tgz", - "integrity": "sha512-4xGClhxMd1BL7UjE+fZr+a4GYkfEjwU216WZ89ouANwR8Q27PhQrra+msEvM4J/mBBCjv/x/eIcS67XBasHKUQ==" + "version": "0.0.6-R1", + "resolved": "https://registry.npmjs.org/@jixun/qmc2-crypto/-/qmc2-crypto-0.0.6-R1.tgz", + "integrity": "sha512-G7oa28/tGozJIIkF2DS7RWewoDsKrmGM5JgthzCfB6P1psfCjpjwH21RhnY9RzNlfdGZBqyWkAKwXMiUx/xhNA==" }, "node_modules/@mrmlnc/readdir-enhanced": { "version": "2.2.1", @@ -23168,9 +23168,9 @@ } }, "@jixun/qmc2-crypto": { - "version": "0.0.5-R4", - "resolved": "https://registry.npmjs.org/@jixun/qmc2-crypto/-/qmc2-crypto-0.0.5-R4.tgz", - "integrity": "sha512-4xGClhxMd1BL7UjE+fZr+a4GYkfEjwU216WZ89ouANwR8Q27PhQrra+msEvM4J/mBBCjv/x/eIcS67XBasHKUQ==" + "version": "0.0.6-R1", + "resolved": "https://registry.npmjs.org/@jixun/qmc2-crypto/-/qmc2-crypto-0.0.6-R1.tgz", + "integrity": "sha512-G7oa28/tGozJIIkF2DS7RWewoDsKrmGM5JgthzCfB6P1psfCjpjwH21RhnY9RzNlfdGZBqyWkAKwXMiUx/xhNA==" }, "@mrmlnc/readdir-enhanced": { "version": "2.2.1", diff --git a/package.json b/package.json index 12644ef..ab4c957 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ }, "dependencies": { "@babel/preset-typescript": "^7.16.5", - "@jixun/qmc2-crypto": "^0.0.5-R4", + "@jixun/qmc2-crypto": "^0.0.6-R1", "@unlock-music/joox-crypto": "^0.0.1-R5", "base64-js": "^1.5.1", "browser-id3-writer": "^4.4.0", diff --git a/src/decrypt/qmc.ts b/src/decrypt/qmc.ts index c5cb845..973bfdd 100644 --- a/src/decrypt/qmc.ts +++ b/src/decrypt/qmc.ts @@ -1,9 +1,9 @@ import { QmcMapCipher, QmcRC4Cipher, QmcStaticCipher, QmcStreamCipher } from './qmc_cipher'; import { AudioMimeType, GetArrayBuffer, SniffAudioExt } from '@/decrypt/utils'; -import { DecryptQMCWasm } from './qmc_wasm'; import { DecryptResult } from '@/decrypt/entity'; import { QmcDeriveKey } from '@/decrypt/qmc_key'; +import { DecryptQMCWasm } from '@/decrypt/qmc_wasm'; import { extractQQMusicMeta } from '@/utils/qm_meta'; interface Handler { @@ -42,20 +42,27 @@ export async function Decrypt(file: Blob, raw_filename: string, raw_ext: string) const fileBuffer = await GetArrayBuffer(file); let musicDecoded: Uint8Array | undefined; + let musicID: number | string | undefined; if (version === 2 && globalThis.WebAssembly) { console.log('qmc: using wasm decoder'); + const v2Decrypted = await DecryptQMCWasm(fileBuffer); - // 如果 v2 检测失败,降级到 v1 再尝试一次 - if (v2Decrypted) { - musicDecoded = v2Decrypted; + // 若 v2 检测失败,降级到 v1 再尝试一次 + if (v2Decrypted.success) { + musicDecoded = v2Decrypted.data; + musicID = v2Decrypted.songId; + } else { + console.warn('qmc2-wasm failed with error %s', v2Decrypted.error || '(no error)'); } } + if (!musicDecoded) { // may throw error console.log('qmc: using js decoder'); const d = new QmcDecoder(new Uint8Array(fileBuffer)); musicDecoded = d.decrypt(); + musicID = d.songID; } const ext = SniffAudioExt(musicDecoded, handler.ext); @@ -65,6 +72,7 @@ export async function Decrypt(file: Blob, raw_filename: string, raw_ext: string) new Blob([musicDecoded], { type: mime }), raw_filename, ext, + musicID, ); return { @@ -81,19 +89,25 @@ export async function Decrypt(file: Blob, raw_filename: string, raw_ext: string) export class QmcDecoder { private static readonly BYTE_COMMA = ','.charCodeAt(0); - file: Uint8Array; - size: number; - decoded: boolean = false; - audioSize?: number; - cipher?: QmcStreamCipher; + private readonly file: Uint8Array; + private readonly size: number; + private decoded: boolean = false; + private audioSize?: number; + private cipher?: QmcStreamCipher; - constructor(file: Uint8Array) { + public constructor(file: Uint8Array) { this.file = file; this.size = file.length; this.searchKey(); } - decrypt(): Uint8Array { + private _songID?: number; + + public get songID() { + return this._songID; + } + + public decrypt(): Uint8Array { if (!this.cipher) { throw new Error('no cipher found'); } @@ -118,9 +132,20 @@ export class QmcDecoder { const sizeView = new DataView(sizeBuf.buffer, sizeBuf.byteOffset); const keySize = sizeView.getUint32(0, false); this.audioSize = this.size - keySize - 8; + const rawKey = this.file.subarray(this.audioSize, this.size - 8); const keyEnd = rawKey.findIndex((v) => v == QmcDecoder.BYTE_COMMA); + if (keyEnd < 0) { + throw new Error('invalid key: search raw key failed'); + } this.setCipher(rawKey.subarray(0, keyEnd)); + + const idBuf = rawKey.subarray(keyEnd + 1); + const idEnd = idBuf.findIndex((v) => v == QmcDecoder.BYTE_COMMA); + if (keyEnd < 0) { + throw new Error('invalid key: search song id failed'); + } + this._songID = parseInt(textEnc.decode(idBuf.subarray(0, idEnd)), 10); } else { const sizeView = new DataView(last4Byte.buffer, last4Byte.byteOffset); const keySize = sizeView.getUint32(0, true); diff --git a/src/decrypt/qmc_wasm.ts b/src/decrypt/qmc_wasm.ts index bec030e..f2ae3f2 100644 --- a/src/decrypt/qmc_wasm.ts +++ b/src/decrypt/qmc_wasm.ts @@ -1,5 +1,6 @@ import QMCCryptoModule from '@jixun/qmc2-crypto/QMC2-wasm-bundle'; import { MergeUint8Array } from '@/utils/MergeUint8Array'; +import { QMCCrypto } from '@jixun/qmc2-crypto/QMCCrypto'; // 检测文件末端使用的缓冲区大小 const DETECTION_SIZE = 40; @@ -7,16 +8,31 @@ const DETECTION_SIZE = 40; // 每次处理 2M 的数据 const DECRYPTION_BUF_SIZE = 2 * 1024 * 1024; +export interface QMC2DecryptionResult { + success: boolean; + data: Uint8Array; + songId: string | number; + error: string; +} + /** * 解密一个 QMC2 加密的文件。 * * 如果检测并解密成功,返回解密后的 Uint8Array 数据。 * @param {ArrayBuffer} mggBlob 读入的文件 Blob - * @return {Promise} */ -export async function DecryptQMCWasm(mggBlob: ArrayBuffer) { +export async function DecryptQMCWasm(mggBlob: ArrayBuffer): Promise { + const result: QMC2DecryptionResult = { success: false, data: new Uint8Array(), songId: 0, error: '' }; + // 初始化模组 - const QMCCrypto = await QMCCryptoModule(); + let QMCCrypto: QMCCrypto; + + try { + QMCCrypto = await QMCCryptoModule(); + } catch (err: any) { + result.error = err?.message || 'wasm 加载失败'; + return result; + } // 申请内存块,并文件末端数据到 WASM 的内存堆 const detectionBuf = new Uint8Array(mggBlob.slice(-DETECTION_SIZE)); @@ -34,12 +50,26 @@ export async function DecryptQMCWasm(mggBlob: ArrayBuffer) { const position = QMCCrypto.getValue(pDetectionResult, 'i32'); const len = QMCCrypto.getValue(pDetectionResult + 4, 'i32'); + result.success = detectOK; + result.error = QMCCrypto.UTF8ToString( + pDetectionResult + QMCCrypto.offsetof_error_msg(), + QMCCrypto.sizeof_error_msg(), + ); + const songId = QMCCrypto.UTF8ToString(pDetectionResult + QMCCrypto.offsetof_song_id(), QMCCrypto.sizeof_song_id()); + if (!songId) { + console.debug('qmc2-wasm: songId not found'); + } else if (/^\d+$/.test(songId)) { + result.songId = songId; + } else { + console.warn('qmc2-wasm: Invalid songId: %s', songId); + } + // 释放内存 QMCCrypto._free(pDetectionBuf); QMCCrypto._free(pDetectionResult); if (!detectOK) { - return false; + return result; } // 计算解密后文件的大小。 @@ -75,5 +105,7 @@ export async function DecryptQMCWasm(mggBlob: ArrayBuffer) { QMCCrypto._free(buf); hCrypto.delete(); - return MergeUint8Array(decryptedParts); + result.data = MergeUint8Array(decryptedParts); + + return result; }