From ec6be66cc125f63c47a7a60c88025ce33a4656e7 Mon Sep 17 00:00:00 2001 From: Jixun Wu Date: Wed, 15 Dec 2021 19:59:06 +0000 Subject: [PATCH] refactor: restore support for QMCv1. (cherry picked from commit 19239f182d71e2e4362309f08706a91c00bb6bd1) --- src/decrypt/qmc.ts | 75 +++++++++++++---------------- src/decrypt/qmcv2.ts | 109 +++++++++++++++++++++++-------------------- 2 files changed, 91 insertions(+), 93 deletions(-) diff --git a/src/decrypt/qmc.ts b/src/decrypt/qmc.ts index 86ba4d9..6bc8117 100644 --- a/src/decrypt/qmc.ts +++ b/src/decrypt/qmc.ts @@ -1,4 +1,4 @@ -import {QmcMask, QmcMaskDetectMflac, QmcMaskDetectMgg, QmcMaskGetDefault} from "./qmcMask"; +import {QmcMask, QmcMaskGetDefault} from "./qmcMask"; import {toByteArray as Base64Decode} from 'base64-js' import { AudioMimeType, @@ -9,7 +9,7 @@ import { SniffAudioExt, WriteMetaToFlac, WriteMetaToMp3 } from "@/decrypt/utils"; import {parseBlob as metaParseBlob} from "music-metadata-browser"; -import {decryptMGG} from "./qmcv2"; +import {DecryptQMCv2} from "./qmcv2"; import iconv from "iconv-lite"; @@ -18,60 +18,49 @@ import {queryAlbumCover, queryKeyInfo, reportKeyUsage} from "@/utils/api"; interface Handler { ext: string - detect: boolean - - handler(data?: Uint8Array): QmcMask | undefined + version: number } export const HandlerMap: { [key: string]: Handler } = { - "mgg": {handler: QmcMaskDetectMgg, ext: "ogg", detect: true}, - "mflac": {handler: QmcMaskDetectMflac, ext: "flac", detect: true}, - "mgg.cache": {handler: QmcMaskDetectMgg, ext: "ogg", detect: false}, - "mflac.cache": {handler: QmcMaskDetectMflac, ext: "flac", detect: false}, - "qmc0": {handler: QmcMaskGetDefault, ext: "mp3", detect: false}, - "qmc2": {handler: QmcMaskGetDefault, ext: "ogg", detect: false}, - "qmc3": {handler: QmcMaskGetDefault, ext: "mp3", detect: false}, - "qmcogg": {handler: QmcMaskGetDefault, ext: "ogg", detect: false}, - "qmcflac": {handler: QmcMaskGetDefault, ext: "flac", detect: false}, - "bkcmp3": {handler: QmcMaskGetDefault, ext: "mp3", detect: false}, - "bkcflac": {handler: QmcMaskGetDefault, ext: "flac", detect: false}, - "tkm": {handler: QmcMaskGetDefault, ext: "m4a", detect: false}, - "666c6163": {handler: QmcMaskGetDefault, ext: "flac", detect: false}, - "6d7033": {handler: QmcMaskGetDefault, ext: "mp3", detect: false}, - "6f6767": {handler: QmcMaskGetDefault, ext: "ogg", detect: false}, - "6d3461": {handler: QmcMaskGetDefault, ext: "m4a", detect: false}, - "776176": {handler: QmcMaskGetDefault, ext: "wav", detect: false} + "mgg": {ext: "ogg", version: 2}, + "mgg1": {ext: "ogg", version: 2}, + "mflac": {ext: "flac", version: 2}, + "mflac0": {ext: "flac", version: 2}, + + "qmc0": {ext: "mp3", version: 1}, + "qmc2": {ext: "ogg", version: 1}, + "qmc3": {ext: "mp3", version: 1}, + "qmcogg": {ext: "ogg", version: 1}, + "qmcflac": {ext: "flac", version: 1}, + "bkcmp3": {ext: "mp3", version: 1}, + "bkcflac": {ext: "flac", version: 1}, + "tkm": {ext: "m4a", version: 1}, + "666c6163": {ext: "flac", version: 1}, + "6d7033": {ext: "mp3", version: 1}, + "6f6767": {ext: "ogg", version: 1}, + "6d3461": {ext: "m4a", version: 1}, + "776176": {ext: "wav", version: 1} }; -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 decodedParts = await decryptMGG(await file.arrayBuffer()); - let musicDecoded = mergeUint8(decodedParts); + const fileBuffer = await GetArrayBuffer(file); + let musicDecoded: Uint8Array; + if (handler.version === 1) { + const seed = QmcMaskGetDefault(); + musicDecoded = seed.Decrypt(new Uint8Array(fileBuffer)); + } else if (handler.version === 2) { + musicDecoded = await DecryptQMCv2(fileBuffer); + } else { + throw new Error(`不支持的加密版本: ${handler.version} (${raw_ext})`); + } const ext = SniffAudioExt(musicDecoded, handler.ext); const mime = AudioMimeType[ext]; - let musicBlob = new Blob(decodedParts, {type: mime}); + let musicBlob = new Blob([musicDecoded], {type: mime}); const musicMeta = await metaParseBlob(musicBlob); for (let metaIdx in musicMeta.native) { diff --git a/src/decrypt/qmcv2.ts b/src/decrypt/qmcv2.ts index fc7f7d8..68f11dc 100644 --- a/src/decrypt/qmcv2.ts +++ b/src/decrypt/qmcv2.ts @@ -1,21 +1,36 @@ import QMCCryptoModule from '@jixun/qmc2-crypto/QMC2-wasm-bundle'; -// EOF Magic detection. +// 检测文件末端使用的缓冲区大小 const DETECTION_SIZE = 40; -// Process in 2m buffer size +// 每次处理 2M 的数据 const DECRYPTION_BUF_SIZE = 2 * 1024 * 1024; +function MergeUint8Array(array: Uint8Array[]): Uint8Array { + let length = 0; + array.forEach(item => { + length += item.length; + }); + + let mergedArray = new Uint8Array(length); + let offset = 0; + array.forEach(item => { + mergedArray.set(item, offset); + offset += item.length; + }); + + return mergedArray; +} + /** * 解密一个 QMC2 加密的文件。 * - * 如果检测并解密成功,返回解密后的 Uint8Array 数组,按顺序拼接即可得到完整文件。 - * 若失败,返回 `null`。 + * 如果检测并解密成功,返回解密后的 Uint8Array 数据。 * @param {ArrayBuffer} mggBlob 读入的文件 Blob * @param {string} name 文件名 - * @return {Promise} + * @return {Promise} */ -export async function decryptMGG(mggBlob: ArrayBuffer) { +export async function DecryptQMCv2(mggBlob: ArrayBuffer) { // 初始化模组 const QMCCrypto = await QMCCryptoModule(); @@ -44,52 +59,46 @@ export async function decryptMGG(mggBlob: ArrayBuffer) { QMCCrypto._free(pDetectionBuf); QMCCrypto._free(pDetectionResult); - if (detectOK) { - // 计算解密后文件的大小。 - // 之前得到的 position 为相对当前检测数据起点的偏移。 - const decryptedSize = mggBlob.byteLength - DETECTION_SIZE + position; - // $prog.max = decryptedSize; + if (!detectOK) { + throw new Error("解密失败: \n " + detectionError); + } + + // 计算解密后文件的大小。 + // 之前得到的 position 为相对当前检测数据起点的偏移。 + const decryptedSize = mggBlob.byteLength - DETECTION_SIZE + position; - // 提取嵌入到文件的 EKey - const ekey = new Uint8Array( - mggBlob.slice(decryptedSize, decryptedSize + len) + // 提取嵌入到文件的 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)); - // 解码 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); + offset += blockSize; + bytesToDecrypt -= blockSize; } + QMCCrypto._free(buf); + hCrypto.delete(); + + return MergeUint8Array(decryptedParts); } \ No newline at end of file