From 0a52d2a20bf70a359de8da25750a99b1af96922f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=B2=81=E6=A0=91=E4=BA=BA?= Date: Wed, 15 Dec 2021 13:53:50 +0000 Subject: [PATCH] feat(qmcv2): Experiment with qmc2-crypto (cherry picked from commit c8eb1bc481347efb6d35e9122e17e624bde18772) --- package-lock.json | 11 +++++ package.json | 1 + src/decrypt/qmc.ts | 41 ++++++++++--------- src/decrypt/qmcv2.ts | 95 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 129 insertions(+), 19 deletions(-) create mode 100644 src/decrypt/qmcv2.ts diff --git a/package-lock.json b/package-lock.json index 5885b6a..9f70d66 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "MIT", "dependencies": { "@babel/preset-typescript": "^7.16.5", + "@jixun/qmc2-crypto": "^0.0.5-R2", "base64-js": "^1.5.1", "browser-id3-writer": "^4.4.0", "core-js": "^3.16.0", @@ -2980,6 +2981,11 @@ "regenerator-runtime": "^0.13.3" } }, + "node_modules/@jixun/qmc2-crypto": { + "version": "0.0.5-R2", + "resolved": "https://registry.npmjs.org/@jixun/qmc2-crypto/-/qmc2-crypto-0.0.5-R2.tgz", + "integrity": "sha512-omrsnXSx7BpOCY8Yla+xwil0bYz/4sj3qEFy4hu4JL/ujeWMzASKq9WnW+UHfSnLUw6EGstub+CoSXrFeRDfqQ==" + }, "node_modules/@mrmlnc/readdir-enhanced": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz", @@ -23019,6 +23025,11 @@ "regenerator-runtime": "^0.13.3" } }, + "@jixun/qmc2-crypto": { + "version": "0.0.5-R2", + "resolved": "https://registry.npmjs.org/@jixun/qmc2-crypto/-/qmc2-crypto-0.0.5-R2.tgz", + "integrity": "sha512-omrsnXSx7BpOCY8Yla+xwil0bYz/4sj3qEFy4hu4JL/ujeWMzASKq9WnW+UHfSnLUw6EGstub+CoSXrFeRDfqQ==" + }, "@mrmlnc/readdir-enhanced": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz", diff --git a/package.json b/package.json index 8545852..0671c51 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ }, "dependencies": { "@babel/preset-typescript": "^7.16.5", + "@jixun/qmc2-crypto": "^0.0.5-R2", "base64-js": "^1.5.1", "browser-id3-writer": "^4.4.0", "core-js": "^3.16.0", diff --git a/src/decrypt/qmc.ts b/src/decrypt/qmc.ts index 7d6366a..86ba4d9 100644 --- a/src/decrypt/qmc.ts +++ b/src/decrypt/qmc.ts @@ -9,6 +9,7 @@ import { SniffAudioExt, WriteMetaToFlac, WriteMetaToMp3 } from "@/decrypt/utils"; import {parseBlob as metaParseBlob} from "music-metadata-browser"; +import {decryptMGG} from "./qmcv2"; import iconv from "iconv-lite"; @@ -42,31 +43,35 @@ export const HandlerMap: { [key: string]: Handler } = { "776176": {handler: QmcMaskGetDefault, ext: "wav", detect: false} }; +function mergeUint8(array: Uint8Array[]): Uint8Array { + // Get the total length of all arrays. + let length = 0; + array.forEach(item => { + length += item.length; + }); + + // Create a new array with total length and merge all source arrays. + let mergedArray = new Uint8Array(length); + let offset = 0; + array.forEach(item => { + mergedArray.set(item, offset); + offset += item.length; + }); + + return mergedArray; +} + export async function Decrypt(file: Blob, raw_filename: string, raw_ext: string): Promise { if (!(raw_ext in HandlerMap)) throw `Qmc cannot handle type: ${raw_ext}`; const handler = HandlerMap[raw_ext]; - const fileData = new Uint8Array(await GetArrayBuffer(file)); - let audioData, seed, keyData; - if (handler.detect) { - const keyLen = new DataView(fileData.slice(fileData.length - 4).buffer).getUint32(0, true) - const keyPos = fileData.length - 4 - keyLen; - audioData = fileData.slice(0, keyPos); - seed = handler.handler(audioData); - keyData = fileData.slice(keyPos, keyPos + keyLen); - if (!seed) seed = await queryKey(keyData, raw_filename, raw_ext); - if (!seed) throw raw_ext + "格式仅提供实验性支持"; - } else { - audioData = fileData; - seed = handler.handler(audioData) as QmcMask; - if (!seed) throw raw_ext + "格式仅提供实验性支持"; - } - let musicDecoded = seed.Decrypt(audioData); + const decodedParts = await decryptMGG(await file.arrayBuffer()); + let musicDecoded = mergeUint8(decodedParts); const ext = SniffAudioExt(musicDecoded, handler.ext); const mime = AudioMimeType[ext]; - let musicBlob = new Blob([musicDecoded], {type: mime}); + let musicBlob = new Blob(decodedParts, {type: mime}); const musicMeta = await metaParseBlob(musicBlob); for (let metaIdx in musicMeta.native) { @@ -80,8 +85,6 @@ export async function Decrypt(file: Blob, raw_filename: string, raw_ext: string) } const info = GetMetaFromFile(raw_filename, musicMeta.common.title, musicMeta.common.artist) - if (keyData) reportKeyUsage(keyData, seed.getMatrix128(), - raw_filename, raw_ext, info.title, info.artist, musicMeta.common.album).then().catch(); let imgUrl = GetCoverFromFile(musicMeta); if (!imgUrl) { diff --git a/src/decrypt/qmcv2.ts b/src/decrypt/qmcv2.ts new file mode 100644 index 0000000..17b00cf --- /dev/null +++ b/src/decrypt/qmcv2.ts @@ -0,0 +1,95 @@ +import QMCCryptoModule from '@jixun/qmc2-crypto'; + +// EOF Magic detection. +const DETECTION_SIZE = 40; + +// Process in 2m buffer size +const DECRYPTION_BUF_SIZE = 2 * 1024 * 1024; + +/** + * 解密一个 QMC2 加密的文件。 + * + * 如果检测并解密成功,返回解密后的 Uint8Array 数组,按顺序拼接即可得到完整文件。 + * 若失败,返回 `null`。 + * @param {ArrayBuffer} mggBlob 读入的文件 Blob + * @param {string} name 文件名 + * @return {Promise} + */ +export async function decryptMGG(mggBlob: ArrayBuffer) { + // 初始化模组 + const QMCCrypto = await QMCCryptoModule(); + + // 申请内存块,并文件末端数据到 WASM 的内存堆 + const detectionBuf = new Uint8Array(mggBlob.slice(-DETECTION_SIZE)); + const pDetectionBuf = QMCCrypto._malloc(detectionBuf.length); + QMCCrypto.writeArrayToMemory(detectionBuf, pDetectionBuf); + + // 检测结果内存块 + const pDetectionResult = QMCCrypto._malloc(QMCCrypto.sizeof_qmc_detection()); + + // 进行检测 + const detectOK = QMCCrypto.detectKeyEndPosition( + pDetectionResult, + pDetectionBuf, + detectionBuf.length + ); + + // 提取结构体内容: + // (pos: i32; len: i32; error: char[??]) + const position = QMCCrypto.getValue(pDetectionResult, "i32"); + const len = QMCCrypto.getValue(pDetectionResult + 4, "i32"); + const detectionError = QMCCrypto.UTF8ToString(pDetectionResult + 8); + + // 释放内存 + QMCCrypto._free(pDetectionBuf); + QMCCrypto._free(pDetectionResult); + + if (detectOK) { + // 计算解密后文件的大小。 + // 之前得到的 position 为相对当前检测数据起点的偏移。 + const decryptedSize = mggBlob.byteLength - DETECTION_SIZE + position; + // $prog.max = decryptedSize; + + // 提取嵌入到文件的 EKey + const ekey = new Uint8Array( + mggBlob.slice(decryptedSize, decryptedSize + len) + ); + + // 解码 UTF-8 数据到 string + const decoder = new TextDecoder(); + const ekey_b64 = decoder.decode(ekey); + + // 初始化加密与缓冲区 + const hCrypto = QMCCrypto.createInstWidthEKey(ekey_b64); + const buf = QMCCrypto._malloc(DECRYPTION_BUF_SIZE); + + const decryptedParts = []; + let offset = 0; + let bytesToDecrypt = decryptedSize; + while (bytesToDecrypt > 0) { + const blockSize = Math.min(bytesToDecrypt, DECRYPTION_BUF_SIZE); + + // 解密一些片段 + const blockData = new Uint8Array( + mggBlob.slice(offset, offset + blockSize) + ); + QMCCrypto.writeArrayToMemory(blockData, buf); + QMCCrypto.decryptStream(hCrypto, buf, offset, blockSize); + decryptedParts.push(QMCCrypto.HEAPU8.slice(buf, buf + blockSize)); + + offset += blockSize; + bytesToDecrypt -= blockSize; + // $prog.value = offset; + + // 避免网页卡死,让 event loop 处理一下其它事件。 + // Worker 应该不需要等待也可以? + // await new Promise((resolve) => setTimeout(resolve)); + } + QMCCrypto._free(buf); + hCrypto.delete(); + + return decryptedParts; + } else { + throw new Error("ERROR: could not decrypt\n " + detectionError); + } +} \ No newline at end of file