Merge pull request #221 from unlock-music/feature/qmc-meta

feature(QMCv2): 通过 SongID 在线查询歌曲信息
This commit is contained in:
MengYX 2021-12-25 20:32:33 +08:00 committed by GitHub
commit 74d0a4137e
4 changed files with 81 additions and 24 deletions

14
package-lock.json generated
View File

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

View File

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

View File

@ -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);

View File

@ -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<Uint8Array|false>}
*/
export async function DecryptQMCWasm(mggBlob: ArrayBuffer) {
export async function DecryptQMCWasm(mggBlob: ArrayBuffer): Promise<QMC2DecryptionResult> {
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;
}