feat(qmcv2): Experiment with qmc2-crypto

(cherry picked from commit c8eb1bc481347efb6d35e9122e17e624bde18772)
master
Jixun Wu 10 months ago committed by MengYX
parent ada078df19
commit 840d750716
No known key found for this signature in database
GPG Key ID: E63F9C7303E8F604
  1. 11
      package-lock.json
  2. 1
      package.json
  3. 41
      src/decrypt/qmc.ts
  4. 95
      src/decrypt/qmcv2.ts

11
package-lock.json generated

@ -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",

@ -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",

@ -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<DecryptResult> {
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) {

@ -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<Uint8Array[]|null>}
*/
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);
}
}
Loading…
Cancel
Save